pythonのクロージャに変数を束縛する方法

April 17, 2013 - python

ハマったので。

a = 2
double = lambda x: x*a
double(4)  # 8 (=4*2)
double(10) # 20 (=10*3)
a = 3
double(4)  # 12 # WTF?
double(10) # 30

doubleというクロージャ内の変数aを、クロージャ宣言時のaで束縛したいのです。

対応は2つ。

  1. lambdaのデフォルト引数で束縛する
  2. functools.partialで束縛する

1. lambdaのデフォルト引数で束縛する

参考:closures - Python lambda’s binding to local values - Stack Overflow

a = 2
double = lambda x, y=a: x*y
double(4)  # 8 (=4*2)
double(10) # 20 (=10*3)
a = 3
double(4)  # 12 (=4*2)
double(10) # 30 (=10*2)

2. functools.partialで束縛する

やってることは1と同じなのですが、一応動くよねということで。

from functools import partial
a = 2
double = partial(lambda x, y=None: x*y, y=a)
double(4) # 8 (=8*2)
double(10) # 30 (=10*2)
a = 3
double(4)  # 12 (=4*2)
double(10) # 30 (=10*2)

そもそも変数上書きしなければ良いんじゃない?

普段は変数の上書きは基本的にやりません。なので変数の束縛とかあまり意識してませんでした。

今回、プロジェクトオイラーを解くにあたって、素数ジェネレータをつくろうとした結果、ハマったのでした。

from itertools import ifilter, count
def gen_primes():
    it = count(2)  # [2, 3, 4, ...]
    while True:
        v = it.next()
        yield v
        it = ifilter(lambda x, y=v: x % y > 0, it)
        # 当初は以下のようにしていた
        # これだと次のループ時のifilter内でvの値が変わってしまう
        # it = ifilter(lambda x: x % v > 0, it)
for v in gen_primes():
    print v
    if v > 100:
        break

余談1: functools.partialの使いどころ

ちょくちょくfunctools.partial使ってましたが、そんなの使わなくてもlambdaで事足りますね。今更気づきました…

mul = lambda a, b: a * b
mul(3,2)  # 6
# lambda
double = lambda a, b=2: mul(a,b)
double(3)  # 6
# functools.partial
import functools
double = functools.partial(mul, 2)
double(3)  # 6

こうなると、functools.partialの使いどころが難しいですね。 戻り値がpartialオブジェクトなので、あとで引数とかが参照できることくらいでしょうか…

>>> f = functools.partial(lambda a, b=0:a+b, b=0)
>>> f
<functools.partial object at 0x1092b2ec0>
>>> f.args
()
>>> f.keywords
{'b': 0}
>>> f.func
<function <lambda> at 0x1092eab18>
>>> f.args = (1,)  # 引数を後から上書きはできない
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: readonly attribute