在 Python 眾多原生特性中,描述符可能是最少被自定義的特性之一,但它在底層實現的方法和屬性卻無時不刻被使用著,它優雅的實現方式體現出 Python 簡潔之美。
定義
- 一個描述符是一個有“繫結行為”的物件屬性(object attribute),它的訪問控制會被描述器協議方法重寫。
- 任何定義了
__get__
,__set__
或者__delete__
任一方法的類稱為描述符類,其例項物件便是一個描述符,這些方法稱為描述符協議。 - 當對一個例項屬性進行訪問時,Python 會按
obj.__dict__
→type(obj).__dict__
→type(obj)的父類.__dict__
順序進行查詢,如果查詢到目標屬性並發現是一個描述符,Python 會呼叫描述符協議來改變預設的控制行為。 - 描述符是
@property
@classmethod
@staticmethod
和super
的底層實現機制。
特性
- 同時定義了
__get__
和__set__
的描述符稱為 資料描述符(data descriptor);僅定義了__get__
的稱為 非資料描述符(non-data descriptor) 。兩者區別在於:如果obj.__dict__
中有與描述符同名的屬性,若描述符是資料描述符,則優先呼叫描述符,若是非資料描述符,則優先使用obj.__dict__
中屬性。 - 描述符協議必須定義在類的層次上,否則無法被自動呼叫。
描述符協議
__get__(self, instance, owner)
:param self: 描述符物件本身
:param instance: 使用描述符的物件的例項
:param owner: 使用描述符的物件擁有者
__set__(self, instance, value)
:param value: 對描述符的賦值
__delete__(self, instance)
例項
class LazyProperty(object):
"""
實現惰性求值(訪問時才計算,並將值快取)
利用了 obj.__dict__ 優先順序高於 non-data descriptor 的特性
第一次呼叫 __get__ 以同名屬性存於例項字典中,之後就不再呼叫 __get__
"""
def __init__(self, fun):
self.fun = fun
def __get__(self, instance, owner):
if instance is None:
return self
value = self.fun(instance)
setattr(instance, self.fun.__name__, value)
return value
class ReadonlyNumber(object):
"""
實現只讀屬性(例項屬性初始化後無法被修改)
利用了 data descriptor 優先順序高於 obj.__dict__ 的特性
當試圖對屬性賦值時,總會先呼叫 __set__ 方法從而丟擲異常
"""
def __init__(self, value):
self.value = value
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
raise AttributeError(
"'%s' is not modifiable" % self.value
)
class Circle(object):
pi = ReadonlyNumber(3.14)
def __init__(self, radius):
self.radius = radius
@LazyProperty
def area(self):
print('Computing area')
return self.pi * self.radius ** 2
複製程式碼
參考文章
pyzh.readthedocs.io/en/latest/D…
歡迎關注
微信公眾號:面向人生程式設計
程式設計思維不應只存留在程式碼之中,更應伴隨於整個人生旅途,這個公眾號不只聊技術,還會聊產品/網際網路/經濟學等廣泛話題,所以也歡迎非程式設計師關注。