プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2022/01/23

[Python] for文の中のラムダ式でループ変数を参照する場合は評価のタイミングに注意

update2022/01/31 event_note2022/01/22 17:23

Python に限らないと思いますが、知らないとハマることになるやつですね。

例えば、以下のように関数を登録して後から実行した場合、

def hoge(x):
    print(x)

funcs = []
for i in range(5):
    funcs.append(lambda: hoge(i))

for func in funcs:
    func()

期待する動作は 0~4 が順に出力されることですが、上記だと全て 4 が出力されます。

ここのラムダ式は遅延評価、関数が呼ばれたときに初めて評価されます。
従って、関数が実行されたときは既にループが終わった後であり、その時には i4 になっているため、常に 4 と出力されてしまいます。

解決方法として、私が知っている範囲では以下の2パターンがあります。

デフォルト引数で対応

以下のようにラムダ式にデフォルト引数を与えてやります。

def hoge(x):
    print(x)

funcs = []
for i in range(5):
    funcs.append(lambda x=i: hoge(x))

for func in funcs:
    func()

x=i とすることで即時評価され、期待した出力となります。

しかし、以下のように実行時に引数を与えると、与えられた引数で上書きされてしまいます。

def hoge(x):
    print(x)

funcs = []
for i in range(5):
    funcs.append(lambda x=i: hoge(x))

for func in funcs:
    func(4)

次の方法だとこの問題も回避できます。

functools.partial を使う

functools.partial() は 引数を部分的に適用(固定化?)した関数を生成する関数です。
以下のように書くことで期待した出力となります。

from functools import partial

def hoge(x):
    print(x)

funcs = []
for i in range(5):
    funcs.append(partial(hoge, i))

for func in funcs:
    func()

この場合、実行時に引数を与えるとエラーになります。