8.1 Python のスコープルールの誤解
Pythonのスコープは、LEGBというルールに基づいていて、この頭字語は次の意味があります:
-
Local(関数(defまたはlambda)内で何らかの方法で割り当てられた名前で、 この関数内でグローバルとして宣言されていないもの); Enclosing(内部から外部までの 静的に含む関数(defまたはlambda)のローカルスコープの名前);Global(モジュールファイルのトップレベルで割り当てられた名前、 またはdef内でglobalステートメントを実行することによって);-
Built-in(組み込みモジュールであらかじめ割り当てられた名前:open,range,SyntaxError, など)。
簡単そうに見えるよね?
でも、Pythonではこれがどのように動作するかに関していくつかの細かい点があって、 それがPythonプログラミングの難しい問題につながることもあるんだ。次の例を見てみよう:
x = 10
def foo():
x += 1
print(x)
foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment
何が問題?
上記のエラーは、スコープ内で変数に値を代入すると、Pythonは それをそのスコープのローカルと自動的にみなすため、あらゆる上位スコープ内で 同じ名前の変数を隠してしまうからなんだ。
このため、多くの人々が関数の内部に代入ステートメントを追加することで、以前は動いていたコードがUnboundLocalErrorを 発生させると驚くことになるんだ。
この特徴は特にリストを使用するときに開発者を混乱させることがある。次の例を見てみて:
lst = [1, 2, 3]
def foo1():
lst.append(5) # これは普通に動くよ...
foo1()
print(lst)
[1, 2, 3, 5]
lst = [1, 2, 3]
def foo2():
lst += [5] # ... でもこれはダメだ!
foo2()
Traceback (most recent call last):
File "
", line 1, in
File "
", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment
なんでfoo2は失敗して、foo1はうまく動くんだろう?
前の例と同じ理由なんだけど、ここではさらに微妙なケースだと思う。foo1はlstに代入演算子を 使用していないけど、foo2は使っているんだ。覚えておくべきなのは、lst += [5]は本当はlst = lst + [5]の 単なる短縮形で、実際にはlstに値を代入しようとしているんだけど、そのlst自体がまだ定義されていない ということなんだ。それでエラーが発生するんだよ。
8.2 リストを反復しながら変更すること
次のコードの問題は結構明らかだよね:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
for i in range(len(numbers)):
if odd(numbers[i]):
del numbers[i] # 悪い: 反復中にリストからアイテムを削除する
Traceback (most recent call last):
File "
", line 2, in
IndexError: list index out of range
リストや配列を反復しながら要素を削除することは、経験豊富なソフトウェア開発者にはよく知られている Pythonの問題なんだ。でも、上記の例が結構明らかであっても、経験豊富な開発者ですら より複雑なコードでこの問題に直面することがあるんだよ。
幸いなことに、Pythonにはいろんなエレガントなプログラミングパラダイムがあって、 それをうまく使うことでコードの大幅な簡略化と最適化につながることがあるんだ。これによって、 より簡単なコードでリストの要素を誤って削除する間違いを避ける可能性が大幅に低くなるんだよ。
その一つがリスト内包表記だね。それを理解することで、この特定の問題を避けるために特に役立つんだ。 上記のコードの代替実装を見てみると、うまく動作するものがわかるよ:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
numbers[:] = [n for n in numbers if not odd(n)] # 新しい要素を選別するだけ
print(numbers)
# [0, 2, 4, 6, 8]
重要! ここでは新しいリストオブジェクトの代入は行われていないよ。numbers[:]を使うことで全ての要素に新しい値を 一斉に代入しているんだ。
8.3 Pythonがクロージャで変数をどのようにバインドするかについての誤解
次の例を考えてみよう:
def create_multipliers():
return [lambda x: i * x for i in range(5)] # 関数のリストを返すよ!
for multiplier in create_multipliers():
print(multiplier(2))
次のような出力を期待するかもしれないけど:
0
2
4
6
8
実際にはこうなるんだよ:
8
8
8
8
8
びっくりだよね!
これはPythonの遅延バインディングが原因で、クロージャで使用される変数の値が 内部関数が呼び出される時点で検索されるんだ。
したがって、上記のコードでは、どの返された関数が呼び出されても、iの値は 呼び出し時に囲むスコープ内で検索されるんだ(その時点でループはすでに終了しているので、iは 最終結果、つまり4にすでに割り当てられている)ということなんだ。
このPythonのよくある問題への解決策は次の通りだよ:
def create_multipliers():
return [lambda x, i = i : i * x for i in range(5)]
for multiplier in create_multipliers():
print(multiplier(2))
# 0 # 2 # 4 # 6 # 8
やったね!ここで匿名関数を生成するためにデフォルト引数を使って期待する動作を 実現しているよ。これをエレガントだと思う人もいるし、精妙だと思う人もいるし、 このようなことを嫌う人もいる。でも、Python開発者ならいずれにしても 理解しておくことが重要なんだ。
GO TO FULL VERSION