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__}')
浙公网安备 33010602011771号