Descriptor Any object which defines the methods get(), set(), or delete(). When a class attribute is a descriptor, its special binding behavior is triggered upon attribute lookup. Normally, using a.b to get, set or delete an attribute looks up the object named b in the class dictionary for a, but if b is a descriptor, the respective descriptor method gets called.
從表現形式來,一個對象如果實現了__get__, __set__, __del__
方法(三個方法不一定要全都實現),那麼這個對象就是一個描述符。__get__, __set__, __del__
的具體聲明如下:
# 用於訪問屬性。它返回屬性的值,若屬性不存在、不合法等都可以拋出對應的異常
__get__(self,instance,owner)
# 在屬性分配操作中調用,不會返回任何內容
__set__(self,instance,value)
# 控制刪除操作,不會返回內容
__del__(self,instance)
Python是動態類型解釋性語言,不像C/C++等靜態編譯型語言,數據類型在編譯時便可以進行驗證。在Python中必須添加額外的類型檢查代碼才能做到這一點,這就是描述符設計的初衷。
比如,現在有一個Student類:
class Student(object):
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english
def __repr__(self):
return "<Student: {}, math:{}, chinese: {}, english: {}>".format(
self.name, self.math, self.chinese, self.english
)
stu1 = Student('Alex', 66, 77, 88)
print(stu1)
>>>
<Student: Alex, math:66, chinese: 77, english: 88>
看起來一切都很順利,但是代碼並不像人那麼智能,不會根據實際使用場景自動判斷數據的合法性,比如在錄入會員某門成績時,不小心多輸入了一位或者根本就是不合法的數字,程序是無法感知的,所以我們還需要在代碼中加入判斷邏輯,完善後代碼如下:
class Student(object):
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english
@property
def math(self):
return self._math
@math.setter
def math(self, score):
if 0 <= score <= 100:
self._math = score
else:
raise ValueError("Valid score must be in [0, 100]")
@property
def chinese(self):
return self._chinese
@chinese.setter
def chinese(self, score):
if 0 <= score <= 100:
self._chinese = score
else:
raise ValueError("Valid score must be in [0, 100]")
@property
def english(self):
return self._english
@english.setter
def english(self, score):
if 0 <= score <= 100:
self._english = score
else:
raise ValueError("Valid score must be in [0, 100]")
def __repr__(self):
return "<Student: {}, math:{}, chinese: {}, english: {}>".format(
self.name, self.math, self.chinese, self.english
)
stu1 = Student('Alex', 666, 77, 88)
print(stu1)
>>>
ValueError: Valid score must be in [0, 100]
在上面的代碼中,使用了 property 特性,把函數調用僞裝成對屬性的調用,並對屬性的合法性進行了有效控制,從功能上來說沒有問題,但是重複代碼率非常高,如果有更多個屬性需要做判斷,那麼代碼就會變得更加冗長,雖然property可以讓類從外部看起來整潔漂亮,但是卻做不到內部同樣整潔漂亮。這個時候,使用描述符就可以解決這個矛盾,它是@property的升級版,允許我們爲重複的property邏輯編寫單獨的類來進行處理。
不要嘗試在
__int__
方法中使用if ..else
的方法來對屬性進行控制,這對於已經存在的實例對象毫無幫助。
使用描述符,對上面的代碼進行重寫:
class Score:
def __init__(self, default=0):
self._score = default
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError('Score must be integer')
if not 0 <= value <= 100:
raise ValueError('Valid score must be in [0, 100]')
self._score = value
def __get__(self, instance, owner):
return self._score
class Student:
math = Score()
chinese = Score()
english = Score()
def __init__(self, name, math, chinese, english):
self.name = name
self.math = math
self.chinese = chinese
self.english = english
def __repr__(self):
return "<Student: {}, math:{}, chinese: {}, english: {}>".format(
self.name, self.math, self.chinese, self.english
)
stu1 = Student('Alex', 66, 77, 88)
print(stu1)
如上所述,Score 類就是一個描述符對象,當從 Student 的實例訪問 math、chinese、english這三個屬性的時候,都會經過 Score 類裏的特殊的方法,由描述符對象爲我們做合法性檢查,這就避免了使用property 時出現的大量代碼無法複用的問題。
在使用描述符時,爲了讓描述符能夠正常工作,它們必須定義在類的層次上。如果不這麼做,那麼Python將無法自動調用__get__
和__set__
方法。如下:
class Test():
t1 = Score(20)
def __init__(self):
self.t2 = Score(20)
運行結果>>>
20 <__main__.Score object at 0x10d0987f0>
可以看到,訪問類層次上的描述符t1可以自動調用__get__
,但是訪問實例層次上的描述符 t2 只會返回描述符本身!
確保實例的數據只屬於實例本身
看下面的例子:
class Rethink():
math = Score(80)
me = Rethink()
Alex = Rethink()
print(me.math, Alex.math)
me.math=90
print(me.math, Alex.math)
>>>
80 80
90 90
運行結果出人意料,這說明所有的Test對象的實例竟然都共享相同的math屬性!這簡直讓人無法接受,它也是描述符中最令人感到彆扭的地方。爲了解決這個問題,我們需要在描述符對象中使用數據字典,__get__
和__set__
的第一個參數告訴我們需要關心哪一個實例,描述符使用這個參數作爲字典的key,爲每一個實例單獨保存一份數據。修改後的Score類的代碼如下:
from weakref import WeakKeyDictionary
class Score:
def __init__(self, default=0):
self.default = default
self.data = WeakKeyDictionary()
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError('Score must be integer')
if not 0 <= value <= 100:
raise ValueError('Valid score must be in [0, 100]')
self.data[instance] = value
def __get__(self, instance, owner):
return self.data.get(instance, self.default)
class Rethink():
math = Score(80)
me = Rethink()
Alex = Rethink()
print(me.math, Alex.math)
me.math=90
print(me.math, Alex.math)
>>>
80 80
90 80
[To be continued…]
參考文檔
- python描述符(descriptor)、屬性(property)、函數(類)裝飾器(decorator )原理實例詳解 , 陳洋Cy
- 描述符:其實你不懂我(一),公衆號:Python編程時光
- 解密 Python 的描述符(descriptor)