Pythonのdescriptor

9729 ワード

もしあなたが私と同じようにmethodとfunctionとそれらに対する様々なアクセス方法にselfパラメータの隠し伝達を含めて困惑していたら、辛抱強く見ることをお勧めします.ここではPythonのプロパティ検索ポリシーについても言及し、Pythonがobjを処理していることを明らかにしました.attrとobj.attr=valの場合、いったいどんな仕事をしたのか.Pythonでは,オブジェクトのメソッドも属性と考えられるため,以下に述べる属性にはメソッドが含まれる.
まず次のクラスを定義し、そのインスタンスも定義し、後で使用します.
class T(object):
    name = 'name'
    def hello(self):
        print 'hello'
t = T() 

dir(t)を使用して、tのすべての有効な属性をリストします.
>>> dir(t)
['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__',
 '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__',
 '__repr__', '__setattr__', '__str__', '__weakref__', 'hello', 'name']

 
属性は2種類に分けることができて、1種類はPythonが自動的に発生したので、例えば_class__,__hash__など、もう1つのクラスは私たちがカスタマイズしたもので、上のhello、nameなどです.カスタムプロパティのみに関心を持っています.クラスとインスタンスオブジェクト(実際、Pythonではすべてオブジェクトであり、クラスはtypeのインスタンス)には__があります.dict__プロパティ.カスタムプロパティが格納されます(クラスとは別のものが格納されています).
>>> t.__dict__
{}
>>> T.__dict__
<dictproxy object at 0x00CD0FF0>
>>> dict(T.__dict__)            #  T.__dict__       dict  ,      ,          
{'__module__': '__main__', 'name': 'name',
 'hello': <function hello at 0x00CC2470>,
 '__dict__': <attribute '__dict__' of 'T' objects>,
 '__weakref__': <attribute '__weakref__' of 'T' objects>, '__doc__': None}
>>> 

リストやstringのような組み込みタイプはありません.dict__属性は、任意にカスタム属性を追加することはできません.
 
今までt._dict__t上で属性をカスタマイズしていないため、空の辞書です.有効な属性helloとnameはTから得られます.Tの_dict__にはhelloとnameが含まれています.t.name文に遭遇した場合、Pythonはどのようにしてtのname属性を見つけますか?
まず、Pythonはname属性が自動的に生成される属性かどうかを判断し、自動的に生成される属性であれば、特別な方法でこの属性を見つけます.もちろん、ここのnameは自動的に生成される属性ではなく、私たち自身が定義したもので、Pythonはtの_dict__で行ないます.やはり見つからなかった.
そして、Pythonは、tが属するクラスTを見つけて、T._を検索するdict__,nameを見つけることを期待して、ラッキーで、直接見つけて、そこでnameの値を返します:文字列‘name’.T._ではdict__の中でまだ探し当てていないで、PythonはTの父類に続きますdict__に表示されます.
 
これは私たちの困惑を解決するのに十分ではありません.事はこんなに簡単ではありません.上で言ったのは実は簡略化されたステップです.
 
上記の例を続けると、name属性T.nameおよびT._についてdict__['name']は全く同じです.
>>> T.name
'name'
>>> T.__dict__['name']
'name'
>>> 

しかしhelloについては状況が少し違います
>>> T.hello
<unbound method T.hello>
>>> T.__dict__['hello']
<function hello at 0x00CC2470>
>>> 

T.helloはunbound methodであることが分かった.そしてT._dict__['hello']は関数です(メソッドではありません).
推定:メソッドはクラスの__にありますdict__には、関数として存在します(メソッドの定義は、最初のパラメータをselfに設定する以外は、関数の定義と同じです).では、T.helloが得たのも関数なのではないでしょうか.どうしてunbound methodになったのでしょうか.
インスタンスtからhelloにアクセスする
>>> t.hello
<bound method T.hello of <__main__.T object at 0x00CD0E50>>
>>> 

bound methodです.
興味深いことに、上の検索戦略に従って、Tの__にある以上dict__では、T.helloとt.helloは同じ関数であるべきです.いったいどうやって方法になったのか、そしてunbound methodとbound methodに分かれています.
unboundとboundについては理解できますが、まず、メソッドがインスタンスから呼び出されるか(インスタンスメソッド、classmethodとstaticmethodの後を指します)、T.helloのようなクラスからアクセスすると、helloはインスタンスに連絡していません.つまり、どのインスタンスにもバインドされていないので、unboundです.t.helloへのアクセス方法です.helloはtと連絡があったのでboundです.
しかし、関数から方法までは確かに理解しにくい.
 
すべての魔法は今日の主役:descriptor
 
属性を検索する場合、obj.attr、Pythonがこの属性attrを発見したら_get__メソッド、Pythonはattrを呼び出します_get__メソッド、戻り_get__メソッドの戻り値はattrではなく(この文は正確ではありません.descriptorに初歩的な概念を持ってほしいだけです).
Pythonでiterator(どうやってIteratorを引っ張ったの?)iteratorプロトコルが実装されたオブジェクトです.つまり、次の2つの方法が実装されています.iter__とnext()です.同様に、descriptorも特定の方法を実装したオブジェクトです.descriptorの特定の方法は__です.get__,__set__および_delete__,そのうち_set__および_delete__方法はオプションです.iteratorは、オブジェクトに依存して存在する必要があります(オブジェクトの__iter__メソッドによって返されます)、descriptorもオブジェクトの属性として依存し、単独では存在しません.もう1つ、descriptorはクラスの__に存在する必要があります.dict__の中で、この言葉の意味はクラスにいるだけです.dict__で属性を見つけて、Pythonはやっとそれがあるかどうかを見に行きます_get__などの方法で、1つのインスタンスに対する_dict__で見つけた属性は、Pythonが持っているかどうか全然気にしない.get__などの方法で、属性自体を直接返します.descriptorとはいったい何なのか:簡単に言えば、descriptorはオブジェクトの属性であり、クラスに存在する__にすぎない.dict__には特別な方法があります.get__(おそらく__set_と__deleteもあるかもしれませんが)特別な機能を持っており、このような属性を指すのを容易にするためにdescriptor属性という名前を付けました.
まだ分からないかもしれませんが、次に例で説明します.
まずこのクラスを定義します.
class Descriptor(object):
	def __get__(self, obj, type=None):
	        return 'get', self, obj, type
	def __set__(self, obj, val):
		print 'set', self, obj, val
	def __delete__(self, obj):
		print 'delete', self, obj

ここで_set__および_delete__実は現れなくてもいいですが、後の説明のために、しばらく全部書いておきます.
3つの方法のパラメータを説明します.
selfはもちろん,現在のDescriptorの例を指す.obj値が属性を持つオブジェクト.これは理解に難くないはずですが、前述したようにdescriptorはオブジェクトの少し特殊な属性で、ここのobjはそれを持つオブジェクトです.注意しなければならないのは、直接クラスでdescriptorにアクセスする場合(くどくどしないでください.descriptorは属性で、直接クラスでdescriptorにアクセスするのは直接クラスでクラスにアクセスする属性です)、objの値はNoneです.typeはobjのタイプで、さっき言ったように、直接クラスを通じてdescriptorにアクセスすると、objはNoneで、このときtypeはクラス自体です.
3つのメソッドの意味は、Tがクラスであると仮定し、tがそのインスタンスであり、dがTのdescriptor属性である(牛なんて、__get__メソッドがあるのではないか!)、valueは有効な値です.
属性を読み込むと、T.dのようにd._が返されます.get__(None,T)の結果、t.dはd._を返すget__(t,T)の結果.
属性を設定すると、t.d=valueとなり、実際にd._が呼び出されます.set__(t,value),T.d=value,これは真の付与値であり,T.dの値はこれからvalueになる.属性の削除と属性の設定は似ています.
次に、Pythonで実行されていることを例に挙げて説明します.
クラスTとインスタンスtを再定義
class T(object):
	d = Descriptor()
t = T()

dはTのクラス属性であり、Descriptorのインスタンスとして__があるget__などの方法で、明らかにdはすべての条件を満たして、今それはdescriptorです!
>>> t.d         #t.d,      d.__get__(t, T)
('get', <__main__.Descriptor object at 0x00CD9450>, <__main__.T object at 0x00CD0E50>, <class '__main__.T'>)
>>> T.d        #T.d,      d.__get__(None, T),  obj    None
('get', <__main__.Descriptor object at 0x00CD9450>, None, <class '__main__.T'>)
>>> t.d = 'hello'   #     descriptor   。     ,         ,  __set__   
                               print     。
set <__main__.Descriptor object at 0x00CD9450> <__main__.T object at 0x00CD0E50> hello
>>> t.d         #  ,   Python   __set__  ,     t.d  
('get', <__main__.Descriptor object at 0x00CD9450>, <__main__.T object at 0x00CD0E50>, <class '__main__.T'>)
>>> T.d = 'hello'   #    __set__  
>>> T.d                #     T.d  
'hello'
>>> t.d               #t.d     ,     ,             ,t.d  T.__dict__    
                              T.__dict__['d']   'hello',t.d    'hello'
'hello'

data descriptorとnon-data descriptor
上のdのように、同時に__を有するget__および_set__メソッド、このようなdescriptorをdata descriptorと呼びます.get__メソッドはnon-data descriptorと呼ばれます.考えやすいのは、non-data descriptorには__がないからです.set__メソッドであるため、インスタンスを介して属性に値を割り当てる場合、例えば上のt.d='hello'は、__を呼び出すことはありません.set__方法は、t.dの値を直接「hello」に変えますか?口説は根拠がなく、実例は証拠である.
class Descriptor(object):
	def __get__(self, obj, type=None):
	        return 'get', self, obj, type
class T(object):
       d = Descriptor()
t = T()
 
>>> t.d
('get', <__main__.Descriptor object at 0x00CD9550>, <__main__.T object at 0x00CD9510>, <class '__main__.T'>)
>>> t.d = 'hello'
>>> t.d
'hello'
>>> 

インスタンスにnon-data descriptorを割り当てると、インスタンスのnon-data descriptorが非表示になります.
 
本当に詳細な属性検索ポリシーを告白する時だobj.attr(注:objはクラスであってもよい):
1.attrがPythonが自動的に生成する属性であれば、見つけます!(優先度が非常に高い!)
2.obj._を検索class__.__dict__,attrが存在し、data descriptorである場合、data descriptorの_を返します.get__メソッドの結果、obj._に続行しなかった場合class__の親クラスと祖先クラスでdata descriptorを探します
3.obj._dict__で検索します.このステップは2つのケースに分けられます.1つ目はobjが普通のインスタンスで、見つかったら直接戻り、次のステップが見つからない場合です.2つ目のケースはobjがクラスであり、objとその親クラス、祖先クラスの順に__である.dict__で検索し、descriptorが見つかったらdescriptorの__を返します.get__メソッドの結果、そうでない場合はattrを直接返します.見つからなかったら、次のステップに進みます.
4.obj._class__.__dict__で検索します.descriptorが見つかった場合(ここでのdescriptorはnon-data descriptorに違いありません.data descriptorであれば、2番目のステップで見つけます)descriptorの_get__メソッドの結果.通常のプロパティが見つかった場合は、プロパティ値を直接返します.見つからなかったら、次のステップに進みます.
5.残念ながら、Pythonはついに我慢できなかった.このステップでは、raise AttributeError
 
これを利用して、descriptorがクラスにいることを強調する理由を簡単に分析します.私たちが興味を持っている検索手順は2,3,4です.ステップ2とステップ4は、クラス内で検索されます.ステップ3では、通常のインスタンスで見つかった場合、直接戻ってきて、_があるかどうかを判断していません.get__()メソッド.
 
属性に値を割り当てるときの検索ポリシー、obj.attr = value
1.obj._を検索class__.__dict__,attrが存在し、data descriptorである場合、attrの_を呼び出すset__メソッド、終了.存在しない場合はobj._に進みますclass__の親クラスと祖先クラスで検索し、data descriptorを見つけたらその__を呼び出します.set__方法.見つからなかったら次のステップに進みます.
2.obj._で直接dict__にobj._を追加dict__['attr'] = value
 
ちなみに、インスタンスにnon-data descriptorを付与すると、インスタンスのnon-data descriptorが非表示になる理由を分析します.
上のnon-data descriptorの例に続く
>>> t.__dict__
{'d': 'hello'}

tの_dict__にdという属性が表示されます.属性に割り当てられた検索ポリシーに基づいて、第1ステップは、確かにt._class__.__dict__つまりT._dict__で属性dが見つかりましたが、non-data descriptorであり、data descriptorの要件を満たさず、第2ステップに進み、直接tの_dict__プロパティには、プロパティとプロパティ値が追加されています.t.dを取得すると、検索ポリシーが実行され、ステップ2はT._dict__でdが見つかりましたが、non-data descriptorで、ステップは要求を満たし、3ステップ目を行い、tの_dict__でdが見つかり、その値「hello」が直接返されます.
 
こんなに長いこと話したのに,まだ関数と方法に着いていない.
もういい、明日話しましょう
簡単に言えば、すべての関数(メソッド)に__があります.get__クラスにあるときにメソッドdict__で、non-data descriptorです.
休憩する