深入解析Python中的descriptor描述器的作用及用法,pythondescriptor
一般來說,一個描述器是一個有“綁定行為”的對象屬性(object attribute),它的存取控制被描述器協議方法重寫。這些方法是 __get__(), __set__(), 和 __delete__() 。有這些方法的對象叫做描述器。
預設對屬性的存取控制是從對象的字典裡面(__dict__)中擷取(get), 設定(set)和刪除(delete)它。舉例來說, a.x 的尋找順序是, a.__dict__['x'] , 然後 type(a).__dict__['x'] , 然後找 type(a) 的父類(不包括元類(metaclass)).如果尋找到的值是一個描述器, Python就會調用描述器的方法來重寫預設的控制行為。這個重寫發生在這個尋找環節的哪裡取決於定義了哪個描述器方法。注意, 只有在新式類中時描述器才會起作用。(新式類是繼承自 type 或者 object 的類)
描述器是強大的,應用廣泛的。描述器正是屬性, 執行個體方法, 靜態方法, 類方法和 super 的背後的實現機制。描述器在Python自身中廣泛使用,以實現Python 2.2中引入的新式類。描述器簡化了底層的C代碼,並為Python的日常編程提供了一套靈活的新工具。
描述器協議
descr.__get__(self, obj, type=None) --> valuedescr.__get__(self, obj, value) --> Nonedescr.__delete__(self, obj) --> None
一個對象如果是一個描述器,被當做對象屬性(很重要)時重寫預設的尋找行為。
如果一個對象同時定義了__get__和__set__,它叫data descriptor。僅定義了__get__的描述器叫non-data descriptor。
data descriptor和non-data descriptor區別在於: 相對於執行個體的字典的優先順序,如果執行個體字典有與描述器具同名的屬性,如果描述器是data descriptor,優先使用data descriptor。如果是non-data descriptor,優先使用字典中的屬性。
class B(object): def __init__(self): self.name = 'mink' def __get__(self, obj, objtype=None): return self.nameclass A(object): name = B()a = A()print a.__dict__ # print {}print a.name # print minka.name = 'kk' print a.__dict__ # print {'name': 'kk'}print a.name # print kk
這裡B是一個non-data descriptor所以當a.name = 'kk'的時候,a.__dict__裡會有name屬性, 接下來給它設定__set__
def __set__(self, obj, value): self.name = value ... do somethinga = A()print a.__dict__ # print {}print a.name # print minka.name = 'kk' print a.__dict__ # print {}print a.name # print kk
因為data descriptor訪問屬性優先順序比執行個體的字典高,所以a.__dict__是空的。
描述器的調用
描述器可以直接這麼調用: d.__get__(obj)
然而更常見的情況是描述器在屬性訪問時被自動調用。舉例來說, obj.d 會在 obj 的字典中找 d ,如果 d 定義了 __get__ 方法,那麼 d.__get__(obj) 會依據下面的優先規則被調用。
調用的細節取決於 obj 是一個類還是一個執行個體。另外,描述器只對於新式對象和新式類才起作用。繼承於 object 的類叫做新式類。
對於對象來講,方法 object.__getattribute__() 把 b.x 變成 type(b).__dict__['x'].__get__(b, type(b)) 。具體實現是依據這樣的優先順序:資料描述器優先於執行個體變數,執行個體變數優先於非資料描述器,__getattr__()方法(如果對象中包含的話)具有最低的優先順序。完整的C語言實現可以在 Objects/object.c 中 PyObject_GenericGetAttr() 查看。
對於類來講,方法 type.__getattribute__() 把 B.x 變成 B.__dict__['x'].__get__(None, B) 。用Python來描述就是:
def __getattribute__(self, key): "Emulate type_getattro() in Objects/typeobject.c" v = object.__getattribute__(self, key) if hasattr(v, '__get__'): return v.__get__(None, self) return v
其中重要的幾點:
- 描述器的調用是因為 __getattribute__()
- 重寫 __getattribute__() 方法會阻止正常的描述器調用
- __getattribute__() 只對新式類的執行個體可用
- object.__getattribute__() 和 type.__getattribute__() 對 __get__() 的調用不一樣
- 資料描述器總是比執行個體字典優先。
- 非資料描述器可能被執行個體字典重寫。(非資料描述器不如執行個體字典優先)
- super() 返回的對象同樣有一個定製的 __getattribute__() 方法用來調用描述器。調用 super(B, obj).m() 時會先在 obj.__class__.__mro__ 中尋找與B緊鄰的基類A,然後返回 A.__dict__['m'].__get__(obj, A) 。如果不是描述器,原樣返回 m 。如果執行個體字典中找不到 m ,會回溯繼續調用 object.__getattribute__() 尋找。(譯者註:即在 __mro__ 中的下一個基類中尋找)
注意:在Python 2.2中,如果 m 是一個描述器, super(B, obj).m() 只會調用方法 __get__() 。在Python 2.3中,非資料描述器(除非是箇舊式類)也會被調用。 super_getattro() 的實現細節在: Objects/typeobject.c ,[del] 一個等價的Python實現在 Guido's Tutorial [/del] (譯者註:原文此句已刪除,保留供大家參考)。
以上展示了描述器的機理是在 object, type, 和 super 的 __getattribute__() 方法中實現的。由 object 派生出的類自動的繼承這個機理,或者它們有個有類似機理的元類。同樣,可以重寫類的 __getattribute__() 方法來關閉這個類的描述器行為。
描述器例子
下面的代碼中定義了一個資料描述器,每次 get 和 set 都會列印一條訊息。重寫 __getattribute__() 是另一個可以使所有屬性擁有這個行為的方法。但是,描述器在監視特定屬性的時候是很有用的。
class RevealAccess(object): """A data descriptor that sets and returns values normally and prints a message logging their access. """ def __init__(self, initval=None, name='var'): self.val = initval self.name = name def __get__(self, obj, objtype): print 'Retrieving', self.name return self.val def __set__(self, obj, val): print 'Updating' , self.name self.val = val>>> class MyClass(object): x = RevealAccess(10, 'var "x"') y = 5>>> m = MyClass()>>> m.xRetrieving var "x"10>>> m.x = 20Updating var "x">>> m.xRetrieving var "x"20>>> m.y5
這個協議非常簡單,並且提供了令人激動的可能。一些用途實在是太普遍以致於它們被打包成獨立的函數。像屬性(property), 方法(bound和unbound method), 靜態方法和類方法都是基於描述器協議的。