このBlogは移転しました。今後は aish.dev を御覧ください。

リスト内包のひみつ

こちらのTweetが Python.jp slack でちょっと話題になっていた。

次のようなコードだ

>>> a = [lambda: print(i) for i in range(3)]
>>> for i in a: i()
2
2
2

結論としては cocoatomo さんの書かれているように、変数の評価タイミングの問題で、

対処としては、次のように、出力する値を、関数の実行時ではなく、関数の作成時に決定する必要がある。

>>> 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の関数のなかに別の関数を作成した時、親となる関数のローカル変数を子となる関数が参照する仕組みのことだ。

クロージャの仕組みは、以前

atsuoishimoto.hatenablog.com

に書いた。

したがって、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

のように、リスト内包式があると他のローカル変数を上書きしてしまい、わかりにくいという問題を解決するためだ。

この経緯は

python-history-jp.blogspot.jp

でも解説されているので、参照されたい