Python♪関数のスコープは3層入れ子構造(ネスト)の具体例で理解しよう。

 スコープの説明って、本読んでもわかりにくいんですよね~。具体例がなければ難しいと思いませんか。そこで、3層の入れ子構造(ネスト)の関数xxx1(), xxx2(), xxx3()と、1層の関数yyy1()の計4つの関数があるコード01について図を用いて説明します。解説は図の中で具体的に行います。

 ちなみに、私はどこからでも使えるグローバル変数を多用するのは嫌いです。もちろん頼ることもあるし、ないと困りますけどね。
 何が関数への入力値なのか、はっきりしていないコードは読み返したときに「わっか・り・ませ~ん。おてあげ。」となります。特に長いコードでは気が狂います。
 Pythonは引数を用いなくても関数の外の値を参照できますが、きちんと引数を使ったり、コード内にコメントで残しておくなど、関数への入出力を明確にする工夫をしないと大変だと思います。

 だからこそ、どの変数がどの範囲まで影響をあたえるか、スコープを知っておくことは関数を使うときの必須事項です。

 Pythonって、できるだけ型宣言はしない方針の言語ですよね。コードの行数も減るし、使いやすいですが、その分、仕様を頭にたたきこんでおかないと、思わぬ事故が起こります。初心者用の言語といわれているけれど、単純にそうとはいえないかな?

0.チュートリアル学習のポイントシリーズ

この記事は「Pythonチュートリアルのポイント整理シリーズ」の記事です。一連の記事は、以下のリンク集を参照してください。
「Pythonチュートリアル」のポイント整理シリーズ

1.スコープとは

 スコープとは、コード内での変数の有効範囲のことです。そして、Pythonには以下の4つのスコープがあります。(書籍とかを読んでも、参照する関数側から見たスコープと、変数を宣言した側からみたスコープの説明が混在していて、わかりにくいんですよね~。)

(1) ローカルスコープ(Local scope)
 関数の内側で宣言された変数は「ローカル変数」といいます。そして、このローカル変数のスコープを「ローカルスコープ」といいます。
 ローカルスコープは、変数の宣言を行った関数の内側です。関数の外側からは関数の内側の変数を参照することはできませんので、外側はローカルスコープの範囲外です。
 なお、関数が入れ子構造になっている場合、変数が定義された位置よりも外側の関数からはその変数を参照できませんが、内側の関数からはその変数を参照することが可能です。つまり、定義された変数の位置よりも内側にある関数の中もローカルスコープに含まれます。 

(2) 外側の関数のスコープ(Enclosing function's scope)
 「外側の関数のスコープ」は、関数が入れ子の構造(ネスト)になっている場合を想定しています。このとき、内側の関数から見て、外側の関数でローカル変数が定義されているときに、その外側の関数のローカル変数のスコープを「外側の関数のスコープ」といいます。
 ここで重要なのは内側の関数から外側の関数のスコープを見ている点です。視点が変わっているだけなので、難しく考える必要はありません。
 スコープの優先順位の説明をするときなど、この用語を使うと便利な場合があります。
 なお、グローバル変数については、関数から見て外側で定義されていますが、関数で定義されたローカル変数ではないので「外側の関数のスコープ」の対象になりません。

(3) グローバルスコープ(Global Scope)
 モジュール(Pythonのファイル「ファイル名.py」に記されたコード全体のこと)のトップレベル(全ての関数やクラスの外側)で宣言された変数をグローバル変数といいます。そして、このグローバル変数のスコープを「グローバルスコープ」といいます。「グローバルスコープ」はモジュール全体です。つまり、グローバル変数は、モジュールの中なら、どこからでも参照することができます。

(4) ビルトインスコープ(Built-in scope )
 「ビルトインスコープ」とは、組み込み関数や組み込み変数のスコープです。組み込みであるため、変数や関数を宣言する必要はありません。ビルトインスコープはプログラム全体です。Pythonを起動した時点で、どのモジュールからでも呼び出すことが可能です。組み込み関数や組み込み変数には、int, str, len, range, None, print などがあります。

 それぞれのスコープが重なった時の優先順位は「ローカルスコープ」「外側の関数のスコープ」「グローバルスコープ」「ビルトインスコープ」です。なお、「外側の関数」が複数ある場合は内側の関数の方が優先順位が高いです。(「外側の関数のスコープ」という言葉が無いと、このような説明が大変になります。)

 関数の外側の変数を参照することは可能ですが、関数の外から関数の内側の変数を参照することはできません。ただし、global, nonlocalを用いて定義された変数は例外です。
 また、関数の外側の変数を参照できるのは、外側から内側の関数に「参照渡し」をされているためです。したがって、変更不能体(イミュータブル)の変更(例:b=2)は、外側の関数に影響しません。一方、リストの様な変更可能体(ミュータブル)の要素の変更(例:b[0]=2)は、外側の関数に影響を与えます。また、要素の変更ではなく、変数に変更可能体を再代入(例:b=[2])した場合は、変更可能体であっても外側の関数に影響しません。

2.サンプルコードによる説明

 具体的な例示でなければ分かりにくいと思いますので、サンプルコードによる説明を行います。3層以上の入れ子構造(ネスト)になっている例や、関数が並列で並べられているような例は少ないので、参考になるのではないでしょうか。

(1) 説明で使用するサンプルコード

以下のコード01を用いてスコープの説明をしたいと思います。
実行してもなにも出力されないまったく意味のないコードですが、できるだけシンプルにするために、よけいなコードは省略しました。

関数xxx1(), xxx2(), xxx3()は、それぞれ、入れ子構造(ネスト)になっており、関数xxx1()の中で関数xxx2()が定義されており、さらに関数xxx2()の中で関数xxx3()が定義されています。
また、関数xxx2(), xxx3()は、それぞれ定義された直後に実行されています。さらに、関数yyy1()は関数xxx3()の中で実行されています。また、変数x0~5, x9, y1は、それぞれのスコープが様々な形で重なるように配置しています。

#コード01
x0 = 0
x5 = 0
def xxx1():
    x1 = 1
    x5 = 5
    def xxx2():
        x1 = 7
        x2 = 2
        def xxx3():
            x3 = 3
            yyy1()
        xxx3()
    xxx2()
    
def yyy1():
    y1 = 1

if __name__ == '__main__':
    x9 = 9
    xxx1()

なお、スコープの説明からは少し脱線しますが、コード01の関数の定義の位置とその関数を実行する位置の関係について簡単に説明します。
コード01の16~17行目で定義された関数yyy1()は、それよりも前の行の12行目で実行することできます。これは、21行目でxxx1()が実行された時にはすでにyyy1()が、定義されているからです。
一方、7~13行目の関数xxx2()を、例えば6行目と7行目の間で実行しようとしても実行できません。これは、例えばローカル変数をprint()で出力するときに、出力するローカル変数を定義したあとでprint()文を実行しなければならないのと似ています。
入れ子構造の内側の関数は、関数の定義行より後の行で実行しましょう。

(2) スコープが重なっている場合の優先順位

下図は上記のコード01について、スコープの範囲と、スコープが重なっている場合の優先順位を示したものです。大きな図ですが、コード01のそれぞれのスコープについて説明します。

(3) 参照可能な「変数」を具体的に図示

 今度は、更に具体的に、それぞれの関数の中でどの変数を参照することができるかを図に書き込みました。こちらの方が具体的でわかりやすいという人もいると思います。なお、どこからでも参照することができるビルトインスコープは図から外しました。

(4) 参照可能な「関数」を具体的に図示

最後に、それぞれの関数の中でどの関数を参照することができるかを図に書き込みました。なお、こちらもビルトインスコープは図から外しました。変数の場合と同じルールで優先順位がつけられていることがわかります。

(5) 同名の関数について

まず、入れ子構造(ネスト)の関数同士の名前が同じでもエラーにはなりません。ただし、バグの原因となるので、同じ名前にするのは避けましょう。

次に、コード02, コード03のように一番上の階層に定義された関数同士の名前が同じ場合も、エラーにはなりません。しかし、この時、先に読み込まれた関数は、後述の関数に上書きされるようですので、同じ名前の関数を用意する意味がありません。引数が違う同名の関数も別の関数とは扱われません。
 いっそのこと、エラーにしてくれた方がよいと思うのですが、どうなんでしょう。私の知らない有用な使い方があるのでしょうか・・・。

 コード02は、同じ名前の関数xxx()を並べた例です。エラーは生じていません。後ろの関数の「ティラミス」が出力されました。

#コード02
def xxx():
    print('イチゴショート')

def xxx():
    print('ティラミス')

xxx()
#出力02
ティラミス

 次に、コード03では、イチゴショートとティラミスの関数を逆にしてみます。すると、出力も逆になりました。

#コード03
def xxx():
    print('ティラミス')

def xxx():
    print('イチゴショート')

xxx()
#出力03
イチゴショート

では、引数の数が違う同名の関数ではどうでしょうか。引数が違うため、同名でも違う関数として扱われるのでしょうか。

検証してみると次のようになります。コード04は、実行可能ですが、コード05はエラーとなります。やはり、引数の数を変えても違う関数とはみなされず、後で記述した方の関数に上書きされるようです。

#コード04
def xxx():
    print('ティラミス')

def xxx(cake):
    print(cake,'イチゴショート')

xxx('モンブラン')
#出力04
モンブラン イチゴショート
#コード05
def xxx(cake):
    print(cake,'イチゴショート')

def xxx():
    print('ティラミス')

xxx('モンブラン')
#出力05
TypeError: xxx() takes 0 positional arguments but 1 was given

(6) うっかり間違える(?)例題

コード06はyyy()の内側にxxx()がある「入れ子構造の関数」ですが、コード07は、xxx()とyyy()は、互いに入れ子構造にはなっていません。どちらも、関数yyy()の中でxxx()を呼び出しています。

#コード06
def yyy():
    def xxx():
        print('a =',a) 
    xxx()
    
a = 1 
yyy()
#出力06
a = 1
#コード07
def xxx():
    print('a =',a) 

def yyy():
    xxx()

a = 1
yyy()
#出力07
a = 1

次に、それぞれxxx()を実行する直前にa = 10というコードを挿入してみました。コード08では、入れ子構造の関数なので、yyy()のローカル変数の値がxxx()に影響を与え、4行目の出力は10になります。

一方、コード09のように、関数yyy()から関数xxx()を呼び出す場合、入れ子構造ではないxxx()の中の変数に影響を与えることはありませんので出力は1のままです。

これは、関数のスコープの基本的な内容なので、慣れている人にはあたりまえのことかもしれませんが、ついつい混同してしまいますので気をつけましょう。

#コード08
def yyy():
    def xxx():
        print('a =',a) # 1ではなく、10が出力。
    a = 10
    xxx()
    
a = 1 
yyy()
#出力08
a = 10
#コード09
def xxx():
    print('a =',a) # 10ではなく、1が出力。

def yyy():
    a = 10 #つまり、この行は意味がない
    xxx()

a = 1
yyy()
#出力09
a = 1

(7) 入れ子構造の内側から呼び出した場合のreturn

せっかくですので、入れ子構造の内側から外側の関数を呼び出した場合のreturnの働きについて調べてみます。ここで重要なのは、「returnは関数を抜ける」とだけ覚えるのではなく、「returnは関数を抜け、関数を最後に呼び出した位置にもどる」と覚えた方がよいことです。

入れ子構造の関数の内側から外側の関数を呼び出す方法を使うケースは多くないと思いますので、さっと目を通して難しいようでしたら、ここから後は読み飛ばしてください。

コード10のリストtmpは変更可能体(ミュータブル)ですので、9行目の要素の変更はこのコードのtmp全てに影響を与えます。つまり、最初はtmp[0] = 0ですが、内側の関数xxx2()が実行されたあとは、xxx2()の外側でもtmp[0] = 1となります。tmp[0] = 1になると、xxx1()は5~7行目のreturnにより13行目のxxx2を呼び出すコードが実行されることなく終了します。もし、5~7行目のコードがなければ、11行目のxxx1を呼び出すコードと13行目のxxx2を呼び出すコードが交互に実行され、無限ループになり、「xxx1-start」と「xxx2-start」が延々と出力されます。入れ子構造の内側から外側の関数を呼び出す場合には、注意深くコーディングする必要があります。

さて、コード10は7行目のreturunの実行により、どの位置にもどるのかを検証するためのサンプルコードです。

#コード10
tmp=[0]
def xxx1():
    print('xxx1-start')
    if tmp[0] == 1:
        print('return発動')
        return
    def xxx2():
        print('xxx2-start')
        tmp[0] = 1
        xxx1()
        print('xxx2-end')
    xxx2()
    print('xxx1-end')
xxx1()
print('end')
#出力10
xxx1-start
xxx2-start
xxx1-start
return発動
xxx2-end
xxx1-end
end

それでは、迷路のようなコード10の実行順序を図示してみます。15行目「②xxx1の1回目の呼出」では、tmp[0]=0なので、5~7行目のコードは実行されません。7行目のreturnが実行されるのは11行目「⑥xxx1の2回目の呼出」により、関数xxx1が実行されたときです。つまり、このコードでは7行目のreturn実行後は関数を抜けて15行目にもどるのではなく、最後にxxx1を呼び出した11行目にもどります。

先ほど「returnは関数を抜ける」とだけ覚えるのではなく、「returnは関数を抜け、関数を最後に呼び出した位置にもどる」と覚えた方がよいと言ったのはこのことです。



3. おまけ記事:検証コード(同じ名前の関数の呼び出し)

以下、名前が同じ関数がある場合に、どの関数が優先的に読み込まれるのかを検証したコードです。記事の内容を検証するために作成したコードであり、だらだらと長く読みにくいです。実際のコードを用いて自分でも検証したい方だけ参考にしてください。

(1) 関数名が全て異なる例

コード11は関数名が全て異なる例です。xxx1(), xxx2(), xxx3()が入れ子になっており、最も内側の関数xxx3(13行目)の中の18行目から一番外側のxxx1(3行目)を呼び出しています。なお、xxx3を実行するときにtmp[0] の値を 0→1とし、2回目以降の関数の実行はreturnですぐにもどってくるようにしているため、無限ループにはならないようになっています。内側の関数xxx3()から外側の関数xxx1()を呼び出すことができました。

説明が繰り返しになりますが、「returnは関数を抜ける」とだけ覚えるのではなく、「returnは関数を抜け、関数を最後に呼び出した位置にもどる」と認識することにより、コードが理解しやすくなります。理解できている人には当たり前のことかもしれませんが、7行目のreturnは28行目の位置にもどるのではなく、関数xxx1を最後に呼び出した18行目にもどります。

#コード11
tmp = [0]
def xxx1():
    print('xxx_1番目')
    if tmp[0] == 1:
        print('return_1番目')
        return
    def xxx2():
        print('xxx_2番目')
        if tmp[0] == 1:
            print('return_2番目')
            return
        def xxx3():
            print('xxx_3番目')
            if tmp[0] == 0:
                tmp[0] = 1
                print('tmp = 1')
                xxx1()
            yyy1()
            print('xxx_3番目_end')
        xxx3()
    xxx2()
    
def yyy1():
    print('yyy1')
    
if __name__ == '__main__':
    xxx1()
#出力11
xxx_1番目
xxx_2番目
xxx_3番目
tmp = 1
xxx_1番目
return_1番目
yyy1
xxx_3番目_end

(2) 1番外側と、外から2番目の関数の名前が同じ場合

コード12は、コード07においてxxx2()となっていた関数名をxxx1()に変更しました。したがって、xxx1()という関数名が2つ存在します。最も内側のxxx3()の18行目でxxx1()を呼び出していますが、より内側の8行目のxxx1()の方が呼び出されていることがわかります。

また、22行目のxxx1()の呼び出しにおいて、3行目ではなく8行目のxxx1()が呼び出されていることもわかります。

#コード12
tmp = [0]
def xxx1():
    print('xxx_1番目')
    if tmp[0] == 1:
        print('return_1番目')
        return
    def xxx1():
        print('xxx_2番目')
        if tmp[0] == 1:
            print('return_2番目')
            return
        def xxx3():
            print('xxx_3番目')
            if tmp[0] == 0:
                tmp[0] = 1
                print('tmp = 1')
                xxx1()
            yyy1()
            print('xxx_3番目_end')
        xxx3()
    xxx1()
    
def yyy1():
    print('yyy1')
    
if __name__ == '__main__':
    xxx1()
#出力12
xxx_1番目
xxx_2番目
xxx_3番目
tmp = 1
xxx_2番目
return_2番目
yyy1
xxx_3番目_end

(3) 入れ子構造の部分が全て同じ名前の場合

コード13はコード07のxxx2(), xxx3()の名前をxxx1()に変更し、入れ子構造になっている部分の関数名を全てxxx1()にしました。入れ子構造の関数では、同じ名前の関数がある場合、例えば22行目のxxx1()では、同じ関数内にある8行目のxxx1()を優先しています。そして、もし、18行目のように同じ関数内にxxx1()がなければ、外側の関数を近いほう(13行目)から順番に探していることがわかります。

#コード13
tmp = [0]
def xxx1():
    print('xxx_1番目')
    if tmp[0] == 1:
        print('return_1番目')
        return
    def xxx1():
        print('xxx_2番目')
        if tmp[0] == 1:
            print('return_2番目')
            return
        def xxx1():
            print('xxx_3番目')
            if tmp[0] == 0:
                tmp[0] = 1
                print('tmp = 1')
                xxx1()
            yyy1()
            print('xxx_3番目_end')
        xxx1()
    xxx1()
    
def yyy1():
    print('yyy1')
    
if __name__ == '__main__':
    xxx1()
#出力13
xxx_1番目
xxx_2番目
xxx_3番目
tmp = 1
xxx_3番目
yyy1
xxx_3番目_end
yyy1
xxx_3番目_end

(4) 入れ子構造の関数の外側にも同じ関数名がある場合

コード14は、入れ子構造の関数の中(8, 13行目)だけではなく、入れ子構造の外側(23行目)にも同じ名前の関数があるケースです。18行目のyyy1()の呼び出しに対して、23行目ではなく、13行目が呼び出されています。

#コード14
tmp = [0]
def xxx1():
    print('xxx_1番目')
    if tmp[0] == 1:
        print('return_1番目')
        return
    def yyy1():
        print('xxx_2番目')
        if tmp[0] == 1:
            print('return_2番目')
            return
        def yyy1():
            print('xxx_3番目')
            if tmp[0] == 0:
                tmp[0] = 1
                print('tmp = 1')
                yyy1()
            print('xxx_3番目_end')
        yyy1()
    yyy1()
    
def yyy1():
    print('yyy1')
    
if __name__ == '__main__':
    xxx1()
#出力14
xxx_1番目
xxx_2番目
xxx_3番目
tmp = 1
xxx_3番目
xxx_3番目_end
xxx_3番目_end

(5) 一番上の階層の関数名が同じ場合

なお、コード15では優先順位を検証する以前に、3行目の関数xxx1()は、その後に記述された23行目のxxx1()によって、上書きされているため、27行目のxxx1()の呼び出しに対して23行目の関数が呼び出され、出力15のような結果となります。

#コード15
tmp = [0]
def xxx1():
    print('xxx_1番目')
    if tmp[0] == 1:
        print('return_1番目')
        return
    def xxx1():
        print('xxx_2番目')
        if tmp[0] == 1:
            print('return_2番目')
            return
        def xxx1():
            print('xxx_3番目')
            if tmp[0] == 0:
                tmp[0] = 1
                print('tmp = 1')
                xxx1()
            print('xxx_3番目_end')
        xxx1()
    xxx1()
    
def xxx1():
    print('yyy1')
    
if __name__ == '__main__':
    xxx1()
#出力15
yyy1

 いかがでしょうか。この記事は、どんな風に書けばよいのか悩みました。重要な内容でありながら、色々なケースが考えられます。記事を読まれた方に1つでも役にたつ内容があればうれしいです。

私が実際に購入した教材のご紹介

以下、私が実際に購入したPythonの教材をまとめてみました。 Pythonを学習する上で、少しでもお役に立つことができればうれしいです。

Python♪私が購入したPythonの書籍のレビュー
UdemyのPythonの動画講座を書籍を買う感覚で購入してみた

その他

Twitterへのリンクです。SNSもはじめました♪

液晶ペンタブレットを買いました
 (1) モバイルディスプレイを買うつもりだったのに激安ペンタブレット購入

以下、私が光回線を導入した時の記事一覧です。
 (1) 2020年「光回線は値段で選ぶ」では後悔する ←宅内工事の状況も説明しています。
 (2) NURO光の開通までWiFiルーターを格安レンタルできる
 (3) NURO光の屋外工事の状況をご紹介。その日に開通!
 (4) 光回線開通!実測するとNURO光はやっぱり速かった
 (5) ネット上のNURO光紹介特典は個人情報がもれないの?