什么是描述符
描述符,是一个类,定义了另一个类的属性的访问方式,即一个类可以将属性管理委托给另一个类
特别注意:必须把描述符定义成这个类的类属性,不能为定义到构造函数中
描述符协议
描述符类基于三种特殊方法组成了描述符协议: 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 = x
def __get__(self, instance, owner):
print("调用__get__方法")
return self.x
def __set__(self, instance, value):
print("调用__set__方法")
self.x = value
class AA:
m = M(x=2) # 类属性m被M()代理
if __name__ == '__main__':
a = AA()
print(a.m)
a.m = 30
print(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 self
return '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 = chinese
self._math = math
self.name = name
@property
def math(self):
return self._math
@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("valid error!")
@property
def chinese(self):
return self._chinese
@chinese.setter
def chinese(self, value):
if 0 <= value <= 100:
self._chinese = value
else:
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 = default
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError("sccore error!")
if not 0 <= value <= 100:
raise ValueError("valid error!")
self._score = value
def __get__(self, instance, owner):
return self._score
def __delete__(self):
del self._score
class Students:
math = Score(0)
chinese = Score(0)
def __init__(self, name, math, chinese):
self._chinese = chinese
self._math = math
self.name = name
if __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 = subject
def __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] = value
else:
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_value
self.max_value = max_value
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 self
return instance.__dict__.get('field')
def __set__(self, instance, value):
value = self._validate_value(value)
instance.__dict__['field'] = value
def _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 value
class 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'>
# 2
print(r.bar)
# Call __get__, instance: <__main__.Foo object at 0x7f1c8ce5c7f0>, owner: <class '__main__.Foo'>
# 2
print(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_value
self.max_value = max_value
def __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 = name
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 self
return instance.__dict__.get(self._name)
def __set__(self, instance, value):
value = self._validate_value(value)
instance.__dict__[self._name] = value
def _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 value
class 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'>
# 2
print(r.bar)
# Call __get__, instance: <__main__.Foo object at 0x7f1c8ce5c7f0>, owner: <class '__main__.Foo'>
# None
print(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 = name
def __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] = value
def __delete__(self, instance):
print('__delete__-->', instance)
instance.__dict__.pop(self.name)
class People:
name = Str('name')
def __init__(self, name, age, salary):
self.name = name
self.age = age
self.salary = salary
print(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方法控制。 对于数据描述符,数据描述符所定义的属性存储逻辑比实例属性优先级高,无法直接重写修改