上一講 https://blog.csdn.net/minemine999/article/details/104499251講到@property
的用法,可以代替getter
和setter
方法。
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
時,其轉譯方式如下:
- 先訪問實例屬性,如果實例沒有該屬性
x
,則轉向訪問同名的類屬性; - 如果類屬性
x
是一個描述符,則Python認爲其對象遵循描述符協議。於是將B.x
轉化爲B.__dict__[‘x’].__get__(t, B)
來訪問 ; - 最後,進入類
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()
#...