本当に誰でも読める!! Pythonソースコードの読み方講座(第8回)Python コードを俯瞰する 1:コードの領域分割と実行順序

NTTデータ数理システム MSIISM Conference 2024 NTTデータ数理システム MSIISM Conference 2024
  • HOME
  • 本当に誰でも読める!! Pythonソースコードの読み方講座(第8回)Python コードを俯瞰する 1:コードの領域分割と実行順序

本記事は当社が発行しているシミュレーションメールマガジンVol.8の記事です。 シミュレーションメールマガジンの詳細・購読申込はこちら のサポートページから

プログラマにもノン・プログラマにも読んでいただきたい「Pythonコードの読み方」講座の第8回、今回は 「Python コードを俯瞰する」と題して、Python コードの全体像について解説します。とりわけ、「コードのどの部分がいつ実行されるのか」に注目して整理をしていきたいと思います。 コードを書く方にとっても有用な視点になると思いますのでぜひご一読ください。

例文

早速ですが今回の例文になります。 コードに慣れている方は、どのような print の結果が得られるかを想像しながら見るとよいでしょう。

print("step0: プログラム開始")
import module

def my_function(x,y):
    print("step1: my_functions の呼び出し")
    return x+y

class MyClass0:
    print("step2: MyClass0 の定義")
    val = 0
    def __init__(self, x):
        print("step3: MyClass0 インスタンスの初期化")
        self.x = x
    def method(self, y):
        print("step4: MyClass0 インスタンスの method の呼び出し")
        return self.val + self.x + y
    print("step5: MyClass0 の定義終了")

class MyClass1(MyClass0):
    print("step6: MyClass1 の定義開始")
    val = 10
    class InnerClass:
        print("step7: InnerClass の定義開始")
        val = 100
    def method(self, y):
        print("step8: MyClass1 の method の呼び出し")
        return self.val + self.InnerClass.val + self.x + y
    print("step9: MyClass1 の定義終了")

if __name__=="__main__":
    print("step10: メイン処理の開始")
    my_function(1,2)
    obj0 = MyClass0(10)
    print(obj0.method(0))
    print(obj0.method(10))
    obj1 = MyClass1(100)
    print(obj1.method(0))
    print(obj1.method(10))
    print("step11: メイン処理の終了")

やや複雑ですが、コードの流れだけ見ていただければ、何をやっているかは気にしなくて構いません。クラスに関する読み方については第2回を、関数に関する読み方については第5回も参照ください。

コードを俯瞰で見る

一般に、ソースコードはいくつかの領域に分割して解釈することができます。そしてコードがどの領域に属しているかによって

  • いつ実行されるのか (定義時 or 実行時)
  • どの範囲の変数 (変数スコープ)

が変化します。 今回は1つ目の、「コードがいつ実行されるのか」に注目して説明をしていきたいと思います。

先程のサンプルコードを俯瞰してみると次のような形で領域に分割することができます:
(呼称についてはこの記事の説明のために付けたもので、必ずしも一般的なものとは限りません)

以下、個々の領域について説明していきます。

グローバル領域

スクリプト全体を覆う領域です。冒頭の print 関数の呼び出しや import 文のように実行文がある他、各関数やクラスの定義など、他の領域の土台にもなっています。 グローバル領域はこのスクリプトが実行される (python コマンドで呼び出される) か、他のスクリプトからインポートされた時点で1度だけ実行されます。

関数内ローカル領域

関数を定義(=どのような処理を行うのか記述)する領域です。この領域はグローバル領域を実行している間には実行されることはありません(定義のためのコンパイル処理だけが行われます)。サンプルコードの33行目のように関数が呼び出されて初めて実行されます。

クラス内グローバル領域

クラス定義の直下に位置する領域で、クラス変数やクラスのメソッドの定義が行われます。この領域はグローバル領域と同様に、スクリプトが実行/インポートされた際に一度だけ実行されます。 この MyClass0 クラスの中に新たなグローバル領域が作られているようなイメージになります。

メソッド内ローカル領域

メソッド(=クラス内の関数)を定義する領域です。関数内ローカル領域と同じように、実際呼び出されて初めて実行されます。

  • グローバル領域にとっての関数内ローカル領域
  • クラス内グローバル領域にとってのメソッド内ローカル領域

が同じような関係になっていると思うとわかりやすいと思います。

ネストしたクラス内グローバル領域

ややトリッキーですが、クラスの中でクラスを定義することも可能です。グローバル領域→ クラス内グローバル領域が同時に実行されるように、クラス内グローバル領域→ ネストしたクラス内グローバル領域も同時に実行されます。 したがって、ネストしたクラス内のグローバル領域も、スクリプトが実行/インポートされた際に一度だけ実行されます。

グローバル領域(メイン処理部分)

サンプル末尾の

if __name__=="__main__":

から始まる if 節はグローバル領域に含まれます。通常、if に限らず、forwhile などの構文を表すキーワードでブロックが作られても、領域はグローバルのまま変化することはありません。 しかし、この

if __name__=="__main__":

は少し特殊な挙動をします。

__name__ はこのスクリプトを呼び出しているモジュールを指す変数で、このスクリプト自体が python コマンドによって実行されている場合に "__main__" の値をとります。 したがって、この if 内のブロックは

  • このスクリプトが python コマンドで実行されている場合には実行される
  • それ以外の場合 (他スクリプトがこのスクリプトをインポートした場合など) には実行されない

ということになります。 この形の if ブロックはしばしば利用されます。グローバル領域ではありますが、スクリプトを呼び出す方法によって実行されるかどうかが変化するため、やや特殊な領域に位置づけられるでしょう。

以上をふまえると、サンプルコードを実行した場合の出力は次のようになります:

step0: プログラム開始
step2: MyClass0 の定義
step5: MyClass0 の定義終了
step6: MyClass1 の定義開始
step7: InnerClass の定義開始
step9: MyClass1 の定義終了
# ここ以降は import しただけの場合は表示されない
step10: メイン処理の開始
step1: my_functions の呼び出し
step3: MyClass0 インスタンスの初期化
step4: MyClass0 インスタンスの method の呼び出し
step3: MyClass0 インスタンスの初期化
step8: MyClass1 の method の呼び出し
step11: メイン処理の終了

その他のケース

今回のサンプルコードには含めませんでしたが、以下のようなコード部分はどのような領域に属する(どのタイミングで実行される)でしょうか?

関数定義の引数のデフォルト値

第5回で扱ったように、関数には引数の値のデフォルト値を設定することができます:

def function(x = other_function(), y = 1+2):
    return x+y

このデフォルト値を記述している部分は*グローバル領域* (より正確には、関数定義が行われている領域と同じ領域扱い)になります。したがって上の例の other_function 関数の呼び出しや 1+2 の計算は一回しか行われません。 この性質のため、引数のデフォルト値に mutable なものを指定してしまうと、呼び出しごとにデフォルト値が変更される可能性があるため危険です。

ネストした関数やクラス定義

クラス内でクラス定義を行う例を示しましたが、他のケースではどうなるでしょうか?

def function(x):
    print("step1: function 実行")
    def inner_function(y):
        print("step2: inner_function 実行")
        return y
    class InnerClass:
        print("step3: InnerClass 定義")
        val = 0
        def __init__(self, z):
            print("step4: InnerClass 初期化")
            self.z = z
    obj = InnerClass(10)
    res = inner_function(obj)
    print("step5: function 終了")
    return res

この例では関数内に関数とクラスを定義してそれらを呼び出しています。 この場合は inner_functionsInnerClass 自体が関数内ローカル領域に置かれているため、これらの定義は function 関数が呼ばれて初めて行われます。 InnerClass 直下のクラス内領域は関数内ローカル領域と続けて実行されるので、表示順は以下のようになります:

step1: function 実行
step3: InnerClass 定義
step4: InnerClass 初期化
step2: inner_function 実行
step5: function 終了

まとめ

  • グローバル領域(緑) : スクリプトが呼び出されたとき、もしくはインポートされたときに上から順に実行される (ただし末尾の if ブロックはインポートされたときには実行されない)
  • ローカル領域(橙) : 関数やメソッドの中身であり、実際に呼び出されてはじめて実行される。

のようになります。

今回はコードを俯瞰し、各領域の実行タイミングという観点で整理を行いました。少しでもコードを読む際の負荷を減らせれば幸いです。 次回は「各領域から他の領域がどのように見えているか」という観点での整理を行いたいと思います。

豊岡 祥 株式会社 NTTデータ数理システム シミュレーション&マイニング部所属。
S4 Simulation System の開発のほか、機械学習・シミュレーション・数理最適化を幅広く扱い、分野を横断した問題解決に取り組んでいる。
入社後に競技プログラミングをはじめ、PGBATTLE2023にて企業の部団体3位入賞。

著書: 「XAI(説明可能なAI)──そのとき人工知能はどう考えたのか?」リックテレコム
趣味: 合唱
「数理科学の基礎知識」e-book無料ダウンロードはこちら