Pyhtonでクロージャを使おうとして、Pythonの変数スコープにハマった話
クロージャは便利です。
以下のようなコードを書いて、期待と動作が違ったので数時間を溶かしてしまいました。詳しい人から見れば常識なのでしょうが…
import threading import itertools import time A = [1,2,3] B = ['a', 'b', 'c'] def main(): for prod in itertools.product(A,B): # `prod` を束縛したクロージャを作る def f(): for i in range(10): # このクロージャは、prodの値を10回表示して終了する print(prod) time.sleep(0.5) if prod == (1,'a'): # `prod`の値が (1,'a')のときだけクロージャ発動 t = threading.Thread(target=f) t.start() time.sleep(1) main()
さて、クロージャf
は変数prod
を束縛して、それをただ表示させているだけです。prod
の値が(1,'a')
のときだけクロージャを作成して起動しているのですから、出力は以下のようになると期待しました。
# 期待される出力 (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a')
ところが実際の出力はこうなります
# 実際の出力 (1, 'a') (1, 'a') (1, 'b') (1, 'b') (1, 'c') (1, 'c') (2, 'a') (2, 'a') (2, 'b') (2, 'b')
こうなる理由は以下のとおりです。スコープ外の変数を参照するクロージャを作った場合、そのクロージャのスコープに束縛されるのではなく、スコープ内で代入されない限りは上位レベルのスコープが参照されます。なので上位スコープ(参照サンプルコードの場合はグローバルスコープ)のprod
の値が参照されるので、forループによって変数の値が変更されると、それを反映してしまいます。一般的に、このルールはグローバル変数への適用において遭遇することが多いですが、関数スコープ内でクロージャを作成するときにも適用されるんですね。
コードを以下のように直せば、期待通りに動きます
import threading import itertools import time A = [1,2,3] B = ['a', 'b', 'c'] def main(): for prod in itertools.product(A,B): # `prod` を束縛したクロージャを作る def f(): prod2 = prod # <---- 新たな変数prod2へ代入する for i in range(10): # このクロージャは、prod2の値を10回表示して終了する print(prod2) time.sleep(0.5) if prod == (1,'a'): # `prod`の値が (1,'a')のときだけクロージャ発動 t = threading.Thread(target=f) t.start() time.sleep(1) main()
まとめ
Hy使おう(錯乱)