Descriptor是什么?簡而言之,Descriptor是用來定制訪問類或實例的成員的一種協議。額。。好吧,一句話是說不清楚的。下面先介紹一下Python中成員變量的定義和使用。
我們知道,在Python中定義類成員和C/C++相比得到的結果具有很大的差別。如下面的定義:
class Cclass { int I; void func(); }; Cclass c;在上面的定義中,C++定義了一個類型,所有該類型的對象都包含有一個成員整數i和函數func;而Python則創建了一個名為Pclass、類型(__class__)為type(詳情請參見MetaClass,Python中一切皆為對象,類型也不例外)的對象,然后再創建一個名為p、類型為Pclass的對象。如下所示:
In [71]: type(pclass) Out[71]: <type 'type'> In [72]: p = pclass() In [73]: type(p) Out[73]: <class '__main__.pclass'>
p和Pclass各自包含了一些成員,如下所示:
1 p.__class__ p.__init__ p.__sizeof__
2 p.__delattr__ p.__module__ p.__str__
3 p.__dict__ p.__new__ p.__subclasshook__
4 p.__doc__ p.__reduce__ p.__weakref__
5 p.__format__ p.__reduce_ex__ p.f
6 p.__getattribute__ p.__repr__ p.i
7 p.__hash__ p.__setattr__
其中,帶有雙下劃線的成員為特殊成員,或者可以稱之為固定成員(和__slots__定義的成員類似),這些成員變量的值可以被改變,但不能被刪除(del)。其中,__class__變量為對象所屬的類型,__doc__為對象的文檔字符串。有一個特殊成員值得注意:__dict__,該字典中保存了對象的自定義變量。相信大家在初學Python對于其中對象可以任意增加刪除成員變量的能力感到驚訝,其實這個功能的玄機就在于__dict__成員中(注意type的__dict__為dictproxy類型):
In [10]: p.x = 2 In [11]: p.__dict__ Out[11]: {'x': 2}通過上面的演示可以很清楚地看出:Python將對象的自定義成員以鍵值對的形式保存到__dict__字典中,而前面提到的類型定義只是這種情況的語法糖而已,即上面的類型定義等價于以下形式的定義:
Class Pclass(object): pass Pclass.i = 1 Pclass.f = lambda x: x
訪問成員變量時,Python也是從__dict__字典中取出變量名對應的值,如下形式的兩種訪問形式是等價的――在Descriptor被引入之前:
p.i p.__dict__['i']
Descriptor的引入即將改變上面的規則,且看下文分解。
定義:Descriptor Protocol
Descriptor如何改變對象成員的訪問規則呢?根據計算機理論中“絕大多數軟件問題都可以用增加一個中間層的方式解決”這一名言,我們需要為對象訪問提供一個中間層,而非直接訪問所需的對象。實現這一中間層的方式是定義Descriptor協議。Descriptor的定義很簡單,如果一個類包含以下三個方法(之一),則可以稱之為一個Descriptor:
1.object.__get__(self, instance, owner)
成員被訪問時調用,instance為成員所屬的對象、owner為instance所屬的類型
2.object.__set__(self, instance, value)
成員被賦值時調用
3.0object.__delete__(self, instance)
成員被刪除時調用
如果我們需要改變一個對象在其它對象中的訪問規則,需要將其定義成Descriptor,之后在對該成員進行訪問時將調用該Descriptor的相應函數。下面是一個使用Descriptor改變訪問規則的例子:
class MyDescriptor(object): def __init__(self, x): self.x = x def __get__(self, instance, owner): print 'get from descriptor' return self.x def __set__(self, instance, value): print 'set from descriptor' self.x = value def __delete__(self, instance) print 'del from descriptor, the val is', self.x class C(object): d = MyDescriptor('hello') >> C.d get from descriptor >> c = C() >> c.d get from descriptor >> c.d = 1 set from descriptor >> del c.d del from descriptor, the val is 1從例子中可以看出:當我們對對象成員進行引用(Reference)、賦值(Assign)和刪除(Dereference)操作時,如果對象成員為一個Descriptor,則這些操作將執行該Descriptor對象的相應成員函數。以上約定即為Descriptor協議。
obj.name背后的魔法
引入了Descriptor之后,Python對于對象成員訪問的規則是怎樣的呢?在回答這一問題之前,需要對Descriptor進行簡單的劃分:
Overriding或Data:對象同時提供了__get__和__set__方法
Nonoverriding或Non-Data:對象僅提供了__get__方法
(__del__方法表示自己被忽略了,很傷心~)
下面是從一個類對象中訪問其成員(如C.name)的規則:
如果“name”在C.__dict__能找到,C.name將訪問C.__dict__['name'],假設為v。如果v是一個Descriptor,則返回type(v).__get__(v, None, C),否則直接返回v;
如果“name”不在C.__dict__中,則向上查找C的父類,根據MRO(Method Resolution Order)對C的父類重復第一步;
還是沒有找到“name”,拋出AttributeError異常。
從一個類實例對象中訪問其成員(如x.name,type(x)為C)要稍微復雜一些:
如果“name”能在C(或C的父類)中找到,且其值v為一個Overriding Descriptor,則返回type(v).__get__(v, x, C)的值;
否則,如果“name”能在x.__dict__中找到,則返回x.__dict__['name']的值;
如果“name”仍未找到,則執行類對象成員的查找規則;
如果C定義了__getattr__函數,則調用該函數;否則拋出AttributeError異常。
成員賦值的查找規則與訪問規則類似,但還是有一點區別:對類成員執行賦值操作時將直接設置C.__dict__中的值,而不會調用Descriptor的__set__函數。
以上面的代碼為例,當訪問C.d時,Python將在C.__dict__中找到d,并且發現d是一個Descriptor,因此將調用d.__get__(None, C);當訪問c.d時,Python首先查找C,并且在其中發現d的定義,且d為一個Overriding Descriptor,因此執行d.__get__(c, C)。
前面介紹了Descriptor的一些細節,那么Descriptor的作用是什么呢?在Python中,Descriptor主要用來實現一些Python本身的功能,如類方法調用、staticmethod和Property等。下面將對這些使用Descriptor進行類方法調用的實現進行介紹。
Bound & Unbound Method
在python中,函數是第一級的對象,即其本質與其它對象相同,差別在于函數對象是callable對象,即對于函數對象f,可以用語法f()來調用函數。上面提到的對象成員訪問規則,對于函數來說是完全一樣的。Python在實現成員函數調用時obj.f()時,會執行一下兩個步驟:
根據對象成員訪問規則獲取函數對象;
用函數對象執行函數調用;
為了驗證上述過程,我們可以執行以下代碼:
Class C(object): def f(self): pass >> fun = C.f Unbound Method >> fun() >> c = C() >> fun = c.f Bound Method >> fun()
我們可以看到C.f和c.f返回了instancemethod類型的對象,這兩個對象也是可調用的,但是卻不是我們本以為的func對象。那么instancemethod對象和func對象之間具有什么關聯呢?
func類型:func類型為Python中原始的函數對象類型,即def f(): pass將定義一個func類型的對象f;
instancemethod:func的一個wrapper,如果類方法沒有綁定到對象,則該instancemethod為一個Unbound Method,對Unbound Method的調用將導致TypeError錯誤;如果類方法綁定到了對象,則該instancemethod為一個Bound Method,對Bound Method的調用不許要指定self參數的值。
如果查看Unbound Method對象和Bound Method對象的成員,我們可以發現它們都包含了一下三個成員:im_func、im_self和im_class。其中im_func為所封裝的func對象,im_self則為所綁定對象的值,而im_class則為定義該函數的類對象。由此我們可以知道,Python會根據不同的情況返回函數的不同wrapper,當通過類對象訪問函數時,返回的是名為Unbound Method對象的Wrapper,而通過類實例訪問函數是,返回的則是綁定了該實例的名為Bound Method對象的Wrapper。
現在是Descriptor大顯身手的時候了。
Python中將func定義為一個Overriding Descriptor,在其__get__方法中構造一個instancemethod對象,并根據被訪問函數被訪問的情況設置im_func、im_self和im_class成員。在instancemethod實例被調用時,則根據im_func和im_self來完成真正的函數調用。演示這一過程的代碼如下:
Class instancemethod(object): def __call__(self, *args): if self.im_self == None: raise 'unbound error' return self.im_func(self.im_self, *args) def __init__(self, im_self, im_func, im_class): self.im_self = im_self self.im_func = im_func self.im_class = im_class class func(object): ... def __get__(self, instance, owner): return instancemethod(instance, self, owner) def __set__(self, instance, value): pass ...
一個小問題的解決
分享一下剛遇到的一個小問題,我有一段類似于這樣的python代碼:
# coding: utf-8class A(object): @property def _value(self):# raise AttributeError("test") return {"v": "This is a test."} def __getattr__(self, key): print "__getattr__:", key return self._value[key]if __name__ == '__main__': a = A() print a.v運行后可以得到正確的結果
__getattr__: vThis is a test.
但是注意,如果把
# raise AttributeError("test")這行的注釋去掉的話,即在_value方法里面拋出AttributeError異常,事情就會變得有些奇怪。程序運行的時候并不會拋出異常,而是會進入一個無限遞歸:
File "attr_test.py", line 12, in __getattr__ return self._value[key] File "attr_test.py", line 12, in __getattr__ return self._value[key]RuntimeError: maximum recursion depth exceeded while calling a Python object
通過多方查找后發現是property裝飾器的問題,property實際上是一個descriptor。在python doc中可以發現這樣的文字:
object.__get__(self, instance, owner)
Called to get the attribute of the owner class (class attribute access) or of an instance of that class (instance attribute access). owner is always the owner class, while instance is the instance that the attribute was accessed through, or None when the attribute is accessed through the owner. This method should return the (computed) attribute value or raise an AttributeError exception.
這樣當用戶訪問._value時,拋出了AttributeError從而調用了__getattr__方法去嘗試獲取。這樣程序就變成了無限遞歸。
這個問題看上去不復雜,但是當你的_value方法是比較隱晦的拋出AttributeError的話,調試起來就會比較困難了。
小結
Descriptor是訪問對象成員時的一個中間層,為我們提供了自定義對象成員訪問的方式。通過對Descriptor的探索,對原來的一些看似神秘的概念頓時有種豁然開朗的感覺:
類方法調用:編譯器并沒有為其提供專門的語法規則,而是使用Descriptor返回instancemethod來封裝func,從而實現類似obj.func()的調用方式;
staticmethod:decorator將創建一個StaticMethod并在其中保存func對象,StaticMethod是一個Descriptor,其__get__函數中返回前面所保存的func對象;
Property:創建一個Property對象,在其__get__、__set__和__delete__方法中分別執行構造對象是傳入的fget、fset、和fdel函數。現在知道為什么Property只提供這三個函數作為參數么。。
最后一個問題是,Python引入Descriptor之后的性能會不會有影響?性能影響是必須的:每次訪問成員時的查找規則,之后再調用Descriptor的__get__函數,如果是方法調用的話之后才是執行真正的函數調用。每次訪問對象成員時都要經歷以上過程,對Python的性能應該會有較大的影響。但是,在Python的世界,貌似Pythonic才是被關注的重點,性能神馬的就別提了。。
新聞熱點
疑難解答
圖片精選