電波塔

21世紀型スノッブを目指すよ!

Python のリスト内包表記とラムダ式でいくつも関数を生成すると

初投稿のプログラミング関係のエントリ.リストの中の要素に応じて動作の変わる関数のリストを作りたいなあということがあって,それでちょっと引っかかったのでメモ.
つまり大雑把に言うと

>>> fs = [lambda a: x + a for x in range(5)]

みたいに書いて,ある引数を与えた際にそれぞにれ0,1,...4を足し合わせるような関数のリストを作りたかった.
でも実際にこれを動かしてみると

>>> [f(0) for f in fs]
[4, 4, 4, 4, 4]

となる.全部range(5)の最後の要素で評価されちゃってて当然これは嬉しくない.
ここでfsの中身自体を見てみるとそれぞれ異なるアドレスの関数を指している一方,xの値はリストを作ったあとでは4になってる.ということ で,Pytohn 2.x系のリスト内包表記中の変数が新しいスコープに作成されないこととPython の lambda式の中の引数以外の変数が実際に使われたタイミングで評価されることとが組み合わさって,どうもこういうことになっているらしい.
解決策として一番簡単なのは[]を()に変える,つまりリスト内包表記の代わりにジェネレータ式を用いること.ジェネレータ式なら新しい関数が作られるタイミングで評価されるからこういう問題は生じない.

>>> fs = (lambda a: x + a for x in xrange(5))
>>> [f(0) for f in fs]
[0, 1, 2, 3, 4]

別の手としてはfunctools.partialを使うことで,partialオブジェクトは生成時に変数が評価されるみたいなので大丈夫(まどろっこしいコードだけどあくまで例示ということでご容赦).

>>> import functools
>>> fs = [functools.partial(lambda x, a: x + a, i) for i in range(5)]
>>> [f(0) for f in fs]
[0, 1, 2, 3, 4]

で,一変数だとぶっちゃけこんな書き方嬉しくないけれど,関数に渡す変数が増えてくるとこういう書き方は俄然旨味が出ると思うんですね.つまり,

for p1 in params1:
    for p2 in params2:
        for p3 in params3:
            func(p1, p2, p3)

より

for f in (lambda : func(p1, p2, p3)
          for p1 in params1
          for p2 in params2
          for p3 in params3):
    f()

の方がクールかなあなんて,……そうでもないかも.元々どうしてこういう問題にぶつかったのかっていうと上のような処理をしようとしたからなのですが.
Python 3.xだとリスト内包表記の変数スコープが変わったようだし大丈夫なんじゃないか,と思ってるけど手元のマシンに入れてなくて確認してない.上記の挙動は Python 2.7.2 でのもの.