Pylintの「W0640 cell-var-from-loop」
以下のようなコードをPylintに通すと、表題のようなwarningが出る。
funcs = [ lambda x: x + elem for elem in range(3) ] results = [ func(1) for func in funcs ]
funcsは「与えられた引数に0〜2を加える関数」のリストであり、resultsではそれらの関数に1を与えた結果を返すので、想定されるresultsの中身は [1, 2, 3]
である。しかし、実際の中身は [3, 3, 3]
である。何が起こっているのか分かりづらいが、これこそが「cell-var-from-loop」というwarningが警告していることである。
わかってしまえば簡単だが、意外とつまづいた所だった。
起こっていること
細かい事は私も詳しくないが、Pythonの関数では基本的に遅延評価が利用されている。簡単に言うと「関数は必要になるときまで評価されない」といったもので、不必要な処理を行わないことで計算量を軽くできる、というメリットがある。
今回の例でもこの遅延評価が影響してくる。 funcsの中にある関数は、resultsが実行されて初めて利用される。よって、Pythonは以下のような処理をする。
funcs
の中に「引数にelem
を加えた値を返す関数」が入れられるresults
にて、funcs
の中にある関数が使われる- 使われた時点で
funcs
の各要素であるfunc
が、現在のelem
の値を見に行く elem
の値はループの最後に使われた2であるため、すべてのfunc
が1+2=3を実行して、3を返す
平たく言うと、「ループ変数で関数作ったら思わぬ結果が返ってくるから気をつけろよ」という警告をしているのが、このwarningである。
どうすればいいのか
ループ変数を使って関数を作らない
おそらく、どうしてもループ中に関数を作らなければいけない事はあまりないと思われるし、そういう場合でも書き方を工夫すれば避けることができるだろう。 例えば上記例であれば、「elemに引数を加える関数」をわざわざ作らずに、そもそもelemに1を加えればよいだけである。
問題ないことを確認して無視する
これはあくまで警告でエラーではないので、場合によっては最悪無視しても良い。 ただし、今回の例のような場合もあるため、安直に無視するのは良くない。
無視して良い基準は、「作った関数がループの外で使われていないかどうか」である。 次のループに入るまでにその関数による処理を完了させれば、遅延評価によるループ変数の上書きは行われないため、警告を無視することができる。
results = [] for elem in range(3): func = lambda x: x + elem results.append(func(1))