Pythonでクロージャはできないの?

答え:できるけどちょっと変(Python 2.7の場合)。

たとえばJavaScriptの場合には、次のように素直にクロージャを作れる。

js> function make_counter(init){
  >     i = init
  >     function counter(){
  >         print(i)
  >         i += 1
  >     }
  >     return counter
  > }
js> c = make_counter(3)

function counter() {
    print(i);
    i += 1;
}

js> c()
3
js> c()
4
js> c()
5

同じことをPythonでやってみる。

>>> def make_counter(init):
...     i = init
...     def counter():
...         print i
...         i += 1
...     return counter
...
>>> c = make_counter(3)

という風に一見できているように見えるけど、いざc()を実行すると、

>>> c()
Traceback (most recent call last):
  File "", line 1, in 
  File "", line 4, in counter
UnboundLocalError: local variable 'i' referenced before assignment

エラーになる。i を代入前に参照している、と言ってるようだ。

ググってみたらこんな記事を見つけた。
cf. Python とクロージャ – プログラマのネタ帳

この記事によると、ネストした内側の関数定義から外側の変数を参照することはできるけど変更することはできない、と書いてある。実際次のような例が載っている。

>>> def outer(val):
...     def inner(arg):
...         return val + arg
... return inner
...
>>> inner = outer(10)
>>> inner(20)
30

この例は確かに問題なく動く。
さて、じゃあ、さっきのエラーになったコードに戻ってみよう。報告されたエラーはコードの4行目、print i の部分で、i が代入前に参照された、というエラーだった。「内側の関数定義から外側の変数が参照できるが変更はできない」と言うのが本当なら、このエラーはおかしい。4行目では参照しかしてないはずだ。5行目の i += 1 がエラーになるのなら理解できるんだけど。

で、結局どうやったらクロージャができるかと言うと、利用したい変数をリストに入れておけばいいらしい。

>>> def make_counter(init):
...     tmp = [init]
...     def counter():
...         print tmp[0]
...         tmp[0] += 1
...     return counter
...
>>> c = make_counter(3)
>>> c()
3
>>> c()
4
>>> c()
5

これだと tmp 変数には参照しかしないのでエラーにならない、と言うことらしい。確かにちゃんと動いているけど、なんか釈然としない。