什么是描述符
描述符,是一个类,定义了另一个类的属性的访问方式,即一个类可以将属性管理委托给另一个类
特别注意:必须把描述符定义成这个类的类属性,不能为定义到构造函数中
描述符协议
描述符类基于三种特殊方法组成了描述符协议: set(self, instance, value):为一个属性赋值时触发(仅对实例起作用) 调用此方法以设置 instance 指定的所有者类的实例的属性为新值 value。
get(self, instance, owner=None):调用一个属性时触发(访问owner类或owner类实例对应属性) 调用此方法以获取所有者类的属性或该类的实例的属性(类属性访问/类实例属性访问) 可选的参数owner参数是所有者类, instance是用来访问属性的实例 owner:描述符对象所绑定的类 instance:假如用实例来访问描述符属性,该参数值为实例对象,如果通过类访问,该值为None
delete(self, instance):删除一个属性时触发 调用此方法以删除 instance 指定的所有者类的实例的属性
使用描述符
最常见的方式是把它的实例对象设置为其他类(常被称为owner类)的属性
实现语法
其中,类中实现了get()和set()的描述符称为数据描述符,只实现了get()的被称为非数据描述符
class M:def __init__(self, x=1):self.x = xdef __get__(self, instance, owner):print("调用__get__方法")return self.xdef __set__(self, instance, value):print("调用__set__方法")self.x = valueclass AA:m = M(x=2) # 类属性m被M()代理if __name__ == '__main__':a = AA()print(a.m)a.m = 30print(a.m)-------------------------#调用__get__方法#2#调用__set__方法#调用__get__方法#30
由上例子可得,如果一个类中的某个属性有数据描述符,那么每次查找这个属性都会调用描述符的get()这个方法并返回其值,每次对这个属性进行赋值,都会调用描述符的set()方法
通过类、实例访问描述符
class Descriptor(object):def __get__(self, instance, owner=None):print(f"Call __get__, instance: {instance}, owner: {owner}")# 当不是通过实例访问时,直接返回描述符对象if not instance:print(f"instance not exists, return {self}")return selfreturn 'test descriptor'class Foo:bar = Descriptor()"""通过类访问描述符"""# Call __get__, instance: None, owner: <class '__main__.Foo'># instance not exists, return <__main__.Descriptor object at 0x7fa610adc5f8>Foo.bar"""类实例访问描述符"""# Call __get__, instance: <__main__.Foo object at 0x7fa610adc630>, owner: <class '__main__.Foo'>Foo().bar
应用场景
实例1-1
class Students:def __init__(self, name, math, chinese):self._chinese = chineseself._math = mathself.name = name@propertydef math(self):return self._math@math.setterdef math(self, value):if 0 <= value <= 100:self._math = valueelse:raise ValueError("valid error!")@propertydef chinese(self):return self._chinese@chinese.setterdef chinese(self, value):if 0 <= value <= 100:self._chinese = valueelse:raise ValueError("valid error!")if __name__ == '__main__':std = Students("zaygee", '33', '67')print(std.name)print(std.math)---------------------------------#zaygee#33#67
上面的例子类中两个类属性:math、chinese,都使用了property对属性的合法性进行了有效控制,就是代码太冗余了
当我们使用描述符对上面的例子进行重写
class Score:def __init__(self, default=0):self._score = defaultdef __set__(self, instance, value):if not isinstance(value, int):raise TypeError("sccore error!")if not 0 <= value <= 100:raise ValueError("valid error!")self._score = valuedef __get__(self, instance, owner):return self._scoredef __delete__(self):del self._scoreclass Students:math = Score(0)chinese = Score(0)def __init__(self, name, math, chinese):self._chinese = chineseself._math = mathself.name = nameif __name__ == '__main__':std = Students("zaygee", '33', '67')print(std.name)print(std.math)print(std.chinese)----------------------------------#zaygee#33#67
应用场景-解决所有实例共享描述符
以上例子中,因为math其实是类变量,所以创建多个实例访问math其实是相当于访问类变量,会造成创建多个实例共享了描述符的情况,可以修改将变量的访问作用于instance上,使得实例之间类变量访问的隔离性
class Score:def __init__(self, subject):self.name = subjectdef __get__(self, instance, owner):print("Call __get__ func")# 变量的访问直接作用于instance上return instance.__dict__[self.name]def __set__(self, instance, value):if 0<= value <= 100:# 变量的设置也是一样作用于instance上instance.__dict__[self.name] = valueelse:raise ValueError
应用场景-一个类定义多个描述符覆盖问题
可通过使用 **set_name(self, owner, name)** 方法解决此问题 owner: 描述符对象当前绑定的类 name: 描述符所绑定的属性名称
class Descriptor(object):def __init__(self, min_value: int = 0, max_value: int = 0):self.min_value = min_valueself.max_value = max_valuedef __get__(self, instance, owner=None):print(f"Call __get__, instance: {instance}, owner: {owner}")# 当不是通过实例访问时,直接返回描述符对象if not instance:print(f"instance not exists, return {self}")return selfreturn instance.__dict__.get('field')def __set__(self, instance, value):value = self._validate_value(value)instance.__dict__['field'] = valuedef _validate_value(self, value):try:value = int(value)except (KeyError, ValueError):raise ValueError('Value is not a valid integer!')if not self.min_value < value < self.max_value:raise ValueError("not self.min_value < value < self.max_value")return valueclass Foo:bar = Descriptor(min_value=0, max_value=3)bar2 = Descriptor(min_value=0, max_value=4)# Call __get__, instance: <__main__.Foo object at 0x7fa610adc630>, owner: <class '__main__.Foo'>r = Foo()r.bar = 2# Call __get__, instance: <__main__.Foo object at 0x7f1c8ce5c7f0>, owner: <class '__main__.Foo'># 2print(r.bar)# Call __get__, instance: <__main__.Foo object at 0x7f1c8ce5c7f0>, owner: <class '__main__.Foo'># 2print(r.bar2)"""上面Foo类的bar、bar2都使用了Descriptor描述符,但是两个字段出现了相互覆盖的情况,可通过__set_name__ 方法解决"""class Descriptor(object):def __init__(self, min_value: int = 0, max_value: int = 0):self.min_value = min_valueself.max_value = max_valuedef __set_name__(self, owner, name):# 将绑定属性名保存在描述符对象中# 对于bar = Descriptor(min_value=0, max_value=3),name就是 "bar"了print(f"Call __set_name__ fun ,owner: {owner}, name: {name}")self._name = namedef __get__(self, instance, owner=None):print(f"Call __get__, instance: {instance}, owner: {owner}")# 当不是通过实例访问时,直接返回描述符对象if not instance:print(f"instance not exists, return {self}")return selfreturn instance.__dict__.get(self._name)def __set__(self, instance, value):value = self._validate_value(value)instance.__dict__[self._name] = valuedef _validate_value(self, value):try:value = int(value)except (KeyError, ValueError):raise ValueError('Value is not a valid integer!')if not self.min_value < value < self.max_value:raise ValueError("not self.min_value < value < self.max_value")return valueclass Foo:bar = Descriptor(min_value=0, max_value=3)bar2 = Descriptor(min_value=0, max_value=4)# Call __set_name__ fun ,owner: <class '__main__.Foo'>, name: bar# Call __set_name__ fun ,owner: <class '__main__.Foo'>, name: bar2# Call __get__, instance: <__main__.Foo object at 0x7fa610adc630>, owner: <class '__main__.Foo'>r = Foo()r.bar = 2# Call __get__, instance: <__main__.Foo object at 0x7f1c8ce5c7f0>, owner: <class '__main__.Foo'># 2print(r.bar)# Call __get__, instance: <__main__.Foo object at 0x7f1c8ce5c7f0>, owner: <class '__main__.Foo'># Noneprint(r.bar2)
实例1-2:以下类.属性调用方式,会报错:AttributeError: ‘NoneType’ object has no attribute ‘dict‘
原因:类去操作属性,会传None给instance
解决方法:if instance is None:return self
class Str:def __init__(self, name):self.name = namedef __get__(self, instance, owner):print("__get__-->", instance, owner)return instance.__dict__[self.name]def __set__(self, instance, value):print('__set__-->', instance, value)instance.__dict__[self.name] = valuedef __delete__(self, instance):print('__delete__-->', instance)instance.__dict__.pop(self.name)class People:name = Str('name')def __init__(self, name, age, salary):self.name = nameself.age = ageself.salary = salaryprint(People.name)-----------------# return instance.__dict__[self.name]#AttributeError: 'NoneType' object has no attribute '__dict__'#__get__--> None <class '__main__.People'>
描述符拓展
描述符是python中复杂属性访问的基础,它在内部被用于实现property、方法、类方法、静态方法和super类型
调用优先级:类属性>数据描述符>实例属性>非数据描述符>找不到的属性触发getattr()
数据描述符和非数据描述符的区别
两者最大的区别主要体现在 所绑定实例的属性存取优先级上 对于非数据描述符,可直接用 instance.attr = xxx,在实例级别重写描述符属性attr,让其读取逻辑不受get方法控制。 对于数据描述符,数据描述符所定义的属性存储逻辑比实例属性优先级高,无法直接重写修改
