第31條 用描述符改寫需要複用的@property方法

上一講 https://blog.csdn.net/minemine999/article/details/104499251講到@property的用法,可以代替gettersetter方法。

Python內置的@property修飾器,有個明顯的缺點,就是不便於複用。受它修飾的方法,無法爲同一個類中的其他屬性所複用,而且與之無關的類也無法複用這些方法。

例如,要編寫一個類,驗證學生的家庭作業成績在0~100之間。

class Homwork(object):
    def __init__(self) -> None:
        self._grade = 0
    
    @property
    def grade(self):
        return self._grade
    
    @grade.setter
    def grade(self,value):
        if not (0<=value<=100):
            raise ValueError('Grade must be between 0 and 100')
        self._grade = value
if __name__ == "__main__":
    hw1 = Homwork()
    hw1.grade = 95

現在把這套驗證邏輯放在考試成績上面,而考試成績是由多個科目所組成,那麼每一個科目都要單獨統計分數。如下所示:

class Exam(object):
    def __init__(self):
        self._written_grade = 0
        self._math_grade = 0
    
    @staticmethod
    def _check_grade(value):
        if not (0<=value<=100):
            raise ValueError('Grade must be between 0 and 100')
     
    @property
    def written_grade(self):
        return self._written_grade
    @written_grade.setter
    def written_grade(self,value):
        self._check_grade(value)
        self._written_grade = value
     
    @property
    def math_grade(self):
        return self._math_grade
    @math_grade.setter
    def math_grade(self,value):
        self._check_grade(value)
        self._math_grade = value

可以看出,該類每添加一個科目,那麼就需要寫一個@property來驗證各科科目,非常繁瑣。

Python描述符

Python描述符可以更好地實現上述功能。Python會對訪問操作進行一定的轉譯,而這種轉譯方式是通過描述符協議來實現的。

任何實現了描述符協議的類都可以作爲描述符類,其實例化對象爲描述符。描述符協議爲一組成員函數所定義,包括:
在這裏插入圖片描述
參數詳解

  • __get__(self,instance,instance_type)
  • __set__(self,instance,value)
  • __delete__(self,instance)
class A(object):
    def __get__(self,instance,instance_type):
        print('A.get')
    def __set__(self,instance,value):
        print('A.set')

class B(object):
    x = A()

if __name__ == '__main__':
    t = B()
    t.x  ##調用__get__
    t.x = 100 ##調用__set__

可以看到,實例化類B後,調用對象t訪問其屬性x,會自動調用類A__get__方法,由輸出信息可以看出:

  • self: 是A的實例對象,也是B的屬性x
  • instance: 是B的實例對象,在上面的例子是實例化後的變量t
  • instance_type: 是B這個類,因爲A是包含在它的內部,

所以B擁有所屬權,其實A類就是一個描述符(描述符是一個類),因爲類A定義了__get__, __set__方法 。

訪問t.x的時候,爲什麼會直接去調用描述符的 __get__方法?
t爲實例,訪問t.x時,其轉譯方式如下:

  1. 先訪問實例屬性,如果實例沒有該屬性x,則轉向訪問同名的類屬性
  2. 如果類屬性x是一個描述符,則Python認爲其對象遵循描述符協議。於是將 B.x轉化爲B.__dict__[‘x’].__get__(t, B)來訪問 ;
  3. 最後,進入類A__get__方法,進行相應的操作。

描述符可以把同一套邏輯運用到類中的不同屬性上面。

下面實現Grade描述符類的創建,但是此種方法實現考試成績的驗證邏輯是錯誤的。

class Grade(object):
    def __init__(self):
        self._value = 0
        
    def __get__(self,instance,instance_type):
        return self._value
        
    def __set__(self,instance,value):
        if not (0<=value<=100):
            raise ValueError('Grade must be between 0 and 100')
        self._value = value
        
class Exam(object):
    # Class attribute
    math_grade = Grade()
    written_grade = Grade()
    science_grade = Grade()
    
if __name__ == "__main__":
    first_exam = Exam()
    first_exam.written_grade = 98
    first_exam.science_grade = 100
    print(first_exam.written_grade)
    print(first_exam.science_grade)
    
    second_exam = Exam()
    second_exam.written_grade = 78
    print('second :',second_exam.written_grade)
    print('first:',first_exam.written_grade)
    

可以發現,當產生多個Exam實例時,second_exam的賦值會覆蓋掉first_exam對象的值。這是由於,所有的Exam實例都是要共享同一份Grade實例

當程序在定義Exam類的時候,它會把Grade實例創建好,以後創建Exam實例時,就不再構建Grade了。

爲了解決這個問題,可以把每個Exam實例所對應的值記錄到Grade中,利用字典保存每個實例狀態。

class Grade(object):
    def __init__(self):
        self._values = {}
    
    def __get__(self,instance,instance_type):
        if instance is None: return self
        return self._values.get(instance,0)
    
    def __set__(self,instance,value):
        if not 0<=value<=100:
            raise ValueError('Grade must be between 0 and 100')
        self._values[instance] = value

進一步,上述方法雖然能夠實現多個實例的存儲,但是考慮到Grade中的字典值,均保存實例對象的引用,造成實例引用計數無法清零,導致內存泄漏

解決這個問題的方法是使用Weakref模塊中的WeakKey-Dictionary特殊字典。

import Weakref
class Grade(object):
    def __init__(self):
        self._values = WeakKeyDictionary()
    #...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章