描述符是对多个属性运用相同存取逻辑的一种方式,,是实现了特性协议的类,这个协议包括了__get__、__set__和__delete__方法。property类实现了完整的描述符协议。通常,可以只实现部分协议,如只实现了__get__或__set__,而不必把__get__、__set__和__delete__全部实现
现在,让我们用描述符协议升级上一个章节Python动态属性和特性(二)的LineItem类
图1-1
我们将定义一个Quantity类,LineItem类会用到两个Quantity实例:一个用于管理 weight属性,另一个用于管理 price属性。weight这个属性出现了两次,但两次都有不同,一个是LineItem的类属性,另一个是各个LineItem 对象的实例属性,同理price
现在,让我们看一些定义:
下面,让我们来看一个例子
class Quantity: # <3> def __init__(self, storage_name): # <4> self.storage_name = storage_name def __set__(self, instance, value): # <5> if value > 0: instance.__dict__[self.storage_name] = value else: raise ValueError(‘value must be > 0‘) class LineItem: weight = Quantity(‘weight‘) # <1> price = Quantity(‘price‘) def __init__(self, description, weight, price): # <2> self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
我们将上面的代码与之前的定义对应起来,首先是Quantity类,我们之前说过,只要实现了__set__、__get__或__delete__方法的类,就是描述符类,所以Quantity毫无疑问的是描述符类,再来是LineItem,根据之前的定义,托管类中的类属性,是描述符类的实例,LineItem类的weight和price两个类属性都是Quantity描述符类的实例,所以LineItem类即为托管类,再来,我们根据代码中的标号分析一下代码:
现在让我们来测试这个类,我们故意将传入的price设为0:
truffle = LineItem(‘White truffle‘, 100, 0)
运行结果:
Traceback (most recent call last): …… ValueError: value must be > 0
可以看到,在设置值得时候确实触发了__set__方法
另外还要重复声明一点:__set__方法中的参数,self和instance分别为描述符实例和托管类实例,instance代表要设置属性的那个对象,而self(描述符实例)则保存了要设置属性的属性名,在上个例子中,如果我们在__set__方法要设置LineItem实例只能用这样的方式:
instance.__dict__[self.storage_name] = value
如果尝试用setattr()方法来赋值
class Quantity: def __init__(self, storage_name): self.storage_name = storage_name def __set__(self, instance, value): if value > 0: setattr(instance, self.storage_name, value) else: raise ValueError(‘value must be > 0‘)
测试:
truffle = LineItem(‘White truffle‘, 100, 10)
运行结果:
Traceback (most recent call last): …… RecursionError: maximum recursion depth exceeded
我们会发现,如果用setattr()方法来赋值,会产生堆栈异常,为什么会这样呢?假设obj是LineItem实例,obj.price = 10和setattr(obj, "price", 10)一样,都会调用__set__方法,如果用setattr()方法来设置值,会不断调用__set__方法,最终产生堆栈异常
上面的例子,LineItem有个缺点,在托管类中每次实例化描述符时都要重复输入属性名,现在,让我们再改造一下LineItem类,使得不需要输入属性名。为了避免在描述符实例中重复输入属性名,我们将每个Quantity实例中的storage_name属性生成一个独一无二的字符串,同时为描述符类加上__get__方法
import uuid class Quantity: def __init__(self): # <1> cls = self.__class__ prefix = cls.__name__ identity = str(uuid.uuid4())[:8] self.storage_name = ‘_{}#{}‘.format(prefix, identity) def __get__(self, instance, owner): # <2> return getattr(instance, self.storage_name) def __set__(self, instance, value): # <3> if value > 0: setattr(instance, self.storage_name, value) else: raise ValueError(‘value must be > 0‘) class LineItem: weight = Quantity() price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
测试:
raisins = LineItem(‘Golden raisins‘, 10, 6.95) print(raisins.weight, raisins.description, raisins.price)
运行结果:
10 Golden raisins 6.95
这里还有一点,当我们尝试打印一下LineItem.weight这个描述符实例
LineItem.weight
运行结果:
Traceback (most recent call last): …… return getattr(instance, self.storage_name) AttributeError: ‘NoneType‘ object has no attribute ‘_Quantity#f9860e73‘
我们会发现,访问LineItem.weight会抛出AttributeError异常,因为在访问LineItem.weight属性时,同样会调用__get__方法,这个时候instance传入的是一个None,为了解决这个问题,我们在__get__方法中检测,如果传入的instance为None,则返回当前描述符实例,如果instance不为None,则返回instance中的实例属性
import uuid class Quantity: def __init__(self): cls = self.__class__ prefix = cls.__name__ identity = str(uuid.uuid4())[:8] self.storage_name = ‘_{}#{}‘.format(prefix, identity) def __get__(self, instance, owner): if instance is None: return self else: return getattr(instance, self.storage_name) def __set__(self, instance, value): if value > 0: setattr(instance, self.storage_name, value) else: raise ValueError(‘value must be > 0‘)
这里我们修改另外一个章节Python动态属性和特性(二)中的quantity()特性工厂方法,使之不需要传入storage_name
import uuid def quantity(): storage_name = ‘_{}:{}‘.format(‘quantity‘, str(uuid.uuid4())[:8]) def qty_getter(instance): return instance.__dict__[storage_name] def qty_setter(instance, value): if value > 0: instance.__dict__[storage_name] = value else: raise ValueError(‘value must be > 0‘) return property(qty_getter, qty_setter) class LineItem: weight = quantity() price = quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price raisins = LineItem(‘Golden raisins‘, 10, 6.95) print(raisins.weight, raisins.description, raisins.price)
运行结果:
10 Golden raisins 6.95
现在,我们对比一下描述符类和特性工厂,两种方法都可以在对属性设值或读取时进行一些额外的操作,哪种更好呢?这里建议使用描述符类的方式,主要有两个原因:
我们通过描述符类Quantity,在访问和设置LineItem托管实例的weight和price时进行额外的操作,现在,让我们更进一步,新增一个description描述符实例,对当要对LineItem实例的description属性进行设置和访问时,也增加一些操作。这里,我们要新增一个描述符类NotBlank,在设计NotBlank的过程中,我们发现它与Quantity描述符类很像,只是验证逻辑不同
回想Quantity的功能,我们注意到它做了两件不同的事,管理托管实例中的存储属性,以及验证用于设置那两个属性的值。由此可见,我们可以通过继承的方式,来复用描述符类,这里,我们创建两个基类:
稍后我们会重写Quantity类,并实现NotBlank类,使它继承Validated类,只编写validate方法,类之间的关系如图1-2:
图1-2
图1-2:几个描述符类的层次结构。AutoStorage基类负责自动存储属性;Validated类做验证,把职责委托给抽象方法validate;Quantity和NonBlank是Validated的具体子类。Validated、Quantity和NonBlank 三个类之间的关系体现了模板方法设计模式。
import abc import uuid class AutoStorage: # <1> def __init__(self): cls = self.__class__ prefix = cls.__name__ identity = str(uuid.uuid4())[:8] self.storage_name = ‘_{}#{}‘.format(prefix, identity) def __get__(self, instance, owner): if instance is None: return self else: return getattr(instance, self.storage_name) def __set__(self, instance, value): setattr(instance, self.storage_name, value) class Validated(abc.ABC, AutoStorage): # <2> def __set__(self, instance, value): # <3> value = self.validate(instance, value) super().__set__(instance, value) @abc.abstractmethod def validate(self, instance, value): # <4> """return validated value or raise ValueError""" class Quantity(Validated): """a number greater than zero""" def validate(self, instance, value): # <5> if value <= 0: raise ValueError(‘value must be > 0‘) return value class NotBlank(Validated): """a string with at least one non-space character""" def validate(self, instance, value): # <6> value = value.strip() if len(value) == 0: raise ValueError(‘value cannot be empty or blank‘) return value
使用Quantity和NonBlank描述符的LineItem类
class LineItem: description = NotBlank() weight = Quantity() price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
测试新的LineItem类
raisins = LineItem(‘ ‘, 10, 6.95)
运行结果:
Traceback (most recent call last): …… ValueError: value cannot be empty or blank
原文:https://www.cnblogs.com/beiluowuzheng/p/9245211.html