📏

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のソースコードから追う

https://github.com/python/cpython
コードを追うと、Python/symtable.c内でsymtable_visit_stmt()という関数があり、こちらでforやifの場合のケースが書かれていました。

歴史的背景

PEP227
https://peps.python.org/pep-0227/
PEP572
https://peps.python.org/pep-0572/

まとめ

今回はPythonのスコープについて少しだけ掘り下げました。皆様の学習の補助になれば幸いです。

Discussion