ブログトップ 記事一覧 ログイン 無料ブログ開設

agwの日記 RSSフィード

2008-07-27 Using closure in Python

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

nishiohirokazunishiohirokazu 2008/07/28 01:34 コピーをしているというより「関数の中で代入文x = 1を使っている場合、そのスコープに新たなローカル変数が作成される」が正解です。

agwagw 2008/07/28 02:04 なるほど氷解しました。上述のコードでは関数内で同名の変数を新たに作成してしまっている関係上、代入文の右辺が未定義になってしまっているという解釈でよいのですよね? def hoge(): x = xみたいな...

elecstaelecsta 2008/07/29 02:03 この python のスコープの件、最近あちこちで見ますが、やっぱ特殊なんですかね。
ということで、最後の例、x = [x[0] + 1] じゃなく、x[0] += 1 であればいいんじゃないですかね。

agwagw 2008/07/29 18:05 > elecstaさん
コメントありがとうございます。大変分かり辛く恐縮ですが、ご指摘の例はエントリ中段ほどに記載しております。
特殊か否かはまだ何とも分からんのですが、Pythonのクロージャになかなか慣れることが出来ません...

elecstaelecsta 2008/07/30 07:45 あいやー。ありましたね。すみません。
私は Python でクロージャを使う場面がどうしても思いつかないのです。
javascript なんかでは意識せずに使ってるんですけど、、、

agwagw 2008/07/30 11:48 > elecstaさん
いえいえ、どうもありがとうございます。
Pythonでは、クラスをきっちり書けばいらないようなものなんでしょうかね > クロージャ
私の場合、完全にJavaScriptののノリで書いているのが悪いのですけれども...

hidezeehidezee 2011/11/25 08:40 agwさんのリストxを使ったクロージャmake_counter()の例は大変参考になりました。ありがとうございます。
ただ、引用されている西尾さんの元々のmake_counter()はクロージャになっていないのではないでしょうか。
というのは、このmake_counter()が生成する関数オブジェクトは一見クロージャのように振る舞いますが、生成後に外から初期化できてしまうからです。これではクロージャ(閉包)とは呼べないのでは??と思いました。
cnt = make_counter()
cnt() # → 1
cnt() # → 2
cnt() # → 3
cnt().x = 100 # 外から初期化できる
cnt() # → 101
cnt() # → 102
・・・

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証