描述符是實現描述符協議方法的Python對象,當將其作為其他對象的屬性進行訪問時,該描述符使您能夠創建具有特殊行為的對象。
通常,描述符是具有“綁定行為”的對象屬性,其屬性訪問已被描述符協議中的方法所覆蓋。這些方法是__get __(),__set __()和__delete __()。如果為對象定義了這些方法中的任何一種,則稱其為描述符。屬性訪問的默認行為是從對象的字典中獲取,設置或刪除屬性。例如,a.x具有一個查找鏈,查找鏈從a .__ dict __ ['x']開始,然后鍵入(a).__ dict __ ['x'],並繼續遍歷類型(a)的基類(不包括元類)。如果查找到的值是定義描述符方法之一的對象,則Python可能會覆蓋默認行為並改為調用描述符方法。優先鏈在何處發生取決於定義了哪些描述符方法。描述符是功能強大的通用協議。它們是屬性,方法,靜態方法,類方法和super()背后的機制。在Python本身中使用它們來實現2.2版中引入的新樣式類。
descr.__get__(self, obj, type=None) -> value descr.__set__(self, obj, value) -> None descr.__delete__(self, obj) -> None
定義這些方法中的任何一個,對象被視為描述符,並且在被視為屬性時可以覆蓋默認行為。
如果對象定義了__set __()或__delete __(),則將其視為數據描述符。僅定義__get __()的描述符稱為非數據描述符(它們通常用於方法,但也可以用於其他用途)。數據和非數據描述符在實例字典中替代計算方式方面有所不同。如果實例的字典中的屬性名稱與數據描述符的名稱相同,則以數據描述符為准。如果實例的字典中具有與非數據描述符同名的屬性,則該字典屬性優先。我們來看一下例子:
class lazy(object): def __init__(self, func): self.func = func def __get__(self, instance, owner): val = self.func(instance) setattr(instance, self.func.__name__, val) return val class Circle(object): def __init__(self, radius): self.radius = radius @lazy def area(self): print('evalute') return 3.14 * self.radius ** 2 def __getattr__(self, item): return 1 c = Circle(4) print(c.area) print(c.area)
輸出結果是
evalute 50.24 50.24
我們定義了一個描述符的類 lazy,它只實現了__get__方法,是一個非數據的描述符,我們用它定義了類Circle中的area方法,所以area方法成為了一個描述符的對象,可以看到,在第一次調用c.area的時候,執行了area的方法,打印了"evalute",在第二次的時間就直接輸出了結果,沒有指向area的方法,這是為什么呢?
那么重點來了,可以看到在lazy定義的__get__方法中,執行了被描述對象的方法,也就是這里的area函數,獲取到結果之后,給當前的instance設置了一個同名的屬性,並且設值為結果,這樣下次在調用的時間,因為這是一個非數據的描述符,看上面的黑體字,實例的字典中的屬性名稱與數據描述符的名稱相同,則以數據描述符為准。所以會取你剛剛設置的屬性的值,不會再去取描述符的值。我們再來看看數據描述符的一個例子:
class lazy(object): def __init__(self, func): self.func = func def __get__(self, instance, owner): val = self.func(instance) setattr(instance, self.func.__name__, val) return val def __set__(self, instance, value): pass class Circle(object): def __init__(self, radius): self.radius = radius @lazy def area(self): print('evalute') return 3.14 * self.radius ** 2 def __getattr__(self, item): return 1 c = Circle(4) print(c.area) print(c.area)
我們看一下輸出的結果:
evalute 50.24 evalute 50.24
同樣的定義,只是在描述符中添加了__set__方法,就會執行調用描述符定義的屬性,和非描述符的調用方式天壤之別。這就是這個高級特性的特別之處。我們可以使用非數據描述符做惰性加載,只計算一次,下次直接取值,我在工作中也是這樣干的。
知其然,知其所以然,我們來看一下是為什么:
根據官方的解釋,描述符可以通過其方法名稱直接調用。例如,d .__ get __(obj)。另外,更常見的是在屬性訪問時自動調用描述符。例如,obj.d在obj的字典中查找d。如果d定義了方法__get __(),則根據下面列出的優先級規則調用d .__ get __(obj)。調用的細節取決於obj是對象還是類。
對於對象,機制位於object .__ getattribute __()中,它將b.x轉換為type(b).__ dict __ ['x'] .__ get __(b,type(b))。該實現通過優先級鏈進行工作,該優先級鏈賦予數據描述符優先於實例變量的優先級,實例變量優先於非數據描述符的優先級,並為__getattr __()分配最低優先級。完整的C實現可在Objects / object.c中的PyObject_GenericGetAttr()中找到。
對於類,機制的類型為.__ getattribute __(),它將B.x轉換為B .__ dict __ ['x'] .__ get __(無,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 __()防止自動描述符調用
-
object .__ getattribute __()和type .__ getattribute __()對__get __()進行不同的調用。
-
數據描述符始終會覆蓋實例字典。非數據描述符可以被實例字典覆蓋。
具體的可以查看Python的c源碼。
以上就是今天要和大家一起學習的內容。
代碼地址
https://github.com/oldman1991/testdemo/blob/master/0028_python_descriptor.py
更多問題歡迎關注微信公眾號