イントロダクション
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.