LoginSignup
8
8

More than 5 years have passed since last update.

スコープの見え方について調べてみた

Posted at

クロージャの実装を調べていたら、どうしてそのように動くのか分からなかったので、スコープの見え方について調べてみた。
とても基本的なことだと思うけど、スコープの見え方について自分が納得できるような記事をネットで見つけられなかった。実際に動かしてみた結果、確かに、説明し難いなーと思った。

トップレベルから見えるスコープ

次コードで、グローバル変数とローカル変数の状態を調べてみる。

確認(1)

from pprint import pprint
print "-----globals----->>>"
pprint(globals())
print "-----locals----->>>"
pprint(locals())
実行結果
-----globals----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes002.py',
 '__name__': '__main__',
 '__package__': None,
 'pprint': <function pprint at 0x02222770>}
-----locals----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes002.py',
 '__name__': '__main__',
 '__package__': None,
 'pprint': <function pprint at 0x02222770>}

この状態だと、グローバル変数もローカル変数も表示される内容は同じ。
変数を一つ定義するとどうなるか。

確認(2)

from pprint import pprint
a = 123
pprint(globals())
print "-----globals----->>>"
print "-----locals----->>>"
pprint(locals())
実行結果
-----globals----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes002.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x02222770>}
-----locals----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes002.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x02222770>}

トップレベルのスクリプトの実行環境では、locals()とglobals()の出力内容は同じ。
定義した変数はどちらの出力にも存在している。
(※globals()、locals()は、それぞれグローバルスコープ、ローカルスコープの名前を出力しているものと判断して、以下を記述する)

あと、pprintがglobals()の出力にある。(locals()にも)
importとかで取り込まれたモジュールや関数はグローバルスコープに含まれるようになる。(トップレベルで直接参照できるものはグローバルスコープに入る)

関数を定義して、関数の中からスコープを見るとどうなるか。

関数から見たスコープ

## 確認(3)
a = 123
def tes001():
    b = 234
    print "-----globals----->>>"
    pprint(globals())
    print "-----locals----->>>"
    pprint(locals())
tes001()
実行結果
-----globals----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes002.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x02422770>,
 'tes001': <function tes001 at 0x02422BF0>}
-----locals----->>>
{'b': 234}

関数ブロックで定義した変数bは、ローカルスコープに存在するが、グローバルスコープには存在しない。
※クラスから見たスコープについては話しが拡散しそうなので、ここでは言及しない。

今度は、関数ブロックで、locals()とglobals()の内容を表示した後で、変数を定義してみる。

確認(4)

a = 123
def tes001():
    print "-----globals----->>>"
    pprint(globals())
    print "-----locals----->>>"
    pprint(locals())
    b = 234
tes001()
実行結果
-----globals----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes002.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x02222770>,
 'tes001': <function tes001 at 0x02222BF0>}
-----locals----->>>
{}

これだと、変数bはローカルスコープの中にもない。
変数定義前後に、locals()の出力を入れるとどうなるか。

確認(5)

a = 123
def tes001():
    print "-----globals----->>>"
    pprint(globals())
    print "-----locals(1)----->>>"
    pprint(locals())
    b = 234
    print "-----locals(2)----->>>"
    pprint(locals())
tes001()
実行結果
-----globals----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes002.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x02222770>,
 'tes001': <function tes001 at 0x02222BF0>}
-----locals(1)----->>>
{}
-----locals(2)----->>>
{'b': 234}

これは予想の通り。変数が定義される前の出力にはないけど、定義後の出力にはある。

関数をネストしてスコープを調べる

次は、関数の定義をネストして、内側の関数からスコープを調べてみる。

確認(6)

a = 123
def tes001():
    b = 234
    def inner():
        c = 345
        print "-----globals----->>>"
        pprint(globals())
        print "-----locals----->>>"
        pprint(locals())
    inner()
tes001()
実行結果
-----globals----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes002.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x022226F0>,
 'tes001': <function tes001 at 0x02222BF0>}
-----locals----->>>
{'c': 345}

inner()のローカルスコープの中にはbがない。
inner()の中でbを参照するとどうなるか。参照する前後にlocals()の出力を表示するようにする。

確認(7)

a = 123
def tes001():
    b = 234
    def inner():
        c = 345
        print "-----globals----->>>"
        pprint(globals())
        print "-----locals(1)----->>>"
        pprint(locals())
        print b
        print "-----locals(2)----->>>"
        pprint(locals())
    inner()
tes001()
実行結果
-----globals----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes002.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x022226F0>,
 'tes001': <function tes001 at 0x02222BF0>}
-----locals(1)----->>>
{'b': 234, 'c': 345}
234
-----locals(2)----->>>
{'b': 234, 'c': 345}

今度は、変数bを参照する(print b)前のローカルスコープにも、変数bが存在する。これは「確認(5)」と若干異なる動きに見える。参照と代入では動きが違う。
感覚的には、
tex
-----locals(1)----->>>
{'c': 345}
234
-----locals(2)----->>>
{'b': 234, 'c': 345}

のようになるものと思っていた。
上記の結果は、コードを実行する前に、inner()関数のローカルスコープに変数bを含んだスコープが作られているものと考える。
そもそも変数bはinner()関数では未定義であるのに、ローカルスコープに含まれている。
動きから見ると、関数をネストした場合に、内側の関数で未定義で外側で定義されている変数を、内側の関数が参照した場合、外側の関数の定義内容を複写して、内側の関数のローカルスコープに設定される。このスコープの設定はコード実行前に行われるものと思われる。感覚的には、ローカルスコープの初期値のようなものが、関数実行前に作られているような感じ。

内側の関数で、外側で定義した変数を変更した場合のスコープについて調べてみる。

確認(8)

a = 123
def tes001():
    b = 234
    def inner():
        c = 345
        print "-----inner: globals----->>>"
        pprint(globals())
        print "-----inner: locals(1)----->>>"
        pprint(locals())
        print b
        b = 777
        print "-----inner: locals(2)----->>>"
        pprint(locals())
    inner()
    print "-----outer: locals(3)----->>>"
    pprint(locals())
tes001()
実行結果
-----inner: globals----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes002.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x022226F0>,
 'tes001': <function tes001 at 0x02222BF0>}
-----inner: locals(1)----->>>
{'c': 345}
Traceback (most recent call last):
  File "tes002.py", line 19, in <module>
    tes001()
  File "tes002.py", line 16, in tes001
    inner()
  File "tes002.py", line 12, in inner
    print b
UnboundLocalError: local variable 'b' referenced before assignment

上記を実行したら、エラー(UnboundLocalError)になった。
ここで面白いことに、inner()関数で最初に出力しているlocals()の内容には、確かに変数bが入っていない。
参照(print b)だけやった場合は、bが入っていた。違いは内側の関数で外側で定義している変数を設定しているかどうかだけ。
やはり、スコープはコードを実行する前に、用意しておいているものと思われる。この用意されたスコープを元にコードが動く。
ところが、上記のサンプルでは内部関数で設定のコード(b = 777)があるため、実行時に用意されるローカルスコープに変数bは設定されない。
この状態(変数bがスコープに入っていない状態)で、変数bを参照しようとしてエラーになっている。

ここまで確認した結果を整理してみる

ここまでに確認した内容を、若干の推論(憶測)を含めて、整理すると、

  • トップレベルの実行環境では、定義した変数や関数は全てグローバルスコープに設定される。変数の定義は代入により行われる。
  • トップレベルからも関数からも同じグローバルスコープが見えている。(おそらく)
  • 関数で定義した変数、関数(内部関数)はその関数のローカルスコープに設定される。
  • 定義されていない変数を、関数内で参照した場合、コード実行前に、その関数に近いところから外側に向かって、外側の関数で定義されている変数を探して、見つかった変数をローカルスコープに設定する。関数はこのローカルスコープ使って動作する。
  • 但し、関数の中で、変数に代入する場合は、関数の中で変数を定義するものとして、実行時にはローカルスコープには変数は設定されない。この状態で、代入より先に変数を参照しようとするとエラー(UnboundLocalError)になる。

グローバル変数を、関数内で参照・設定した場合のスコープの見え方を調べる

次に、グローバル変数を、関数内で参照、設定した場合の、スコープの見え方について調べてみる。

確認(8)

a = 123
def tes001():
    print a
    print "-----tes001: globals(1)----->>>"
    pprint(globals())
    print "-----tes001: locals(1)----->>>"
    pprint(locals())
    a = 345
    print "-----tes001: globals(2)----->>>"
    pprint(globals())
    print "-----tes001: locals(2)----->>>"
    pprint(locals())
tes001()
print "-----top: globals(3)----->>>"
pprint(globals())
print "-----top: locals(3)----->>>"
pprint(locals())
実行結果
Traceback (most recent call last):
  File "tes003.py", line 15, in <module>
    tes001()
  File "tes003.py", line 5, in tes001
    print a
UnboundLocalError: local variable 'a' referenced before assignment

この場合も、確認(7)と同じ。UnboundLocalErrorになる。グローバルスコープとローカルスコープが混ざる場合も同じ動き。
関数tes001()で、変数aを代入してから参照するのは上手く動くはず。

確認(9)

a = 123
def tes001():
    print "-----tes001: globals(1)----->>>"
    pprint(globals())
    print "-----tes001: locals(1)----->>>"
    pprint(locals())
    a = 345
    print a
    print "-----tes001: globals(2)----->>>"
    pprint(globals())
    print "-----tes001: locals(2)----->>>"
    pprint(locals())
tes001()
print "-----top: globals(3)----->>>"
pprint(globals())
print "-----top: locals(3)----->>>"
pprint(locals())
実行結果
-----tes001: globals(1)----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes003.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x02222770>,
 'tes001': <function tes001 at 0x02222BB0>}
-----tes001: locals(1)----->>>
{}
345
-----tes001: globals(2)----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes003.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x02222770>,
 'tes001': <function tes001 at 0x02222BB0>}
-----tes001: locals(2)----->>>
{'a': 345}
-----top: globals(3)----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes003.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x02222770>,
 'tes001': <function tes001 at 0x02222BB0>}
-----top: locals(3)----->>>
{'__builtins__': <module '__builtin__' (built-in)>,
 '__doc__': None,
 '__file__': 'tes003.py',
 '__name__': '__main__',
 '__package__': None,
 'a': 123,
 'pprint': <function pprint at 0x02222770>,
 'tes001': <function tes001 at 0x02222BB0>}

これは、問題なく動いた。
tes001: globals(2)の出力では、aは123で、tes001: locals(2)の出力では345。
tes001()の中では、ローカル変数のaを書き換えているので、トップレベルに戻ってきた時のaの値は関数呼び出し前の値(123)のまま。
理屈は何となく分かったけど、ちょっと奇妙な気がする。
print文とかを取り除いて、上手く行く場合と、上手く行かない場合のコードを並べてみる。

グローバル変数として定義されている変数にアクセスする関数のコードを整理

上手くいく場合(1)

a = 123
def tes001():
    print a
tes001()

上手くいく場合(2)

a = 123
def tes001():
    a = 345
    print a
tes001()

上手くいかない場合

a = 123
def tes001():
    print a         #UnboundLocalError
    a = 345
tes001()

色々動かして、動作結果に対して理由付けしてきたが、上記を見ると、やっぱり分かり難い。
もう少し調べたいこともあるけど、長くなってきたので一旦ここで終了する。
クロージャの実装についても説明ができそう。

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8