:date: 2010-09-22 16:41:03 :tags: python ============================================================= やっとPythonのクロージャの仕組みを少しは理解した件 ============================================================= 先日、 `Pythonの動的クラス生成と特殊メソッドとフレームの謎`_ というBlog投稿をしたら複数の関係筋から「 `Pythonのクロージャはいわゆるレキシカルクロージャ`_ 」「 `これがPython的に正しい挙動`_ 」というツッコミをもらいました。 @atsuoishimoto さん @okuji さんありがとうございました。 .. _`Pythonの動的クラス生成と特殊メソッドとフレームの謎`: http://www.freia.jp/taka/blog/734 .. _`これがPython的に正しい挙動`: http://twitter.com/okuji/status/24442935510 .. _`Pythonのクロージャはいわゆるレキシカルクロージャ`: http://twitter.com/atsuoishimoto/status/24399596167 ということで、自分が何を分かってなかったのかのまとめです。 単純にlambdaを作った場合 ------------------------ まず、以下のようにlambdaを使って関数を作るとします。 .. code-block:: python >>> x = 1 >>> f = lambda: x この関数を呼び出したら以下のような結果になります。 .. code-block:: python >>> f() 1 ここまでは想定通りだと思いますが、以下の場合は想定通りでしょうか? .. code-block:: python >>> g = lambda: y >>> y = 1 >>> g() 1 >>> y = 2 >>> g() 2 yを後から定義しても動作します。自分は、こういう動きをすることは知っていて、そのようなコードも書いていましたが、ちゃんとは理解できていませんでした>< lambdaで関数を作ってdictに入れた場合 ------------------------------------------ .. code-block:: python >>> d = {} >>> d[1] = lambda: 1 >>> d[2] = lambda: 2 >>> d[3] = lambda: 3 >>> d[1]() 1 >>> d[2]() 2 >>> d[3]() 3 こういうことをしたいシーンは時々あります(dictのキーとlambdaの返す値が一緒かどうかはさておき)。で、こういうコードはfor文で回すコードにしたいと思いますよね。 for文でlambdaを作った場合 ----------------------------- .. code-block:: python >>> d = {} >>> for i in (1, 2, 3): ... d[i] = lambda: i ... 上記のようなコードを用意して、それぞれの関数を呼び出すと以下の結果になります。 .. code-block:: python >>> d[1]() 3 >>> d[2]() 3 >>> d[3]() 3 前述のgとyの例から、このような結果になることは想定できたはずですが、自分はこの動きは想定外でした。for文を使ったことと、dに代入するキーにもiを使ったこなど、あとは実際に書いていたコードがもうすこし複雑だったことなどが原因で、 **iの値が** lambda式の実行時に束縛されると思い込んでしまったんだと思います。 ちなみに、前の例でx=2とした時のように、i=2にすれば前述のコードと同様の結果になります。 .. code-block:: python >>> i = 2 >>> d[1]() 2 >>> d[2]() 2 >>> d[3]() 2 >>> i = d >>> d[1]() {1: at 0x027C53F0>, 2: at 0x027EECF0>, 3: at 0x027EED70>} ここで注意が必要なのは、あくまで名前とフレームオブジェクトを束縛しているのであって、値、または参照しているデータを束縛しているのではないという点。 解決版のコード ------------------------- ここまでのことから、以下のようにコードを書き換えれば、束縛されるフレームオブジェクトがlambda毎に異なるため、最初のサンプルコードと同じ結果を得ることができます。 .. code-block:: python >>> d = {} >>> for i in (1, 2, 3): ... def wrap(x): ... return lambda: x ... d[i] = wrap(i) ... >>> d[1]() 1 >>> d[2]() 2 >>> d[3]() 3 wrapという関数を呼び出すことで、lambdaが束縛する名前=x, フレームオブジェクト=wrap関数のフレーム, という組み合わせになります。lambda生成毎に関数を呼び出して個別のフレームを生成しているところがミソですね。 次の確認に向けてコードを修正 --------------------------------- とりあえずlambdaをdef文に置き換えます。 .. code-block:: python >>> d = {} >>> for i in (1, 2, 3): ... def wrap(x): ... def f(): ... return x ... return f ... d[i] = wrap(i) ... >>> d[1]() 1 さらにこれらの処理を再利用できるように、関数の中で行うようにします。 .. code-block:: python >>> def gen(): ... d = {} ... for i in (1, 2, 3): ... def wrap(x): ... def f(): ... return x ... return f ... d[i] = wrap(i) ... return d ... >>> d = gen() >>> d[1]() 1 これで下準備完了。 f()呼び出し時のローカル変数を確認 ----------------------------------- 前述のコードに以下のようにprint文を埋め込んで、f()関数内で使用できるローカル変数の一覧を確認します。 .. code-block:: python >>> def gen(): ... d = {} ... for i in (1, 2, 3): ... def wrap(x): ... def f(): ... print '%%%', locals() ... return x ... return f ... d[i] = wrap(i) ... return d ... >>> g = gen() >>> g[1]() %%% {'x': 1} 1 このように、f()の中で利用できるローカル変数はxだけす。iやdは束縛されていないためか、ローカル変数にはありません。globals() で確認すればモジュール内のグローバル変数も確認できますが、i,dは含まれていないでしょう。 ここでf()の関数定義内でiやdを参照すれば、束縛されてf()内のローカル変数として参照できます。 .. code-block:: python >>> def gen(): ... d = {} ... for i in (1, 2, 3): ... def wrap(x): ... def f(): ... i ... print '%%%', locals() ... return x ... return f ... d[i] = wrap(i) ... return d ... >>> g = gen() >>> g[1]() %%% {'i': 3, 'x': 1} 1 あとは、フレームオブジェクトはどこまで保存されるのかとか、コールスタックの途中のフレームオブジェクトは解放されるのかとか、もうちょっと調べたいことはありますが、それはまたいつか自分か、あるいは誰かが書いてくれるんじゃないかと期待。 .. スタックトレースの確認 .. --------------------------- .. .. 以下のコードをファイルに保存して実行すれば、gen()関数がコールスタックに含まれていない事がわかります。つまり束縛されているのは変数束縛されているフレームだけだと言うことになります。 .. .. .. warning:: .. .. (ここは確認が足りない。本当にgen()のフレームが束縛されていないかどうかをどうやって調べる?) .. .. .. code-block:: Python .. .. import sys .. .. def stack_list(frame): .. l = [] .. while frame: .. l.append(frame) .. frame = frame.f_back .. return l .. .. def show_stacktrace(stacks): .. for s in reversed(stacks): .. print "%s(%d)%s()" % \ .. (s.f_code.co_filename, s.f_lineno, s.f_code.co_name) .. .. def gen(): .. d = {} .. for i in (1, 2, 3): .. def wrap(x): .. def f(): .. show_stacktrace(stack_list(sys._getframe())) .. return x .. return f .. d[i] = wrap(i) .. return d .. .. d = gen() .. print d[1]() .. .. :extend type: text/x-rst .. :extend: