Using closure in Python

随分と久しぶりのエントリになってしまいました。

色々と思うところがあって、最近はPythonを使うことが多くなってきました。不慣れなこともあり、当初はなかなか思った通りに記述することが出来ませんでしたが、最近はちょっとだけ慣れてきたように感じています。

Pythonを使う頻度が高くなった経緯として、クロージャの存在が上げられます。例えば、以下はガウス分布を扱った際に記述した関数です(ガウス分布に関しては、Wikipedia - 正規分布を参照してください)。

from math import sqrt, atan2, exp

def gaussian_fn(sigma, mu):
    pi = atan2(0.0, -1.0)
    denom1 = sqrt(2.0 * pi) * sigma
    denom2 = 2.0 * sigma * sigma
    def fn(x):
        return exp(- (x - mu) * (x - mu) / denom2) / denom1
    return fn

gaussian = gaussian_fn(1.0, 0.0)
for x in xrange(-3, 4):
    print 'x=% .1f gaussian(x)=%1.6f' % (x, gaussian(x))

このコードでは、標準偏差が-3から3までの値を1刻みに出力します。実行結果は以下のようになります。

x=-3.0 gaussian(x)=0.004432
x=-2.0 gaussian(x)=0.053991
x=-1.0 gaussian(x)=0.241971
x= 0.0 gaussian(x)=0.398942
x= 1.0 gaussian(x)=0.241971
x= 2.0 gaussian(x)=0.053991
x= 3.0 gaussian(x)=0.004432

このような小さなコードを書き続け、Pythonにおけるクロージャの記載にはそれなりに慣れてきたように感じていました。しかし、今日id:nishiohirokazuさんのダイアリを見て、頭を抱えてしまいました。以下は、引用です。

Pythonだってクロージャつくれるもんっ!><

最もタメになる「初心者用言語」は Python! - 西尾泰和のはてなダイアリー
# Python
def make_counter():
    def counter():
        counter.x += 1
        print counter.x
        return counter
    counter.x = 0
    return counter

make_counter()()()()

何故、クロージャを用いるのにこのような記述をしなければならないのだろうと思い、以下のように書いてみました。

counter1.py:

#!/usr/bin/python -t

def make_counter():
    x = 0
    def counter():
        x += 1
        print x,
        return counter
    return counter

make_counter()()()()

予想に反して、結果はエラーとなりました。

./counter1.py
Traceback (most recent call last):
  File "./counter1.py", line 11, in <module>
    make_counter()()()()
  File "./counter1.py", line 6, in counter
    x += 1
UnboundLocalError: local variable 'x' referenced before assignment

これは不思議だなと思い、xを参照としてのみ用いてみました。

def make_counter():
    x = 0
    def counter():
#         x += 1
        print x,
        return counter
    return counter

make_counter()()()()

これは動作します(0 0 0と表示)。また、xを0以外に初期化した場合でも、きちんと反映されます。多分にxは参照ではなく、シャローコピーされた値であり、変更が出来ないのでしょう。

次に、以下を試してみました。

def make_counter():
    x = [0]
    def counter():
        x[0] += 1
        print x[0],
        return counter
    return counter

make_counter()()()()
make_counter()()()()

これは動作します。

1 2 3 1 2 3

念のため、このmake_counter関数を用い、以下のようなコードも試してみました。

f = make_counter()
g = make_counter()

f()	# 1と出力
f()	# 2と出力
g()	# 1と出力
f()	# 3と出力
g()	# 2と出力
g()	# 3と出力

出力は、

1 2 1 3 2 3

となりました。fとgとして保存したインスタンスごとに、異なるクロージャx異なるx*1を保持出来ているようです。

さて最後に、クロージャとして渡された値そのものは変更出来ないだろうとの予想の元に、以下のコードを試してみました。

counter4.py:

#!/usr/bin/python -t

def make_counter():
    x = [0]
    def counter():
        x = [x[0] + 1]
        print x[0],
        return counter
    return counter

make_counter()()()()

確かにエラーとなります。

Traceback (most recent call last):
  File "./counter4.py", line 11, in <module>
    make_counter()()()()
  File "./counter4.py", line 6, in counter
    x = [x[0] + 1]
UnboundLocalError: local variable 'x' referenced before assignment

まとめ

今回のエントリでは、クロージャを用いたPythonプログラミングを検証しました。しかしながら、自分の中ではその仕組みを全く消化出来ていませんし、このエントリも悪例としての価値のみしかありません。

知れば知るほど奥深い言語です。

追記(2008/8/1)

関数でのローカル変数スコープのエントリをUsing closure in Python(2)として作成しました。そちらも是非参照ください。

*1:2008/7/29修正