Python描述符的一些补充
最近有空在看新版的《Effective Python》一书,看到了一些描述符的使用。
补充一些我的一些自己看法
当一个实例成为一个类的属性时,当这个类有__get__属性,__set__属性,这个类的实例成为类的属性就称为描述符
我们的日常使用中,函数就属于描述符。因为创建函数的类有__get__属性
首先,我来解释一下,覆盖性与非覆盖型的描述符区别
覆盖与非覆盖,是相对实例属性来说的
# 创建描述符 class Quantiy: # 外部定一个描述符类属性 __count = 0 def __init__(self): # 描述符初始化赋值 cls = self.__class__ prefil = cls.__name__ index = cls.__count # 设立一个独一无二的名称 self.storage = '_{}#{}'.format(prefil, index) cls.__count += 1 def __get__(self, instance, owner): return 'This is quantiy' class T1: q = Quantiy() def __init__(self): self.q = 1 self.hello = 666 def hello(self): return 'h_func' if __name__ == '__main__': t = T1() print(t.q) print(t.hello)
上面的代码运行,输出还是self的实例属性,实例通过点的方式取属性,还是获取实例自身__dict__中的属性
# 创建描述符 class Quantiy: # 外部定一个描述符类属性 __count = 0 def __init__(self): # 描述符初始化赋值 cls = self.__class__ prefil = cls.__name__ index = cls.__count # 设立一个独一无二的名称 self.storage = '_{}#{}'.format(prefil, index) cls.__count += 1 def __get__(self, instance, owner): return 'This is quantiy' def __set__(self, instance, value): ... class T1: q = Quantiy() def __init__(self): self.q = 1 self.hello = 666 def hello(self): return 'h_func' if __name__ == '__main__': t = T1() print(t.q) print(t.hello)
当在描述符中加入了__set__之后,对于实例通过点的方式取值或者设置同名的实例属性时,会设置该类的同名属性优先,也就是对描述符[实例]进行操作
后面经过研究,其实还挺复杂的逻辑,网上找到一个坐着写的挺好的。
参考链接:https://halfclock.github.io/2019/06/04/python-descriptor_02/
他对覆盖型与非覆盖型的描述符有着明确的说明
各类描述符的使用场景
全覆盖型描述符
这里指实现了
__set__
和__get__
协议的描述符。
能够使用全覆盖型描述符的场景,通常还需要考虑是否使用特性。这部分可以参照上一篇博文最后的总结。
此类描述符还有另一个使用的场景,即只读属性,只读属性的 __set__
只需要抛出指定的异常即可。
必须设置 __get__
的原因是,防止用户使用 __dict__
直接对实例属性进行修改,因为覆盖型描述符不管是否有实例属性,在读值时都会访问 __get__
方法。
半覆盖型描述符
这里指没有
__get__
方法的覆盖型描述符。
此类描述符通常用于验证属性。
即检查用户给的 value 是否符合系统定义的规则,如果符合规则,才将之存储至实例属性中,当需要拿到实例属性时,不用通过 __get__
,直接访问实例属性即可能快速的拿到需要的值。
非覆盖型描述符
这里指没有
__set__
方法的覆盖型描述符。
此类描述符适合使用在第一次访问需要加载数据(花费时间长)的场景。———— 高效缓存
因为第一次访问实例属性时,调用描述符实例的 __get__
方法,在该方法中加载数据,然后将加载完成的数据(value),使用 obj. attr = value 赋给实例属性。
之后再访问实例属性就无需加载数据,不再访问描述符实例的 __get__
方法了,直接访问实例属性即可。
总结
本篇博文与上一篇博文总结了属性描述符是什么、怎么使用、以及何时使用的问题。
指出了属性描述符是实现了描述符协议的类、其实例通常被托管类类属性所承载、并且根据是否实现 __set__
方法,分为覆盖型描述符和非覆盖型描述符,他们分别应用于只读属性、属性验证和高效缓存中。
# 创建描述符 class Quantiy: # 外部定一个描述符类属性 __count = 0 def __init__(self): # 描述符初始化赋值 cls = self.__class__ prefil = cls.__name__ index = cls.__count # 设立一个独一无二的名称 self.storage = '_{}#{}'.format(prefil, index) cls.__count += 1 def __get__(self, instance, owner): return instance.__dict__[self.storage + str(id(instance))] def __set__(self, instance, value): # 托管属性 描述符示例操作托管示例对属性进行赋值 if value > 0: # 增强属性赋值,通过计算器与实例的id作为每个实例的属性唯一码 instance.__dict__[self.storage + str(id(instance))] = value else: raise ValueError('value must be > 0') class LineItem: # 描述符实例赋值给托管类属性 weight = Quantiy() price = Quantiy() def __init__(self, description, weight, price): # 储存实例,托管实例中存储自身托管属性的属性 self.weight与self.price self.description = description # 这个按照书中的说法叫特性的赋值方法了,不是属性赋值了。 self.weight = weight self.price = price @property def subtoall(self): return self.weight * self.price
上面的代码是摘抄至流畅的Python一书,由于Python模块在导入执行后,类的描述符只会初始化一次,通过描述符的加工器给实例属性的__dict__中赋值属性我给加上了实例的id码,做唯一。
这样,基本确保了每次实例出来的对象通过获取的属性不会出问题
前面我的理解错了,其实对于不同的实例,通过描述符赋值相同的属性名是正确的,这并不会影响各个实例的属性之间干扰
# 创建描述符 class Quantiy: # 外部定一个描述符类属性 __count = 0 def __init__(self): # 描述符初始化赋值 cls = self.__class__ prefil = cls.__name__ index = cls.__count # 设立一个独一无二的名称 self.storage = '_{}#{}'.format(prefil, index) cls.__count += 1 def __get__(self, instance, owner): # return instance.__dict__[self.storage + str(id(instance))] return instance.__dict__[self.storage] def __set__(self, instance, value): # 托管属性 描述符示例操作托管示例对属性进行赋值 if value > 0: # 增强属性赋值,通过计算器与实例的id作为每个实例的属性唯一码 # instance.__dict__[self.storage + str(id(instance))] = value instance.__dict__[self.storage] = value else: raise ValueError('value must be > 0') class LineItem: # 描述符实例赋值给托管类属性 weight = Quantiy() price = Quantiy() def __init__(self, description, weight, price): # 储存实例,托管实例中存储自身托管属性的属性 self.weight与self.price self.description = description # 这个按照书中的说法叫特性的赋值方法了,不是属性赋值了。 self.weight = weight self.price = price @property def subtoall(self): return self.weight * self.price
这样是对的
如果通过秒速符对实例的__dict__属性进行操作,可以理解为描述符就是一个工具而已,上面的方式,没有用到描述符实例内部的属性来保存相对托管类的实例的属性。
确实非常不错
下面是我摘抄至《Effective Python》书中使用描述的方式,他直接将描述符实例放入托管类,并且没有进行__init__的托管类实例化函数的操作
所以他相关的实例属性[伪],其实托管类的实例__dict__中还是空的
# Example 14 from weakref import WeakKeyDictionary class Grade: def __init__(self): self._values = WeakKeyDictionary() 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 # Example 15 class Exam: math_grade = Grade() writing_grade = Grade() science_grade = Grade() first_exam = Exam() first_exam.writing_grade = 82 second_exam = Exam() second_exam.writing_grade = 75 print(f'First {first_exam.writing_grade} is right') print(f'Second {second_exam.writing_grade} is right') print(first_exam.__dict__) print(second_exam.__dict__)
两种方式的操作,我还是更加喜欢第一种,话说流畅的Python一书确实很多操作与解释很棒,最近太懒了。哈哈,要么等新版的流畅的Python一书出来,再看一本
希望我英语能够学好,下次直接看新版的英文原书
参照书<effective python>书中第50章节的代码参考,通过__set_name__初始化描述符属性,可以获取属性赋值时的变量名值
# Example 11 class Field: def __init__(self): print('__init__') self.name = None self.internal_name = None # 自动触发在__init__之后 def __set_name__(self, owner, name): print('__set_name__') # Called on class creation for each descriptor self.name = name self.internal_name = '_' + name def __get__(self, instance, instance_type): if instance is None: return self return getattr(instance, self.internal_name, '') def __set__(self, instance, value): setattr(instance, self.internal_name, value) # Example 2 class Customer: # Class attributes first = Field() # Example 3 cust = Customer() print(f'Before: {cust.first!r} {cust.__dict__}') cust.first = 'Euclid' print(f'After: {cust.first!r} {cust.__dict__}')