読者です 読者をやめる 読者になる 読者になる

suzukiyou blog

建築設計とかpythonとかその辺をとりあえずする

pythonのdecorater,getattr,classmethodとかその辺

はじめに

pythonの関数などの振る舞いを調べました。何回調べても忘れているので、記事にします。
環境はpython2.7.11です。

pythonの関数(def文)

pythonの関数は変数を探すとき以下の方法で進む。

  1. 関数内部のローカルスコープで定義されてないか探す
  2. 関数内部のローカルスコープで見つからんかったら、スコープを大きくする。
  3. 最終的にglobalまで調べて見つからんかったらNameErrorを吐きます。

実際以下のようになる。

>>>def testscope():
>>>    print g
>>>#testscope() -> NameError
>>>g=1
>>>testscope() #-> 1
>>>del g
>>>#testscope() -> NameError

グローバルでgが定義されている時にtestscope.func_globalのgのキーをdelするとグローバルでもNameErrorとなる。

>>>testscope.func_global #->{ ...... ,"g":1 }とかいう感じの辞書
>>>del testscope.func_global["g"]
>>>#g -> NameError
>>>locals() == testscope.func_global #-> True

.func_globalって単にグローバルを参照してるだけなんだなという気持ち。

pythonの関数オブジェクトの扱い

上の、「関数内部のローカルスコープで見つからんかったら、スコープを大きくする。」というのは、結構面倒くさい感じだ。
ここで関数を返す関数などをつくる。

>>>g=1
>>>def testcloserscope():
>>>    g=2
>>>    def inner():
>>>        return g
>>>    return inner
>>>tcs=testcloserscope()
>>>tcs() #->2

スコープ階層としては、
グローバルのスコープ->testcloserscopeのスコープ->innerのスコープ となる。tcsを呼ぶとinnerのスコープをまず探し、innerのスコープでgが見つからんのでtestcloserscopeのスコープを探す。gが見つかったので2を返す。innerスコープではgはローカル変数じゃないが、testcloserscopeではgがローカル変数なので、innerから見た時、グローバルのgは遮蔽されている。またinner生成時に見つかったtestcloserscopeのスコープのgは保存されているので、tcs()->2となる。というふうに理解している。保存されているというか、

>>>tcs.func_closure #-> (<cell at 0x000... :int object at 0x000...>,)
>>>dir(tcs.func_closure[0]) #->[... ,"cell_contents"]
>>>tcs.func_closure[0].cell_contents #->2

などとなっている。以下を参考としました。
pythonのクロージャ - kk6のメモ帳*

@decoratorの件

ついでに糖衣構文であるところの@decoratorみたいなものを書くと、

>>>def testdeco(func):
>>>    def wrapper(*args, **kwargs):
>>>        print "decorated:"+func.__name__
>>>        func(*args,**kwargs)
>>>    return wrapper
>>>@testdeco
>>>def testfunc():
>>>    print "hoge"
>>>testfunc()
>>>#->"decorated:testfunc"
>>>#->"hoge"
>>>testfunc.__name__ #->"wrapper"

という感じだが、これは以下と等価。

>>>def testdeco(func):
>>>    def wrapper(*args, **kwargs):
>>>        print "decorated:"+func.__name__
>>>        func(*args,**kwargs)
>>>    return wrapper
>>>def testfunc():
>>>    print "hoge"
>>>testfunc=testdeco(testfunc) #この辺を@testdeco defなどと書ける。
>>>testfunc()
>>>#->"decorated:testfunc"
>>>#->"hoge"
>>>testfunc.__name__ #->"wrapper"
>>>testfunc.func_closer #->(<cell at 0x00... :function object at 0x00... >,)

デコレート後のtestfuncに入っている実体としてはwrapperで、wrapperのローカル変数にはfuncはないけど、wrapperのクロージャに元のtestfuncが入っているのでhogeできるよ~。ということですね。

関数をクラスにぶち込むぞ!!!!という気概

クロージャはイミュータブル

上のような気概ですが、その前にクロージャがイミュータブルであることを確認します。

>>>def testcloserscope():
>>>    g=[]
>>>    def inner():
>>>        return g
>>>    return inner
>>>tcs=testcloserscope()
>>>g2=tcs() #->[]
>>>g2.append(1) #g2=[1]
>>>tcs2=testcloserscope()
>>>tcs2() #->[]

gをミュータブルな型にした。もしtcs()がtestcloserscope内のgのポインタ(的なもの)を返していれば、ポインタを操作すればtestcloserscope内のgを変更できることになる。
しかし、あくまでtcsでの保存されたスコープはクロージャであって、スコープの外側から中のgを変更することは出来なかった。(少なくともこの例では)

クラスの仕様とかのよくわからん感じを感じてもらいたい。

さて以下の様なクラスのメソッドがあります。

class A(object):
    x=1
    def hoge(self):
        return self.x

これを普通のクラスの普通のメソッドだと思っているし、実際以下のようになる。

>>> A.x #->1
>>> A.hoge #-> <unbound method A.hoge>
>>> #A.hoge() #-> unbound method hoge() must be called with A instance as first argument
>>> a=A()
>>> a.hoge #-> <bound method A.hoge of <__main__.A object at 0x00...>>
>>> a.hoge() #-> 1
>>> a.x=2
>>> a.hoge() #-> 2
>>> A.x #->1
>>> A.x=3
>>> a.x #->2
>>> a2=A()
>>> a2.hoge #->3

期待通りですね?ではこれはどうか。

class B(object):
    x=[]
    def hoge(self):
        return self.x

クラスのattributeをミュータブルな型にした。こうなる。

>>> B.x #->[]
>>> B.hoge #-> <unbound method B.hoge>
>>> #B.hoge() #-> unbound method hoge() must be called with B instance as first argument
>>> b=B()
>>> b.hoge #-> <bound method b.hoge of <__main__.B object at 0x00...>>
>>> b.hoge() #-> []
>>> b.x.append(1)
>>> b.hoge() #-> [1]
>>> B.x #->[1] !!!!
>>> B.x.append(2)
>>> b2=B()
>>> b2.hoge #->[1,2]

Bのクラス内で宣言したものがリストなどのミュータブルなものである場合、普通にインスタンスに参照が渡されているっぽく見える。
b.x=B.xめいた処理がどこかで行われている。

とりあえずメソッドの束縛とか非束縛とかについて考える。

クラス内のメソッドについては以下のように調べる。

>>> A.hoge is a.hoge #->False
>>> A.hoge.im_class #->__main__.A
>>> A.hoge.im_self #->None
>>> A.hoge.im_func #-> <function __main__.hoge>
>>> a.hoge.im_class #->__main__.A
>>> a.hoge.im_self #-> <__main__A at 0x3eab898>
>>> a.hoge.im_self is a #->True
>>> a.hoge.im_func #-> <function __main__.hoge>
>>> a.hoge.im_func is A.hoge.im_func #->True
>>> #上記はBも同様

>>> A.hoge.im_func(A) #->3
>>> A.hoge.im_func(a) #->2
>>> a.hoge.im_func(A) #->3
>>> a.hoge.im_func(a) #->2

>>> B.hoge.im_func(B) #->[1,2]
>>> B.hoge.im_func(b) #->[1,2]
>>> b.hoge.im_func(B) #->[1,2]
>>> b.hoge.im_func(b) #->[1,2]

>>> B.hoge.im_func(A) #->3

面倒なので名前で察してほしい。以下のことがわかる。

  1. A.hogeとa.hogeは異なったインスタンスである。
  2. A.hoge.im_selfはNoneであって、束縛されていない。
  3. a.hoge.im_selfはa自身で、束縛されている。
  4. A.hoge.im_func,a.hoge.im_funcは元の関数らしいので、return A.xだかreturn a.xだかとなる。
  5. .im_func(B)はなんか同じものが参照されている。
  6. .im_funcは同じような関数だったので別にB.hoge.im_func(A)としても通る。
デスクリプタのあれ

3. データモデル — Python 2.7.x ドキュメント
公式を読むとこう書いてある。

属性アクセスのデフォルトの動作は、オブジェクトの辞書から値を取り出したり、値を設定したり、削除したりするというものです。例えば、 a.x による属性の検索では、まず a.__dict__['x'] 、次に type(a).__dict__['x'] 、そして type(a) の基底クラスでメタクラスでないものに続く、といった具合に連鎖が起こります。

オーケー、調べよう。

>>>a.__dict__ #->{"x":2}
>>>a2.__dict__ #->{}
>>>A.__dict__ #->{...... ,"hoge":<function __main__.hoge>,"x":3}
>>>b.__dict__ #->{}
>>>b2.__dict__ #->{}
>>>B.__dict__ #->{...... ,"hoge":<function __main__.hoge>,"x":[1,2]}

わかりましたね?a,a2のhoge,bやb2のhogeやxは結局AやBのhogeを呼び出しているのだ。等価っぽいコードを書くと以下のようになる。

def search(ins,attrkey):
    if attrkey in ins.__dict__.keys():
        return ins.__dict__[attrkey]
    else:
        if attrkey in type(ins).__dict__.keys():
               return type(ins).__dict__[attrkey]
        else:
            ...

組み込み関数のgetattrはほぼ上のsearchのような関数であるようだ。ただし、メソッド呼び出しの場合は引数が定義時そのままの関数が呼び出されるのではなく、ラップされてboundとかunboundなメソッドが帰るようになっている。

メソッドの動的結合

関数オブジェクト以外についてはクラスオブジェクト・インスタンスオブジェクトの両方の属性に代入できる。
クラスオブジェクトに代入した場合、上述の通りインスタンスオブジェクトからも__dict__を遡って呼び出すことが出来る。
関数オブジェクトは、インスタンスの属性として代入した場合、バインドされない。
以下のようになる。

>>>def fuga(self):
>>>    return self.x**5
>>>fuga(a) #->32
>>>a.fuga=fuga
>>>#a.fuga() ->Type Error:fuga() takes exactly 1 argument(0 given)

しかしクラスに代入した場合、ラップされたメソッドになり、クラス定義時に定義した関数と同じ扱いになる。この時点ではバインドはされていない。
また、インスタンスオブジェクトから遡って呼び出せる。この場合の呼び出しはバインドされる。

>>>A.fuga=fuga
>>>a.fuga() #-> 32
>>>#A.fuga() -> unbound method

インスタンスバインドされているメソッドは呼び出すとき暗黙的に第一引数に自らを渡すことになる。関数定義時にはselfと書かれる。
バインドメソッドのように暗黙的に第一引数にcls(Aなど)を取ることにする関数を作れる。クラスメソッドという。これは関数をclassmethod()でラップしなければならない。デコレータで@classmethodと書いてもよい。

>>>@classmethod
>>>def foo(cls):
>>>    return -cls.x
>>>A.foo=foo
>>>A.foo() #->-3 
>>>a.fuga() ->-2
疲れてきた

バインドされたりされなかったりするの、なんかクラスオブジェクトに特異的な感じする。多分typeのsetattrかなんかがいろいろしているんだろうな。