_log

備忘録

Pythonのクロージャについてのメモ

他の言語でクロージャ書いていて、Pythonはどうだったっけと思ったのでメモ。

Effective Pythonの項目15に詳しかった。

クロージャの説明については割愛。

引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。 (https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%83%BC%E3%82%B8%E3%83%A3)

Pythonのスコープ

まずPythonのスコープについて。

Pythonだと基本として内側の関数から外側の変数を参照できるが、更新はできない。

# 1.これは動くが
... def outer():
...     x = 1
...     def inner():
...         print(x)
...         return x
...     return inner

>>> o = outer()
>>> o()
1    
1    

# 2.これはエラー  
>>> def outer():
...     x = 1
...     def inner():
...         print(x)
...         x = 2
...         return x
...     return inner
...
>>> o = outer()
>>> o()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in inner
  UnboundLocalError: local variable 'x' referenced before assignment

# 3.これもエラー   
>>> def counter():
...     x = 1
...     def inner():
...         x += 1
...         return x
...     return inner

>>> c = counter()
>>> c()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in inner
  UnboundLocalError: local variable 'x' referenced before assignment

1はinnerから見て外側の変数であるouterのxを参照できている。

2と3はinnerがxをローカル変数として宣言している(代入によって別の変数として宣言している)。

そして、それぞれ初期化前に評価しようとしてUnboundLocalErrorが発生している。

3の累算代入文については若干腑に落ちなかったが、

  • xへ代入されているのでaddのローカル変数として宣言されている
  • 加算前に評価を行うが、その際には初期化されていないので、UnboundLocalErrorとなる

...ということだと思われる(理解が間違っていたらご教示ください!)。

(参考)

通常の代入とは違い、累算代入文は右辺を評価する前に左辺を評価します。たとえば、a[i] += f(x) はまず a[i] を調べ、f(x) を評価して加算を行い、最後に結果を a[i] に割り当てます。 (http://docs.python.jp/3/reference/simple_stmts.html#augmented-assignment-statements)

Python3.xでのクロージャ

では実際にPythonクロージャをどう書くかというところで、Python3.xではnonlocalを使ってこう書く

# Python3.x
def counter():
    x = 0
    def inc():
        nonlocal x
        x += 1
        return x
    return inc

c = counter()
c()
# >>> 1    
c()
# >>> 2    
c()
# >>> 3    

nonlocal 文は、列挙された識別子がグローバルを除く一つ外側のスコープで先に束縛された変数を参照するようにします。これは、束縛のデフォルトの動作がまずローカル名前空間を探索するので重要です。この文は、中にあるコードが、グローバル (モジュール) スコープ以外のローカルスコープの外側の変数を再束縛できるようにします。 (http://docs.python.jp/3/reference/simple_stmts.html#nonlocal)

Python2.xでのクロージャ

ちなみにPython2.xでは、リストの要素を使って以下のように書いていたらしい。

# Python2.x
def counter():
    x = [0]
    def inc():
        x[0] += 1
        return x[0]
    return inc 

c = counter()
c()
# >>> 1    
c()
# >>> 2    
c()
# >>> 3    

参考

Effective Python ―Pythonプログラムを改良する59項目

Effective Python ―Pythonプログラムを改良する59項目