📏
Pythonのスコープを少しだけ掘り下げる
はじめに
Pythonはifの中で変数を定義してもifの外でも参照できる仕様で、他の言語とは異なった仕様をしています。Pythonユーザは当たり前かもしれませんが、これを掘り下げてみます。
マシンスペック
MacBook Air M2 arm64
準備
プロジェクトの作成
python用のディレクトリとvenvで環境を作成しておきます。
python -V # 例: 3.12.8
mkdir cpy_scope_lab && cd $_
python -m venv venv && . venv/bin/activate
実験
Pythonコードで確認
事象の確認
# demo.py
if True:
x = 100
for i in range(1):
y = 200
while False:
z = 300
print("x =", x) # OK
print("y =", y) # OK
try:
print("z =", z)
except NameError as e:
print("z is undefined:", e)
こちらを実行すると
python demo.py
x = 100
y = 200
z is undefined: name 'z' is not defined
となり、ifブロック内に入る場合のみ定義された変数が保持されています。
バイトコードで確認
python - <<'PY'
import dis, pathlib, textwrap, inspect, sys
src = pathlib.Path("demo.py").read_text()
code = compile(src, "demo.py", "exec")
dis.dis(code)
PY
結果は
0 0 RESUME 0
2 2 NOP
3 4 LOAD_CONST 1 (100)
6 STORE_NAME 0 (x)
4 8 PUSH_NULL
10 LOAD_NAME 1 (range)
12 LOAD_CONST 2 (1)
14 CALL 1
22 GET_ITER
>> 24 FOR_ITER 4 (to 36)
28 STORE_NAME 2 (i)
5 30 LOAD_CONST 3 (200)
32 STORE_NAME 3 (y)
34 JUMP_BACKWARD 6 (to 24)
4 >> 36 END_FOR
6 38 NOP
9 40 PUSH_NULL
42 LOAD_NAME 5 (print)
44 LOAD_CONST 4 ('x =')
46 LOAD_NAME 0 (x)
48 CALL 2
56 POP_TOP
10 58 PUSH_NULL
60 LOAD_NAME 5 (print)
62 LOAD_CONST 5 ('y =')
64 LOAD_NAME 3 (y)
66 CALL 2
74 POP_TOP
11 76 NOP
12 78 PUSH_NULL
80 LOAD_NAME 5 (print)
82 LOAD_CONST 6 ('z =')
84 LOAD_NAME 4 (z)
86 CALL 2
94 POP_TOP
96 RETURN_CONST 8 (None)
>> 98 PUSH_EXC_INFO
13 100 LOAD_NAME 6 (NameError)
102 CHECK_EXC_MATCH
104 POP_JUMP_IF_FALSE 19 (to 144)
106 STORE_NAME 7 (e)
14 108 PUSH_NULL
110 LOAD_NAME 5 (print)
112 LOAD_CONST 7 ('z is undefined:')
114 LOAD_NAME 7 (e)
116 CALL 2
124 POP_TOP
126 POP_EXCEPT
128 LOAD_CONST 8 (None)
130 STORE_NAME 7 (e)
132 DELETE_NAME 7 (e)
134 RETURN_CONST 8 (None)
>> 136 LOAD_CONST 8 (None)
138 STORE_NAME 7 (e)
140 DELETE_NAME 7 (e)
142 RERAISE 1
13 >> 144 RERAISE 0
>> 146 COPY 3
148 POP_EXCEPT
150 RERAISE 1
ExceptionTable:
78 to 94 -> 98 [0]
98 to 106 -> 146 [1] lasti
108 to 124 -> 136 [1] lasti
136 to 144 -> 146 [1] lasti
xとyともにSTORE_NAMEが実行されています。
参考 STORE_*
| 命令 | 保存場所 | 使用ケース |
|---|---|---|
| STORE_FAST |
ローカル変数スロット(PyFrameObject.f_localsplus の連続配列) |
関数内で “ローカル” と解決された名前 |
| STORE_GLOBAL |
グローバル辞書(f_globals) |
global x 宣言された名前/トップレベルでの代入 |
| STORE_NAME |
現在実行中コードオブジェクトの “名前” 辞書 (モジュールなら f_globals、クラス定義中ならクラス dict) |
コンパイラが「ローカルでもグローバルでもない」と判定した場合 |
AST とシンボルテーブルを覗く
python - <<'PY'
import ast, symtable, pathlib, pprint, inspect
src = pathlib.Path("demo.py").read_text()
tree = ast.parse(src, filename="demo.py")
print(ast.dump(tree, indent=2))
st = symtable.symtable(src, "demo.py", "exec")
print("\n=== symtable ===")
for child in st.get_children():
print(child.get_name(), child.get_symbols())
PY
こちらを実行すると
Module(
body=[
If(
test=Constant(value=True),
body=[
Assign(
targets=[
Name(id='x', ctx=Store())],
value=Constant(value=100))],
orelse=[]),
For(
target=Name(id='i', ctx=Store()),
iter=Call(
func=Name(id='range', ctx=Load()),
args=[
Constant(value=1)],
keywords=[]),
body=[
Assign(
targets=[
Name(id='y', ctx=Store())],
value=Constant(value=200))],
orelse=[]),
While(
test=Constant(value=False),
body=[
Assign(
targets=[
Name(id='z', ctx=Store())],
value=Constant(value=300))],
orelse=[]),
Expr(
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Constant(value='x ='),
Name(id='x', ctx=Load())],
keywords=[])),
Expr(
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Constant(value='y ='),
Name(id='y', ctx=Load())],
keywords=[])),
Try(
body=[
Expr(
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Constant(value='z ='),
Name(id='z', ctx=Load())],
keywords=[]))],
handlers=[
ExceptHandler(
type=Name(id='NameError', ctx=Load()),
name='e',
body=[
Expr(
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Constant(value='z is undefined:'),
Name(id='e', ctx=Load())],
keywords=[]))])],
orelse=[],
finalbody=[])],
type_ignores=[])
=== symtable ===
こちらを確認すると、スコープ境界を作るノードが存在しないことがわかります。
ASTにFunctionDef, ClassDef, Lambdaなど新しい名前空間を生成するノードが1つもない
ため、コンパイラはグローバルスコープだけでンボル表を作っています。
CPythonのソースコードから追う
コードを追うと、Python/symtable.c内でsymtable_visit_stmt()という関数があり、こちらでforやifの場合のケースが書かれていました。
歴史的背景
PEP227 PEP572
まとめ
今回はPythonのスコープについて少しだけ掘り下げました。皆様の学習の補助になれば幸いです。
Discussion