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

Python 3.x では…

Python 3.x では nonlocal 文が追加されていて、

>>> def outer(val):
...     def inner(arg):
...         nonlocal val
...         val = val + arg
...         return val
...     return inner

とすると、問題なく書き換えられます。

ちょっと追記

という mention をもらってなんのことだろうと思っていたら、 Twitter で [twitter:@yuroyoro] さんが というようなつぶやきをしていたので、 Java でも同じようなものらしいです。