上一讲 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()
#...