root3のメモ帳

データサイエンスな知識を残していくブログ。

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は以下のような処理をする。

  1. funcsの中に「引数に elem を加えた値を返す関数」が入れられる
  2. resultsにて、funcsの中にある関数が使われる
  3. 使われた時点でfuncsの各要素であるfuncが、現在の elem の値を見に行く
  4. 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))