リスト内包のひみつ
こちらのTweetが Python.jp slack でちょっと話題になっていた。
どういうこと? pic.twitter.com/BxyyWbyvQo
— ahuglajbclajep (@ahuglajbclajep) 2018年1月24日
次のようなコードだ
>>> a = [lambda: print(i) for i in range(3)] >>> for i in a: i() 2 2 2
結論としては cocoatomo さんの書かれているように、変数の評価タイミングの問題で、
初めまして.
— tomo🐧 (@cocoatomo) 2018年1月24日
そこは Python のループでよくハマるポイントで, i の値の評価が後で行われるのが混乱の原因です. ループの本体の中で一度 i を別の変数に入れるなどして, 評価を走らせると回避できます.
FAQ → https://t.co/5iCqdIIhUZ
対処としては、次のように、出力する値を、関数の実行時ではなく、関数の作成時に決定する必要がある。
>>> a = [lambda x=i: print(x) for i in range(3)] >>> for i in a: i() 0 1 2
もともとの問題はこれで解決するのだが、このコード、単純だがPython2とPython3では動作が異なっている。
Pythonの動作を理解するのに良い教材だと思うので、ちょっと解説してみよう。
Python2の場合
Python2では、先程のコードは、次のようなループとして実行される。
>>> from __future__ import print_function >>> a=[] >>> for i in range(3): ... f = lambda : print(i) ... a.append(f) ... >>> for i in a: i() 2 2 2
リスト内包式で使われているループ変数 i
は、グローバル変数 i
として、リスト内包式を終了した後も参照できる。
i
の値は、必ず 2
となる。
>>> a = [lambda: print(i) for i in range(3)] >>> i 2
この場合、lambda
関数の print(i)
という式は、グローバル変数の i
を参照して、出力している。
したがって、 lambda
関数を実行する前に i
の値を変更すると、出力も変化する。
>>> a = [lambda: print(i) for i in range(3)] >>> i = 10000000 >>> for u in a:u() ... 10000000 10000000 10000000
Python3の場合
Python3では、リスト内包は次のように展開され、関数呼び出しとして実行される。
>>> _it = range(3) >>> def _listcomp(_it): ... ret = [] ... for i in it: ... ret.append(lambda : print(i)) ... return ret ... >>> a = _listcomp(_it) >>> for i in a: i() 2 2 2
Python2と違って、Python3ではループ変数 i
は外部からは参照できない。リスト内包は関数内で実行され、 i
はグローバル変数ではなくローカル変数となるためだ。
>>> a = [lambda: print(i) for i in range(3)] >>> i Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'i' is not defined
Python2 では lambda
関数の print(i)
は、グローバル変数 i
を参照するが、Python3 では、lambda
式はローカル変数 i
を参照するクロージャとなる。
クロージャとは、Pythonの関数のなかに別の関数を作成した時、親となる関数のローカル変数を子となる関数が参照する仕組みのことだ。
クロージャの仕組みは、以前
に書いた。
したがって、Python2のように、後から i
の値を変更することはできない。
>>> a = [lambda: print(i) for i in range(3)] >>> i = 10000000 >>> for u in a:u() ... 2 2 2
Python2/3の違い
Python2 と Python3 では、リスト内包の実現方法に上記のような違いがある。これは、Python2 では
>>> i = 10000 >>> a = [lambda: print(i) for i in range(3)] >>> print(i) 3
のように、リスト内包式があると他のローカル変数を上書きしてしまい、わかりにくいという問題を解決するためだ。
この経緯は
でも解説されているので、参照されたい