""" #描述符实例是托管类的类属性;此外,托管类还有自己实例的同名属性 #20.1.1 LineItem类第三版:一个简单的描述符#栗子20-1 dulkfood_v3.py 使用 Quantity 描述符管理 LineItem 的属性class Quantity:# 描述符基于协议实现,无需创建子类。 def __init__(self,storage_name): self.storage_name = storage_name def __set__(self, instance, value): # instance是托管类实例,不用self是为了不和描述符实例冲突 if value > 0 : instance.__dict__[self.storage_name] = value #这里,必须直接处理托管实例的 __dict__ 属性;如果使用内置的 setattr 函数,会再次触发 __set__ 方法,导致无限递归。 else: raise ValueError(‘value must be > 0‘)class LineItem: weight = Quantity(‘weight‘) price = Quantity(‘price‘) def __init__(self,description,weight,price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price truffle = LineItem(‘White truffle‘,100,0) #ValueError: value must be > 0 #20.1.2 LineItem第四版:自动获取储存属性的名称#栗子20-2 bulkfood_v4.py:每个 Quantity 描述符都有独一无二的 storage_name‘‘‘为了生成 storage_name,我们以 ‘_Quantity#‘ 为前缀,然后在后面拼接一个整数:Quantity.__counter 类属性的当前值,每次把一个新的 Quantity 描述符实例依附到类上,都会递增这个值。在前缀中使用井号能避免 storage_name 与用户使用点号创建的属性冲突,因为 nutmeg._Quantity#0 是无效的 Python 句法。但是,内置的 getattr和 setattr 函数可以使用这种“无效的”标识符获取和设置属性,此外也可以直接处理实例属性 __dict__‘‘‘class Quantity: __counter = 0 def __init__(self): cls = self.__class__ prefix = cls.__name__ index = cls.__counter self.storage_name = ‘_{}#{}‘.format(prefix,index) # cls.__counter += 1 def __get__(self, instance, owner): return getattr(instance,self.storage_name) #这里可以使用内置的高阶函数 getattr 和 setattr 存取值,无需使用instance.__dict__,因为托管属性和储存属性的名称不同,所以把储存属性传给getattr 函数不会触发描述符,不会像示例 20-1 那样出现无限递归 def __set__(self, instance, value): 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 cocounts = LineItem(‘Brazilian cocount‘,20,17.95)print(getattr(cocounts,‘_Quantity#0‘),getattr(cocounts,‘_Quantity#1‘)) #20 17.95print(getattr(cocounts,‘weight‘),getattr(cocounts,‘price‘)) #20 17.95print(cocounts.weight,cocounts.price) #20 17.95#print(cocounts._Quantity#0) #SyntaxError: unexpected EOF while parsing‘‘‘, __get__ 方法有三个参数: self、 instance 和 owner。 owner 参数是托管类(如LineItem)的引用,通过描述符从托管类中获取属性时用得到。如果使用LineItem.weight 从类中获取托管属性(以 weight 为例),描述符的 __get__ 方法接本文档由Linux公社 www.linuxidc.com 整理收到的 instance 参数值是 None。因此,下述控制台会话才会抛出 AttributeError 异常抛出 AttributeError 异常是实现 __get__ 方法的方式之一,如果选择这么做,应该修改错误消息,去掉令人困惑的 NoneType 和 _Quantity#0,这是实现细节。把错误消息改成"‘LineItem‘ class has no such attribute" 更好。最好能给出缺少的属性名,但是在这个示例中,描述符不知道托管属性的名称,因此目前只能做到这样‘‘‘#print(LineItem.weight) #AttributeError: ‘NoneType‘ object has no attribute ‘_Quantity#0‘ #示例 20-3 bulkfood_v4b.py(只列出部分代码):通过托管类调用时, __get__ 方法返回描述符的引用class Quantity: __counter = 0 def __init__(self): cls = self.__class__ prefix = cls.__name__ index = cls.__counter self.storage_name = ‘_{}#{}‘.format(prefix,index) # cls.__counter += 1 def __get__(self, instance, owner): if instance is None: return self #如果不是通过实例调用,返回描述符自身 else: return getattr(instance, self.storage_name) #这里可以使用内置的高阶函数 getattr 和 setattr 存取值,无需使用instance.__dict__,因为托管属性和储存属性的名称不同,所以把储存属性传给getattr 函数不会触发描述符,不会像示例 20-1 那样出现无限递归 def __set__(self, instance, value): 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 print(LineItem.weight) #<__main__.Quantity object at 0x0000000001ECA710>br_nuts = LineItem(‘Brazil nuts‘,10,34.95)print(br_nuts.price) #34.95 ‘‘‘特性工厂函数与描述符类比较特性工厂函数若想实现示例 20-2 中增强的描述符类并不难,只需在示例 19-24 的基础上添加几行代码。 __counter 变量的实现方式是个难点,不过我们可以把它定义本文档由Linux公社 www.linuxidc.com 整理成工厂函数对象的属性,以便在多次调用之间持续存在,如示例 20-5 所示。示例 20-5 bulkfood_v4prop.py:使用特性工厂函数实现与示例 20-2 中的描述符类相同的功能def quantity(): ?try:quantity.counter += 1 ?except AttributeError:quantity.counter = 0 ?storage_name = ‘_{}:{}‘.format(‘quantity‘, quantity.counter) ?def qty_getter(instance): ?return getattr(instance, storage_name)def qty_setter(instance, value):if value > 0:setattr(instance, storage_name, value)else:raise ValueError(‘value must be > 0‘)return property(qty_getter, qty_setter)? 没有 storage_name 参数。? 不能依靠类属性在多次调用之间共享 counter,因此把它定义为 quantity 函数自身的属性。? 如果 quantity.counter 属性未定义,把值设为 0。? 我们也没有实例变量,因此创建一个局部变量 storage_name,借助闭包保持它的值,供后面的 qty_getter 和 qty_setter 函数使用。? 余下的代码与示例 19-24 一样,不过这里可以使用内置的 getattr 和 setattr 函数,而不用处理 instance.__dict__ 属性。那么,你喜欢哪个?示例 20-2 还是示例 20-5 ?我喜欢描述符类那种方式,主要有下列两个原因。描述符类可以使用子类扩展;若想重用工厂函数中的代码,除了复制粘贴,很难有其他方法。与示例 20-5 中使用函数属性和闭包保持状态相比,在类属性和实例属性中保持状态更易于理解。此外,解说示例 20-5 时,我没有画机器和小怪兽的动力。特性工厂函数的代码不依赖奇怪的对象关系,而描述符的方法中有名为 self 和 instance 的参数,表明里面涉及奇怪的对象关系。本文档由Linux公社 www.linuxidc.com 整理总之,从某种程度上来讲,特性工厂函数模式较简单,可是描述符类方式更易扩展,而且应用也更广泛。‘‘‘ #20.1.3 LineItem类第5版:一种新型描述符 [避免商品信息为空,导致无法下单]#几个描述符类的层次结构。 AutoStorage 基类负责自动存储属性; Validated 类做验证,把职责委托给抽象方法 validate; Quantity 和NonBlank 是 Validated 的具体子类 import abc class AutoStorage: __counter = 0 def __init__(self): cls = self.__class__ prefix = cls.__name__ index = cls.__counter self.storage_name = ‘_{}#{}‘.format(prefix,index) cls.__counter += 1 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): def __set__(self, instance, value): value = self.validate(instance,value) super().__set__(instance,value) @abc.abstractmethod def validate(self,instance,value): ‘‘‘return validated value or raise ValueError‘‘‘ class Quantity(Validated): ‘‘‘a number greater than zero‘‘‘ def validate(self,instance,value): if value <= 0: raise ValueError(‘value must be > 0‘) return value class NonBlank(Validated): ‘‘‘a string with at least one non-space character‘‘‘ def validate(self,instance,value): value = value.strip() if len(value) == 0: raise ValueError(‘value cannot be empty or blank‘) return value class LineItem: description = NonBlank() 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 #20.2 覆盖型与非覆盖型描述符对比‘‘‘#Python 贡献者和作者讨论这些概念时会使用不同的术语。覆盖型描述符也叫数据描述符或强制描述符。非覆盖型描述符也叫非数据描述符或遮盖型描述符#依附在类上的描述符无法控制为类属性赋值的操作。其实,这意味着为类属性赋值能覆盖描述符属性‘‘‘ ### 辅助函数,仅用于显示 ###def cls_name(obj_or_cls): cls = type(obj_or_cls) if cls is type: cls = obj_or_cls return cls.__name__.split(‘.‘)[-1] def display(obj): cls = type(obj) if cls is type: return ‘<class {}>‘.format(obj.__name__) elif cls in [type(None), int]: return repr(obj) else: return ‘<{} object>‘.format(cls_name(obj)) def print_args(name, *args): pseudo_args = ‘, ‘.join(display(x) for x in args) print(‘-> {}.__{}__({})‘.format(cls_name(args[0]), name, pseudo_args)) ### 对这个示例重要的类 ###class Overriding: ‘‘‘也称数据描述符或强制描述符‘‘‘ def __get__(self, instance, owner): print_args(‘get‘, self, instance, owner) def __set__(self, instance, value): print_args(‘set‘, self, instance, value) class OverridingNoGet: ‘‘‘没有``__get__``方法的覆盖型描述符‘‘‘ def __set__(self, instance, value): print_args(‘set‘, self, instance, value) class NonOverriding: ‘‘‘也称非数据描述符或遮盖型描述符‘‘‘ def __get__(self, instance, owner): print_args(‘get‘, self, instance, owner) class Managed: over = Overriding() over_no_get = OverridingNoGet() non_over = NonOverriding() def spam(self): print(‘-> Managed.spam({})‘.format(display(self)))#覆盖型描述符obj = Managed()print(obj.over) #-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)print(Managed.over) #-> Overriding.__get__(<Overriding object>, None, <class Managed>) 【解析】因为没有实例obj.over = 7 #-> Overriding.__set__(<Overriding object>, <Managed object>, 7)print(obj.over) #-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)obj.__dict__[‘over‘] = 8 #跳过描述符,直接通过 obj.__dict__ 属性设值,所以不打印任何内容print(vars(obj)) #{‘over‘: 8} 【解析】确认值在 obj.__dict__ 属性中,在 over 键名下print(obj.over) #-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>) 【解析】然而,即使是名为 over 的实例属性, Managed.over 描述符仍会覆盖读取 obj.over这个操作 #没有 __get__ 方法的覆盖型描述符print(obj.over_no_get) #<__main__.OverridingNoGet object at 0x000000000385F860> 【解析】这个覆盖型描述符没有 __get__ 方法,因此, obj.over_no_get 从类中获取描述符实例print(Managed.over_no_get) #<__main__.OverridingNoGet object at 0x0000000001EE3A58> 【解析】直接从托管类中读取描述符实例也是如此obj.over_no_get = 7 #-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)print(obj.over_no_get) #<__main__.OverridingNoGet object at 0x0000000002203A58> 【解析】因为 __set__ 方法没有修改属性,所以在此读取 obj.over_no_get 获取的仍是托管类中的描述符实例obj.__dict__[‘over_no_get‘] = 9print(obj.over_no_get) #9 【解析】现在, over_no_get 实例属性会遮盖描述符,但是只有读操作是如此obj.over_no_get = 7 #-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)print(obj.over_no_get) #9 【解析】但是读取时,只要有同名的实例属性,描述符就会被遮盖 # 非覆盖型描述符obj = Managed()print(obj.non_over) #-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)obj.non_over = 7print(obj.non_over) #7print(Managed.non_over) #-> NonOverriding.__get__(<NonOverriding object>, None, <class Managed>)del obj.non_overprint(obj.non_over) #-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>) #通过类可以覆盖任何描述符obj = Managed()Managed.over = 1Managed.over_no_get = 2Managed.non_over = 3print(Managed.over,Managed.over_no_get,Managed.non_over) #1 2 3 【解析】揭示了读写属性的另一种不对等:读类属性的操作可以由依附在托管类上定义有 __get__ 方法的描述符处理,但是写类属性的操作不会由依附在托管类上定义有__set__ 方法的描述符处理 #...若想控制设置类属性的操作,要把描述符依附在类的类上,即依附在元类上。默认情况下,对用户定义的类来说,其元类是 type,而我们不能为 type 添加属性。不过在第 21 章,我们会自己创建元类 #方法是描述符#方法是非覆盖性描述符obj = Managed()print(obj.spam) #<bound method Managed.spam of <__main__.Managed object at 0x000000000385F860>> 【解析】 obj.spam 获取的是绑定方法对象print(Managed.spam) #<function Managed.spam at 0x00000000038D7B70> 【解析】但是 Managed.spam 获取的是函数obj.spam = 7print(obj.spam) #7 【解析】 函数没有实现 __set__ 方法,因此是非覆盖型描述符‘‘‘obj.spam 和 Managed.spam 获取的是不同的对象。与描述符一样,通过托管类访问时,函数的 __get__ 方法会返回自身的引用。但是,通过实例访问时,函数的 __get__ 方法返回的是绑定方法对象:一种可调用的对象,里面包装着函数,并把托管实例(例如 obj)绑定给函数的第一个参数(即self),这与 functools.partial 函数的行为一致‘‘‘ #20.3 方法是描述符 import collectionsclass Text(collections.UserString): def __str__(self): return ‘Text({!r})‘.format(self.data) def reverse(self): return self[::-1] word = Text(‘forward‘)print(word) #Text(‘forward‘)print(word.reverse()) #Text(‘drawrof‘)print(Text.reverse(word))#Text(‘drawrof‘) 【解析】在类上调用方法相当于调用函数print(type(Text.reverse),type(word.reverse)) #<class ‘function‘> <class ‘method‘>print(list(map(Text.reverse,[‘repaid‘,(10,20,30),Text(‘stressed‘)]))) #[‘diaper‘, (30, 20, 10), ‘desserts‘]print(Text.reverse.__get__(word)) #<bound method Text.reverse of ‘forward‘> 【解析】 函数都是非覆盖型描述符。在函数上调用 __get__ 方法时传入实例,得到的是绑定到那个实例上的方法print(Text.reverse.__get__(None,word)) #<function Text.reverse at 0x0000000001E8CD08> 【解析】调用函数的 __get__ 方法时,如果 instance 参数的值是 None,那么得到的是函数本身。print(word.reverse) #<bound method Text.reverse of ‘forward‘>print(word.reverse.__self__) #Text(‘forward‘)print(word.reverse.__func__ is Text.reverse) #True‘‘‘绑定方法的 __func__ 属性是依附在托管类上那个原始函数的引用。绑定方法对象还有个 __call__ 方法,用于处理真正的调用过程。这个方法会调用__func__ 属性引用的原始函数,把函数的第一个参数设为绑定方法的 __self__ 属性。这就是形参 self 的隐式绑定方式。函数会变成绑定方法,这是 Python 语言底层使用描述符的最好例证。‘‘‘ #20.4 描述符用法建议‘‘‘下面根据刚刚论述的描述符特征给出一些实用的结论。使用特性以保持简单 内置的 property 类创建的其实是覆盖型描述符, __set__ 方法和 __get__ 方法都实现了,即便不定义设值方法也是如此。特性的 __set__ 方法默认抛出AttributeError: can‘t set attribute,因此创建只读属性最简单的方式是使用特性,这能避免下一条所述的问题。只读描述符必须有 __set__ 方法 如果使用描述符类实现只读属性,要记住, __get__ 和 __set__ 两个方法必须都定义,否则,实例的同名属性会遮盖描述符。只读属性的 __set__ 方法只需抛出AttributeError 异常,并提供合适的错误消息。Python 为此类异常提供的错误消息不一致。如果试图修改 complex 的 c.real 属性,那么得到的错误消息是AttributeError: read-only attribute;但是,如果试图修改 c.conjugat(e complex 对象的方法),那么得到的错误消息是 AttributeError: ‘complex‘ object attribute ‘conjugate‘ is read-only。用于验证的描述符可以只有 __set__ 方法 对仅用于验证的描述符来说, __set__ 方法应该检查 value 参数获得的值,如果有效,使用描述符实例的名称为键,直接在实例的 __dict__ 属性中设置。这样,从实例中读取同名属性的速度很快,因为不用经过 __get__ 方法处理。参见示例 20-1 中的代码。仅有 __get__ 方法的描述符可以实现高效缓存 如果只编写了 __get__ 方法,那么创建的是非覆盖型描述符。这种描述符可用于执行某些耗费资源的计算,然后为实例设置同名属性,缓存结果。同名实例属性会遮盖描述符,因此后续访问会直接从实例的 __dict__ 属性中获取值,而不会再触发描述符的__get__ 方法。非特殊的方法可以被实例属性遮盖 由于函数和方法只实现了 __get__ 方法,它们不会处理同名实例属性的赋值操作。因此,像 my_obj.the_method = 7 这样简单赋值之后,后续通过该实例访问the_method 得到的是数字 7——但是不影响类或其他实例。然而,特殊方法不受这个问题的影响。解释器只会在类中寻找特殊的方法,也就是说, repr(x) 执行的其实是x.__class__.__repr__(x),因此 x 的 __repr__ 属性对 repr(x) 方法调用没有影响。出于同样的原因,实例的 __getattr__ 属性不会破坏常规的属性访问规则。实例的非特殊方法可以被轻松地覆盖,这听起来不可靠且容易出错,可是在我使用 Python的 15 年中从未受此困扰。然而,如果要创建大量动态属性,属性名称从不受自己控制的数据中获取(像本章前面那样),那么你应该知道这种行为;或许你还可以实现某种机制,过滤或转义动态属性的名称,以维持数据的健全性。 示例 19-6 中的 FrozenJSON 类不会出现实例属性遮盖方法的问题,因为那个类只有几个特殊方法和一个 build 类方法。只要通过类访问,类方法就是安全的,在示例 19-6 中我就是这么调用 FrozenJSON.build 方法的——在示例 19-7 中替换成__new__ 方法了。 Record 类(见示例 19-9 和示例 19-11)及其子类也是安全的,因为只用到了特殊的方法、类方法、静态方法和特性。特性是数据描述符,因此不能被实例属性覆盖。讨论特性时讲了两个功能,这里讨论的描述符还未涉及,结束本章之前我们来讲讲:文档和对删除托管属性的处理‘‘‘ """
时间: 2024-10-08 13:04:35