Python とクロージャ
[twitter:@kumagi] さんの
というつぶやきの説明を書こうとしたら 140字じゃあどうしても収まらなかったのでエントリにしてみたり。ちなみになんでこのような実装になっているのか、というのはわかりません。
間違ってたら突っ込んでね。
ネストした関数
まず、関数の中で関数を定義した場合。
>>> def outer(val): ... def inner(arg): ... return val + arg ... return inner ... >>> inner = outer(10) >>> inner(20) 30
outer の中で定義した inner は、定義時点での outer の環境を持っています。
私のクロージャに対する理解が正しければこれはクロージャです。
このように、外側の環境の変数を参照するということは問題なく行えます。
ネストした関数で外側の変数を書き換える
問題はクロージャではなく、ネストした関数から外側の関数のスコープの変数を書き換えることができないという点です。
>>> def outer(val): ... def inner(arg): ... val = 10 # これは inner のローカル変数を定義して 10 を代入している ... return inner ... >>> inner = outer(10) >>> inner(20)
この例では、 inner の中で代入されている val は inner 関数のローカル変数として定義されてしまいます。
では、 val に代入する際に val を参照するとどうなるでしょうか。
>>> def outer(val): ... def inner(arg): ... val = val + arg ... return val ... return inner ... >>> inner = outer(10) >>> inner(20) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "closure.py", line 8, in inner val = val + arg UnboundLocalError: local variable 'val' referenced before assignment
未束縛のローカル変数を参照したと怒られます。
これは、 Python の関数がローカル変数として使われる名前のリストを持っていて、そのリストに val が含まれるために val + arg の部分で参照しようとして未束縛だと言われるわけです。
関数呼び出し時にローカル変数のための環境を一括で確保しているのでしょうか? 何となくそんな気がしますが正確なところはわかりません。
解法
一般的に外側の環境を書き換えるようなことを行いたい場合は、
>>> def outer(val): ... tmp = [val] ... def inner(arg): ... tmp[0] = tmp[0] + arg ... return tmp[0] ... return inner
このような事を行うようです。
先程も説明したとおり、ネストされた環境から外側の環境を書き換えることはできません。参照は可能です。
まず、 Python の仕様として
- tmp[idx] は tmp.__getitem__(idx) の糖衣構文
- tmp[idx] = val は tmp.__setitem__(idx, val) の糖衣構文
として扱われます。
なので、 tmp[0] = tmp[0] + arg は
tmp.__setattr__(0, tmp.__getattr__(0) + arg)
となり、外の環境を書き換えているのではなく、参照された tmp に対する破壊的操作になります。
そのため、外の環境に対しては参照しか行われません。
ちなみに若干キモイけど、こういうのもありです。
>>> def outer(val): ... def inner(arg): ... innder.val = inner.val + arg ... return inner.val ... inner.val = val ... return inner