Pythonのリスト内包表記が大好きである。
これは非常に強力な構文で、慣れてくると何でもかんでもリスト内包表記で書きたくなってしまう。Lispで言うmapみたいなモンだ。
Python2の時だと、printが関数じゃなかったので、出力絡みだと上手く使えないケースも散見したが、今のPython3だと殆ど無敵状態な気がしてる。
プログラミング初心者に対しても、フツーのfor文教えるより先にリスト内包表記を覚えた方がエエんちゃうんか、と思うくらい使いまくれる。そのうちその辺の話も書こうか、とは計画してはいるのだが。80%くらいはリスト内包表記「だけ」で表現出来るのだ。それくらい強力である。
ところが、である。
ひっさびさにこのリスト内包表記でハマってしまった。
次のコードを見て欲しい。
>>> [i for i in [lambda : x for x in [1, 2, 3]]]
[<function <listcomp>.<lambda> at 0x7fe3e3f531f0>, <function <listcomp>.<lambda> at 0x7fe3e3f53280>, <function <listcomp>.<lambda> at 0x7fe3e3f53310>]
>>>
これはリスト[1, 2, 3]にクロージャを適用しようとしたサンプルである。与えられたリスト[1, 2, 3]のそれぞれの要素にはラムダ式が適用され、クロージャに閉じ込められる。結果、返ってくるのは3要素がクロージャのリストなわけだ。
じゃあ、クロージャである各要素を「実行」させたらどうなるだろうか?
想像してみて欲しい。僕はリスト[1, 2, 3]に戻るだろ、って予想してた。
しかし違うのだ。次の結果はある意味驚くべき結果だと思う。
>>> [i() for i in [lambda : x for x in [1, 2, 3]]]
[3, 3, 3]
>>>
なんと[3, 3, 3]が返ってくるのである。ええええええ?とかマジでビックリした。何でやねん、と。
これはバグじゃねぇの?ってぇんでちょっと知らせてみた。そしたら驚くべき回答が来たのである。
リスト内包表記でラムダ式内のxは順次、次に来る値で書き換えられます。結果、最後の値が残るのです。
と。
つまりこうなるわけだな。
>>> [i() for i in [lambda : x for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]]
[10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
>>>
んなバカな(笑)。
いや、本音言うと「んなバカな」なのである。リスト内包表記は純粋関数型言語、Haskellから借りてきた機能である。ところが、Python内部の計算では破壊的変更が行われてる。本当にこれは「仕様」なのか?副次的なモンじゃねーの、って気がしてならない。
じゃあ、Haskellだとどうなのか、っつーと実はHaskellではこの計算が出来ない。
と言うのも、Haskellは由緒正しいラムダ算法に立脚しているプログラミング言語で、実はラムダ式は「必ず引数は最低でも一つ取らないとならない」と言うルールが数学上あって、そこに純粋に従ってる為、上のようなコードはHaskellでは書けないのだ。
しかも、ある意味、上の計算は遅延評価をクロージャを使って無理矢理行おうとしたコードである。反面、Haskell自体は遅延評価を基礎としているプログラミング言語なので、上のようなコードを書く必要性が全くないのだ。
つまり、Pythonの上のコードの結果の「怪しさ」は、そもそもこんなコードを書く事自体がラムダ算法的には間違いで、本家の「結果」と比べようもない、と言うオチまで付いてしまった。
ちなみに、SchemeのSRFI-42と言うライブラリではリスト内包表記が提供されている。ここでは紹介しないが(あまりにも表現がLispらしくない・笑)、そこでの計算はこちらの予想と一致した。まぁ、当たり前だよな。Schemeは「予想と違う実装上の理由によるおかしな事は」基本しない言語なので。
まぁ、他の人が「リストの各要素をクロージャで包み込む」必然性を感じるかどうかは分からないんで、役に立つ情報かどうかは知らんけど、万が一、クロージャを使う必然性が出た時は、リスト内包表記は危険なので使わん方が良い、って事だ。変数が破壊的に変更されてしまうからとんでもないバグを生み出す可能性がある。
上のような事をやりたい場合、一番確実で安全なのは、古き良きmapを使う事である。
>>> [i() for i in map(lambda x: lambda: x, [1, 2, 3])]
[1, 2, 3]
>>>
殆どリスト内包表記に置き換えられて出番の無くなった感のあるmapだが、まさかこんなトコで出番が出てくるたぁ思わんかった。
Pythonのlambda式はヘナチョコで、構文的にも「?」って思うような書き方をせなアカンし、上のlambda x: lambda: xとか、一体何がしたいんだかパッと見分からなくなるし、正直汚い印象しかねぇんだけど、それでも「思った通りの結果を得る」にはここではリスト内包表記に頼るよかmapを使った方が安全なのは間違いない。
と言うわけで、リスト内包表記とクロージャ、混ぜれば危険、と言う話である。