Python の変数スコープを code object と inspect で見てみる
Overview
(おすすめできない方法だが) eval()
でラムダ式を生成しようとして、スコープについてやや複雑であることがわかったので知見を共有する。スコープについては色々なところで紹介されているため、あまり新しい内容はないかもしれない。
注意: このあたりの仕様にあまり詳しくないため、誤りの可能性にはご注意いただきたい。
なお、スコープや変数束縛について、基本的な知識があることを仮定する。
動作環境
Python 3.9.6 で動作を確認した。
参考文献
意外なところに書かれていたりする。
- Python チュートリアル > 9. クラス > 9.2 Python のスコープと名前空間 https://docs.python.org/ja/3/tutorial/classes.html#python-scopes-and-namespaces
- Python 言語リファレンス
- 3.データモデル > 3.2. 標準型の階層 (の「ユーザ定義関数」のところ) https://docs.python.org/ja/3/reference/datamodel.html#the-standard-type-hierarchy
- 4. 実行モデル > 4.2. 名前付けと束縛 https://docs.python.org/ja/3/reference/executionmodel.html#naming-and-binding
- Python 標準ライブラリ > Python ランタイムサービス > inspect --- 活動中のオブジェクトの情報を取得する https://docs.python.org/ja/3/library/inspect.html
Summary
- 関数定義時に解決されていない変数は、関数実行時のスコープから上にたどってグローバルスコープまでを見に行くことにより解決しようとする。
- 解決が成功すれば正常実行できる。
- 解決が失敗すれば
NameError
が発生する。
- スコープ情報は以下の方法で確認できる
inspect.getclosurevars()
<関数>.__globals__
<関数>.__closure__
-
<関数>.__code__.co_varnames
などのコードオブジェクト内の情報
Motivating example
まず、私にとっては驚きだったが、次のコードは動く。
エディタはソース上にエラーを表示するかもしれないが、動く。
from pprint import pprint
v = 1
def define_g():
z = 5
def g(x, y):
o = v + w + x + y + z
return o
return g
f = define_g()
f
に代入されるのは define_g()
の中で定義されている関数 g
である。
g
において、
-
x
とy
はこの関数の引数である。(=g
の local scope にある) -
o
はこの関数内で定義されている。(= これもg
の local scope にある) -
v
はこのファイル (= モジュール) のトップレベルで定義されている。(= global scope にある) -
z
はこの関数の外側の関数内で定義されている。(g
の enclosing scope にある)
いっぽう、w
に対する参照が解決していない。でも関数 g
を定義することはできる。
ただし、もちろん、f
を実行すること (呼び出して関数内をすべて実行すること) はできない。
# 略: scope1.py の内容
f(1, 2)
を付け足すと出力はこうなる:
Traceback (most recent call last):
File ".../scope2.py", line 16, in <module>
f(1, 2)
File ".../scope2.py", line 10, in g
o = v + w + x + y + z
NameError: name 'w' is not defined
正しく動かすには f
(つまり g
) の呼び出し前に w
を (f
の呼び出しを含むどこかの) スコープで定義してやればいい:
# 略: scope1.py の内容
w = 3
f(1, 2)
つまり、スコープは変数名をキーとする辞書だと思っておけば大体よくて、
- 実行時にどのスコープに変数を探しに行けばよいかは関数の定義時に決定している。
- 実行時にはそれらのスコープのその時点での値を使って変数を解決する。
ことがわかる。なお、def
の代わりに lambda
を使っても同様の振る舞いをする。
ではこれらのスコープ情報をプログラム的に取得できるか、というのが興味の的である。
inspect モジュールを使う
おあつらえ向きに、inspect
モジュールには getclosurevars()
関数がある。
from inspect import getclosurevars
v = 1
def define_g():
z = 5
def g(x, y):
o = v + w + x + y + z
return o
return g
f = define_g()
print(getclosurevars(f))
実行結果はこう:
ClosureVars(nonlocals={'z': 5}, globals={'v': 1}, builtins={}, unbound={'w'})
z
を enclosing scope から探しに行くこと、w
が解決されていないこと、などがわかる。
g
が組み込み関数などを呼んでいる場合は、それらが builtins
の中に含まれる。
一方で、関数名が示すように、ローカル変数 o, x, y
はこれらに含まれない。私が知る限り、これらを取得する inspect
のメソッドはないので、次節のようにコードオブジェクトを取得するのがよいと思う。
コードオブジェクトから変数スコープを取得する
ユーザが定義した関数 f
はコードオブジェクト f.__code__
を持っており、変数のスコープに関する情報を持っている。
v = 1
def define_g():
z = 5
def g(x, y):
o = v + w + x + y + z
return o
return g
f = define_g()
print(f.__code__)
print(f.__code__.co_varnames)
print(f.__code__.co_freevars)
print(f.__code__.co_names)
実行結果はこちら:
<code object g at 0x1085dd5b0, file ".../scope5.py", line 7>
('x', 'y', 'o')
('z',)
('v', 'w')
属性名と取得できる内容があまり一致していない気がするが、私の解釈はこうである:
-
co_varnames
:f
(g
) の local scope でアクセスできる変数 -
co_freevars
: enclosing scope で定義されている変数 -
co_names
: 外の scope で解決する変数 (global scope で定義されているか、すべての scope で未定義である変数)
f.__closure__
について
enclosing scope で定義されている変数については、その値をセルオブジェクト経由で取得できる。
# 略: spec5.py と同じ
f = define_g()
print(f.__closure__)
print(f.__closure__[0].cell_contents)
実行結果はこう:
(<cell at 0x10ee3dfd0: int object at 0x10ecef9b0>,)
5
たしかに z
の値が入っている。f.__closure__
でセルオブジェクトの tuple が取得できて、それらオブジェクトは cell_contents
属性を持ち、そこに変数の値が入っている。
試していないが、これは書き換えが可能で、それによりクロージャ変数の値を変更できるらしい。
f.__globals__
について
コードオブジェクトのその他の属性については inspect
モジュールに説明があるので見てほしい。
たとえば、関数が実行されるときの「グローバルスコープ」が格納されている変数がある:
from pprint import pprint
# 略
f = define_g()
pprint(f.__globals__)
結果はこう
{'__annotations__': {},
'__builtins__': <module 'builtins' (built-in)>,
'__cached__': None,
'__doc__': None,
'__file__': '/Users/shinsa/git/zenn-docs/articles/scope7.py',
'__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x10823fca0>,
'__name__': '__main__',
'__package__': None,
'__spec__': None,
'define_g': <function define_g at 0x108289f70>,
'f': <function define_g.<locals>.g at 0x108325c10>,
'pprint': <function pprint at 0x108325af0>,
'v': 1}
これはこのファイルに相当するモジュールの global scope そのものであり、それが g
が定義されたときの global scope でもある。
実は、関数が実行されるときに変数を探しに行く「グローバルスコープ」とは、この f.__globals__
である。これは普通、関数呼び出しが入っているモジュールの、ではなくて、関数定義が入っているモジュールの global scope である。
メタプログラミングを使わなければ、この内容を変更することはできないが、eval()
で関数を作るときに第2引数として「グローバルスコープ」を与えることができる。これにより関数実行時の scope を tweak することができる。
もしくは関数の実行を
exec()
で行う。繰り返しになるが、知らないコードを実行しちゃったりすると危険なので、あまり推奨されない方法であることに注意。
本来の動機であった、eval()
で関数を作るときのポイントについては別記事で。
Discussion