イントロダクション
Python の変数のスコープ、参照のメカニズムは意外に直感的でない部分があり、初心者が罠にはまる可能性がある。しかし、一旦ルールを覚えればさほど複雑ではない。ここではその理解を助けるための問題を紹介する。
問題ごとに何が出力されるか、もしくはエラーが出力されるかどうかを答えよう。実行環境は Python 3 とする。難しい(というかマニアックな)問題は見出しが赤色になっている。
問題1
x = 1
if True:
x = 2
print(x)
解答
2
Python では if 文はスコープを形成しない。そのため if 文内の x は外の x と同一の変数となる。
問題2
for i in range(10):
x = i * 2
print(i, x)
解答
9 18
if と同様に for 文もスコープを形成しないので、ループ変数や内部で定義した変数はその for 文の後でもアクセスすることができる。
問題3
ls = [x * 2 for x in range(10)]
print(x)
解答
(print(x) のとこで)
NameError: name 'x' is not defined
リスト内包表記(list comprehension)が実行されるとき、新たにスコープが作られそこでループ変数が定義される。そのため、外側のスコープに存在する print(x) からは x にアクセスできない。
問題4
funs = []
for i in range(5):
def f():
print(i)
funs.append(f)
for fun in funs:
fun()
解答
4
4
4
4
4
関数 f の中から外側の変数 i を参照しているが、print(i) が実行される時点での i の値が使われることになる。5つの f が実行されるのはいずれも for 文の後であり、その時点では i は 4 であるため、すべて 4 を出力することになる。
なお、リスト内包表記についても同じことが起きる。
問題5
x = 0
def f():
x = 1
f()
print(x)
解答
0
関数 f が形成するブロック1には x = 1 という代入文が存在するため、このブロックのスコープには x という新しい変数が作られることになる。つまり、トップレベルの x は f によって変更されない。
問題6
x = 0
def f():
print(x)
x = 1
f()
解答
(print(x) のとこで)
UnboundLocalError: local variable 'x' referenced before assignment
1つ前の問題と同様に、関数 f が形成するブロックに x = 1 という代入文が存在するため、ブロックが実行される前にそのスコープに x という変数が作られる。しかし print(x) が実行される時点では x は値を持たない (unbound) であるため、例外が発生する。
問題7
x = 0
def f():
x += 1
f()
print(x)
解答
(print(x) のとこで)
UnboundLocalError: local variable 'x' referenced before assignment
+= も代入文とみなされるため、1つ前の問題と同様に f が形成するブロックに変数 x が作られる。しかし、x += 1 が実行されるとき x の値は存在しないため、例外が発生する。
外側のスコープの変数の値を変更するには global や nonlocal を使う必要がある。
問題8
x = 0
def f():
if False:
x = 1
print(x)
f()
解答
(print(x) のとこで)
UnboundLocalError: local variable 'x' referenced before assignment
if 文はブロックを形成しないため、これも1つ前の問題と同様に f が形成するブロックに x への代入文が存在し、変数 x が作られる。しかし実際には x に値は代入されないため例外が発生する。
問題9
x = 0
def f():
del x
f()
解答
(del x のとこで)
UnboundLocalError: local variable 'x' referenced before assignment
実は del 文も代入文と同様に、そのブロックに変数を生成する効果がある。そのため、f が形成するブロックに変数 x が作られるが、x の値が存在しないまま del x が実行されるため例外が発生する。
このように変数を作る効果のある構文は代入文、del 文、for 文、クラス定義、関数定義、import 文、with と except の as がある。
問題10
x = 0
def f():
def g():
print(x)
x = 1
g()
f()
解答
1
g が実行される時点では、一つ外側のスコープ (f) に x が存在し値が 1 であるため 1 が出力される。
問題11
x = 0
def f():
x = 1
del x
def g():
print(x)
g()
f()
解答
(print(x) のとこで)
NameError: free variable 'x' referenced before assignment in enclosing scope
del x が実行されても f のスコープに x は存在しつづける(値がなくなるだけ)。そのため、g 内の x は f で定義された x を参照することになる。しかしその x は値を持たないので例外が発生する。
同じ理由で、以下の2つのコードでも同じ結果となる。
x = 0
def f():
x = 1
def g():
print(x)
del x
g()
f()
x = 0
def f():
def g():
print(x)
x = 1
del x
g()
f()
問題12
x = 1
def f():
x = 2
try:
raise Exception()
except Exception as x:
pass
def g():
print(x)
g()
f()
解答
(print(x) のとこで)
NameError: free variable 'x' referenced before assignment in enclosing scope
try 文で例外が発生し except 節が実行されると、その as で指定した変数は del と同様に値が削除される。そのため、g が実行される時点では f 内の x は存在するが値がないため例外が発生する。
なお、except 節が実行されなければ、該当する変数は削除されない。
問題13
x = 1
class C:
x = 2
def f():
print(x)
f()
解答
1
クラス定義内のコードはスコープに関して特殊であり、そこで定義された変数(クラス変数)はそのスコープからしかアクセスできず、クラス定義内の関数(つまりメソッド)からは直接アクセスできない。
問題14
x = 0
def f():
x = 1
def g():
print(eval("x"))
g()
f()
解答
0
これは eval 関数の(直感に反する)振る舞いに関する問題である。eval 関数で実行したコードからアクセスできる変数は、実は**「グローバル変数」と「eval が実行されたブロックで定義された変数**」のみであり、その間で定義された変数にはアクセスできない。今のコードでは、eval 関数に渡されたコードからアクセスできる(ビルトイン以外の)変数は、1行目の x と g 内で定義された変数(1つもない)である。よってトップレベルの x が参照され、0 が出力される。
なお、exec 関数も同様の振る舞いをする。
参考
- 4. Execution model — Python 3.8.0 documentation
- satwikkansal/wtfpython: A collection of surprising Python snippets and lesser-known features.