このBlogは移転しました。今後は aish.dev を御覧ください。

func_closureのひみつ

Pythonでデコレータで修飾した関数から、修飾される前の関数を取り出したいというニーズは多いようで

デコレータ式を適用した関数から元の関数名を探す - gumi Engineer’s Blog
デコレータを外す - logiqboard
デコレータを取り除いて見たかった。 - podhmoの日記

のようなブログエントリが上がっているが、Pythonでは元の関数を取り出す確実な手法は用意されていないので諦めていただきたい。デコレータを使って関数を定義しても、Pythonはその情報をどこにも記録していない。

必要であれば http://docs.pylonsproject.org/projects/venusian/dev/ のようにデコレータ自身に細工をする、テスト対象モジュールのインポート時にフックを入れ、修飾せずに元の関数を返すデコレータに置き換えるなどとするしかないだろう。

自由変数・束縛変数・クロージャ

まず、ざっくりと用語の確認をしておこう。

def spam():
    ham = 1
    def egg():
        sausage = 2
        print ham, sausage
    return egg

関数spam()では、hamという変数を作成している。このように、代入文などで変数と値を定義している変数のことを、束縛変数と呼ぶ。

関数egg()でもsausageという変数を定義しており、これも束縛変数だ。egg()では他に、自分では定義していない、親関数で定義された変数のhamを参照している。このようにして定義せずに参照されている変数を、自由変数と呼び、自由変数を参照する関数(ここではegg())はクロージャ(閉包)を持っている。

自由変数だの閉包だのとめんどくさそうな用語が出てきたが、コンピュータサイエンスっぽくてかっこいいので憶えておくと良いだろう。

以前 http://d.hatena.ne.jp/atsuoishimoto/20101006/1286338675クロージャの仕組みを簡単に解説したが、このページの解説はあまり正確ではない。全ての変数はセルオブジェクトに格納されているように書いているが、実際にはセルオブジェクトに格納される変数は自由変数だけで、束縛変数はセルを使用しないのである。この辺の話は、@Pythonのクロージャ - Google スライドで詳しく解説されているので参照していただきたい。

func_closureの中身

関数オブジェクトのfunc_closure/__closure__には、クロージャが参照している自由変数が全て格納されている。

def spam():
    ham=1    # 束縛変数
    egg=2    # 束縛変数
    def sausage():
        return ham+egg    # ham, eggは自由変数
    
    return sausage
    
print spam().func_closure

とすると、

(<cell at 0x02CBD8B0: int object at 0x01534704>, <cell at 0x02CBD830: int object at 0x01534710>)

のような結果が得られる。この二つのセルオブジェクトは、sausage()内の二つの自由変数、hameggを格納しているセルオブジェクトである。

デコレータはがしにfunc_closureを使えないわけ

これまでの解説で、func_closureはデコレータはがしには使えない事はご理解いただると思う。単純なデコレータ

def simpledeco(f):
    # fは束縛変数
    def wrapper():
        return f()    # fは自由変数
    return wrapper

では、引数で渡された関数fwrapper()内では自由変数となるため、wrapper()func_closureにはfを参照するセルオブジェクトが登録される。しかし、これはたまたまこのようなコーディングをしたからであって、例えば

def simpledeco(f):
    # fは束縛変数
    def wrapper(f=f):
        return f()    # fは束縛変数
    return wrapper

のように引数でfを渡してしまえばwrapper()クロージャを持たなくなり、func_closueは作成されなくなってしまう。また、

def simpledeco(f):
    def nuisance():
        pass

    def wrapper():
        nuisance()    # nuisanceは自由変数
        return f()    # fは自由変数
    return wrapper

とすると、関数nuisance()wrapper()から見れば自由変数になるので、func_closureにはnuisancefの両方が登録されることになる。後からfunc_closureを走査しても、どちらがデコレートしている元関数なのか、判断することは難しいだろう。