Python描述符

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…]

參考文檔

  1. python描述符(descriptor)、屬性(property)、函數(類)裝飾器(decorator )原理實例詳解 , 陳洋Cy
  2. 描述符:其實你不懂我(一),公衆號:Python編程時光
  3. 解密 Python 的描述符(descriptor)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章