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 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修正