1. 特性与描述符
除了属性之外,我们还可以创建特性(property),在不改变类接口的前提下,使用存取方法(即读值方法和设值方法)修改数据属性.这与统一访问原则相符--不管服务是由存储还是计算实现的,一个模块提供的所有服务都应该通过统一的方式使用.
property是一个用于类中方法的装饰器,用于将方法属性转换为特性,如果要设定特性的增删改查能力,则可以使用<property>.setter,<property>.deleter定义.
class Event(DbRecord):      @property     def venue(self):     '''The Event attribute'''         return self.__venue      @venue.setter     def venue(self,value):         self.__venue = value      @venue.deleter     def venue(self,value):         del self.__venue 虽然内置的property经常用作装饰器,但它其实是一个类.在Python中,函数和类通常可以互换,因为二者都是可调用的对象,而且没有实例化对象的new运算符,所以调用构造方法与调用工厂函数没有区别.此外,只要能返回新的可调用对象,代替被装饰的函数,二者都可以用作装饰器.
property构造方法的完整签名如下:
property(fget=None, fset=None, fdel=None, doc=None)
所有参数都是可选的,如果没有把函数传给某个参数,那么得到的特性对象就不允许执行相应的操作.
某些情况下,这种经典形式比装饰器句法好.但是在方法众多的类定义体中使用装饰器的话,一眼就能看出哪些是读值方法,哪些是设值方法,而不用按照惯例在方法名的前面加上get和set.类中的特性能影响实例属性的寻找方式,而一开始这种方式可能会让人觉得意外.
特性都是类属性,但是特性管理的其实是实例属性的存取.如果实例和所属的类有同名数据属性,那么实例属性会覆盖(或称遮盖)类属性--至少通过那个实例读取属性时是这样.
本节的先验知识有:
1.1. 实例属性遮盖类的数据属性
class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' obj = Class() vars(obj) {} obj.data 'the class data attr' obj.data = 'bar' vars(obj) {'data': 'bar'} obj.data 'bar' class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 0'the class data attr' 1.2. 实例属性不会遮盖类特性
class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 2class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 3class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 4class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 5class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 6class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 7class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 8class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 9obj = Class() vars(obj) 0class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 4class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 5class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 2class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 3class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 4class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 51.3. 新添的类特性遮盖现有的实例属性
obj.data 'bar' class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 0'the class data attr' {} 1{} 2{} 3'bar' 1.4. 特性的文档
控制台中的help()函数或IDE等工具需要显示特性的文档时,会从特性的__doc__属性中提取信息.
- 如果使用经典调用句法,为 - property对象设置文档字符串的方法是传入- doc参数:- weight = property(get_weight, set_weight, doc='weight in kilograms')
- 使用装饰器创建 - property对象时,读值方法(有- @property装饰器的方法)的文档字符串作为一个整体,变成特性的文档.
1.5. 使用特性获取链接的记录
- Record - __init__方法与schedule1.py 脚本(见示例19-9)中的一样;为了辅助测试,增加了- __eq__方法.
- DbRecord - Record类的子类,添加了- __db类属性,用于设置和获取- __db属性的- set_db和- get_db静态方法,用于从数据库中获取记录的fetch类方法,以及辅助调试和测试的- __repr__实例方法.
- Event - DbRecord类的子类,添加了用于获取所链接记录的- venue和- speakers属性,以及特殊的- __repr__方法.
{} 5{} 6{} 7{} 8{} 9obj.data 0obj.data 1obj.data 2obj.data 3obj.data 4obj.data 5obj.data 6obj.data 7obj.data 81.6. 使用特性验证属性
目前,我们只介绍了如何使用@property装饰器实现只读特性.本节要创建一个可读写的特性
1.6.1. LineItem类第1版:表示订单中商品的类
假设有个销售散装有机食物的电商应用,客户可以按重量订购坚果、干果或杂粮.在这个系统中,每个订单中都有一系列商品,而每个商品都可以使用.
obj.data 9'the class data attr' 0'the class data attr' 1'the class data attr' 2'the class data attr' 3'the class data attr' 1'the class data attr' 5这个类没法限制参数.比如作为一个商品订单,它的值可以是负的.
1.6.2. LineItem类第2版:能验证值的特性
'the class data attr' 61.7. 特性工厂函数
我们的weight 和price有相似的特点,都不能为负.如果一个类有很多这样的特性,那一个一个写特性会很麻烦,因此可以使用特性工厂函数来产生一样特点的特性.
我们将定义一个名为quantity的特性工厂函数,取这个名字是因为,在这个应用中要管理的属性表示不能为负数或零的量.下例是LineItem类的简洁版,用到了quantity特性的两个实例:
- 一个用于管理weight属性,
- 另一个用于管理price属性。
'the class data attr' 7'the class data attr' 8'the class data attr' 9obj.data = 'bar' vars(obj) 0obj.data = 'bar' vars(obj) 1obj.data = 'bar' vars(obj) 2工厂函数构建的特性利用了特性覆盖实例属性的行为,因此对self.weight或nutmeg.weight的每个引用都由特性函数处理,只有直接存取__dict__属性才能跳过特性的处理逻辑.
在真实的系统中,分散在多个类中的多个字段可能要做同样的验证,此时最好把quantity工厂函数放在实用工具模块中,以便重复使用.最终可能要重构那个简单的工厂函数,改成更易扩展的描述符类,然后使用专门的子类执行不同的验证.
1.8. 属性描述符
描述符是对多个属性运用相同存取逻辑的一种方式,ORM 中的字段类型是往往使用描述符,把数据库记录中字段里的数据与Python对象的属性对应起来.
描述符是实现了特定协议的类,这个协议包括__get__、__set__ 和__delete__方法.
property类实现了完整的描述符协议.通常可以只实现部分协议.其实我们在真实的代码中见到的大多数描述符只实现了__get__ 和__set__方法,还有很多只实现了其中的一个.描述符是Python的独有特征,不仅在应用层中使用,在语言的基础设施中也有用到.除了特性之外,使用描述符的Python功能还有方法及classmethod和staticmethod装饰器.理解描述符是精通Python的关键.
1.8.1. LineItem类第3版:一个简单的描述符
实现了__get__、__set__ 或__delete__方法的类是描述符.描述符的用法是,创建一个实例,作为另一个类的类属性.
我们将定义一个Quantity描述符用来代替特性工厂函数,LineItem类会用到两个Quantity实例:
- 一个用于管理weight属性
- 另一个用于管理price属性
从现在开始我会使用下述定义:
- 描述符类 - 实现描述符协议的类.在上图中,是 - Quantity类.
- 托管类 - 把描述符实例声明为类属性的类——上图中的 - LineItem类
- 描述符实例 - 描述符类的各个实例,声明为托管类的类属性.在上图中,各个描述符实例使用箭头和带下划线的名称表示(在UML中下划线表示类属性).与黑色菱形接触的 - LineItem类包含描述符实例.
- 托管实例 - 托管类的实例.在这个示例中, - LineItem实例是托管实例
- 储存属性 - 托管实例中存储自身托管属性的属性.在上图中, - LineItem实例的- weight和- price属性是储存属性.这种属性与描述符属性不同,描述符属性都是类属性.
- 托管属性 - 托管类中由描述符实例处理的公开属性,值存储在储存属性中.也就是说描述符实例和储存属性为托管属性建立了基础. 
obj.data = 'bar' vars(obj) 3各个托管属性的名称与储存属性一样,而且读值方法不需要特殊的逻辑,所以Quantity类不需要定义__get__方法.
编写__set__方法时,要记住self 和instance参数的意思:
- self 是描述符实例,
- instance 是托管实例
管理实例属性的描述符应该把值存储在托管实例中.因此Python才为描述符中的那个方法提供了instance参数.
obj.data = 'bar' vars(obj) 4obj.data = 'bar' vars(obj) 5obj.data = 'bar' vars(obj) 6上面的方式还是不够简洁,我们不得不在申明LineItem时为每个属性指定Quantity()的参数--属性的名称.
可问题是赋值语句右手边的表达式先执行,而此时变量还不存在.
Quantity()表达式计算的结果是创建描述符实例,而此时Quantity类中的代码无法猜出要把描述符绑定给哪个变量(例如weight或price).
因此上例必须明确指明各个Quantity实例的名称.这样不仅麻烦,还很危险--如果程序员直接复制粘贴代码而忘了编辑名称,比如写成price = Quantity('weight'),那么程序的行为会大错特错,设置price的值时会覆盖weight的值.
1.8.2. LineItem类第4版:自动获取储存属性的名称
为了避免在描述符声明语句中重复输入属性名,我们将为每个Quantity实例的storage_name属性生成一个独一无二的字符串.下图是更新后的Quantity和LineItem类的UML类图.
为了生成storage_name,我们以'_Quantity#'为前缀,然后在后面拼接一个整数:
Quantity.__counter类属性的当前值,每次把一个新的Quantity描述符实例依附到类上,都会递增这个值.在前缀中使用井号能避免storage_name与用户使用点号创建的属性冲突,因为nutmeg._Quantity#0是无效的Python句法.但是,内置的getattr 和setattr函数可以使用这种"无效的"标识符获取和设置属性,此外也可以直接处理实例属性__dict__
obj.data = 'bar' vars(obj) 7obj.data = 'bar' vars(obj) 8obj.data = 'bar' vars(obj) 9{'data': 'bar'} 0{'data': 'bar'} 1{'data': 'bar'} 2{'data': 'bar'} 11.8.3. LineItem类第5版:一种新型描述符
我们虚构的有机食物网店遇到一个问题:
不知怎么回事儿有个商品的描述信息为空,导致无法下订单.
为了避免出现这个问题,我们要再创建一个描述符NonBlank.在设计NonBlank的过程中,我们发现它与Quantity描述符很像,只是验证逻辑不同.
回想Quantity的功能,我们注意到它做了两件不同的事:
- 管理托管实例中的储存属性
- 验证用于设置那两个属性的值
由此可知,我们可以重构,并创建两个基类
- AutoStorage - 自动管理储存属性的描述符类 
- Validated - 扩展AutoStorage类的抽象子类,覆盖 - __set__方法,调用必须由子类实现的- validate方法
我们重写Quantity类,并实现NonBlank,让它继承Validated类,只编写validate方法.类之间的关系见图.
Validated、Quantity和NonBlank 三个类之间的关系体现了模板方法设计模式.具体而言,Validated.__set__ 方法正是Gamma等四人所描述的模板方法的例证--一个模板方法用一些抽象的操作定义一个算法,而子类将重定义这些操作以提供具体的行为.
{'data': 'bar'} 4{'data': 'bar'} 5{'data': 'bar'} 6{'data': 'bar'} 7{'data': 'bar'} 8obj.data = 'bar' vars(obj) 9{'data': 'bar'} 0{'data': 'bar'} 1obj.data 2obj.data 3obj.data 4obj.data 51.9. 覆盖型与非覆盖型描述符
Python存取属性的方式特别不对等.通过实例读取属性时,通常返回的是实例中定义的属性;但是如果实例中没有指定的属性,那么会获取类属性.而为实例中的属性赋值时,通常会在实例中创建属性,根本不影响类.这种不对等的处理方式对描述符也有影响.其实根据是否定义__set__方法,描述符可分为两大类.其中覆盖型又可以分为2小类.
- 覆盖型 - 定义 - __set__,描述符的- __set__方法使用托管实例中的同名属性覆盖(即插手接管)了要设置的属性,这种类型描述符的典型用途是管理数据属性- 没有 - __get__方法的覆盖型描述符- 通常,覆盖型描述符既会实现 - __set__方法,也会实现- __get__方法,不过也可以只实现- __set__方法.此时,只有写操作由描述符处理.通过实例读取描述符会返回描述符对象本身,因为没有处理读操作的- __get__方法.如果直接通过实例的- __dict__属性创建同名实例属性,以后再设置那个属性时,仍会由- __set__方法插手接管,但是读取那个属性的话,就会直接从实例中返回新赋予的值,而不会返回描述符对象.也就是说实例属性会遮盖描述符,不过只有读操作是如此
 
- 非覆盖型 - 没有实现 - __set__方法的描述符是非覆盖型描述符.如果设置了同名的实例属性,描述符会被遮盖,致使描述符无法处理那个实例的那个属性.方法是以非覆盖型描述符实现的
我们通过下面的例子观察这两类描述符的行为差异
obj.data 6obj.data 7obj.data 8obj.data 9'bar' 01.9.1. 覆盖型描述符的行为
上面的例子都是覆盖型描述符
'bar' 1'bar' 2'bar' 3'bar' 4'bar' 5'bar' 6'bar' 7'bar' 2'bar' 3class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 00class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 9class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 02'bar' 2'bar' 31.9.2. 没有__get__的覆盖型描述符的行为
只有写操作由描述符处理.通过实例读取描述符会返回描述符对象本身
class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 05class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 06class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 07class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 06class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 09class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 10class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 05class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 06class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 13class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 05class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 15class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 09class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 10class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 05class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 151.9.3. 非覆盖型描述符的行为
如果设置了同名的实例属性,描述符会被遮盖,致使描述符无法处理那个实例的那个属性
'bar' 1class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 21class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 22class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 23class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 21class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 25class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 26class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 27class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 28class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 21class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 221.10. 在类中覆盖描述符
依附在类上的描述符无法控制为类属性赋值的操作.其实,这意味着为类属性赋值能覆盖描述符属性.这是一种猴子补丁技术,不过在下例中,我们把描述符替换成了整数,这其实会导致依赖描述符的类不能正确地执行操作.
'bar' 1class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 32class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 33读类属性的操作可以由依附在托管类上定义有__get__ 方法的描述符处理,但是写类属性的操作不会由依附在托管类上定义有__set__方法的描述符处理.
若想控制设置类属性的操作,要把描述符依附在类的类上,即依附在元类上.默认情况下,对用户定义的类来说,其元类是type,而我们不能为type 添加属性,但我们可以自定义元类.
1.11. 描述符协议增强[3.6]
上面的LineItem有个缺陷--就是初始化的时候都明确让属性的值绑定在Integer上的name属性上,而无法获知所有者类的属性名.如果使用自定义内部名字,又会难以调试.使用在PEP487上提供的可选的__set_name__()可以获得这个属性名字,并且可以自定义这部分内容:
class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 34{'data': 'bar'} 8obj.data 2class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 37class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 38class Class:     data = 'the class data attr'     @property     def prop(self):         return 'the prop value' 391.12. 方法是描述符
在类中定义的函数属于绑定方法(bound method),因为用户定义的函数都有__get__方法,所以依附到类上时,就相当于描述符.函数没有实现__set__方法,因此是非覆盖型描述符.
与描述符一样,通过托管类访问时,函数的__get__方法会返回自身的引用.但是通过实例访问时,函数的__get__方法返回的是绑定方法对象--一种可调用的对象,里面包装着函数,并把托管实例(例如obj)绑定给函数的第一个参数(即self),这与functools.partial函数的行为一致
1.13. 描述符用法建议
下面根据刚刚论述的描述符特征给出一些实用的结论:
- 使用特性以保持简单 - 内置的property 类创建的其实是覆盖型描述符, - __set__方法和- __get__方法都实现了,即便不定义设值方法也是如此.特性的- __set__方法默认抛出- AttributeError:can't set attribute,因此创建只读属性最简单的方式是使用特性,这能避免下一条所述的问题.
- 只读描述符必须有 - __set__方法- 如果使用描述符类实现只读属性,要记住 - __get__和- __set__两个方法必须都定义,否则实例的同名属性会遮盖描述符.只读属性的- __set__方法只需抛出- AttributeError异常,并提供合适的错误消息.
- 用于验证的描述符可以只有 - __set__方法- 对仅用于验证的描述符来说, - __set__方法应该检查value参数获得的值,如果有效,使用描述符实例的名称为键,直接在实例的- __dict__属性中设置.这样从实例中读取同名属性的速度很快,因为不用经过- __get__方法处理.
- 仅有 - __get__方法的描述符可以实现高效缓存- 如果只编写了 - __get__方法,那么创建的是非覆盖型描述符.这种描述符可用于执行某些耗费资源的计算,然后为实例设置同名属性,缓存结果.同名实例属性会遮盖描述符,因此后续访问会直接从实例的- __dict__属性中获取值,而不会再触发描述符的- __get__方法.
- 非特殊的方法可以被实例属性遮盖 - 由于函数和方法只实现了 - __get__方法,它们不会处理同名实例属性的赋值操作.因此,像- my_obj.the_method = 7这样简单赋值之后,后续通过该实例访问- the_method得到的是数字7——但是不影响类或其他实例.然而,特殊方法不受这个问题的影响.解释器只会在类中寻找特殊的方法,也就是说- repr(x)执行的其实是- x.__class__.__repr__(x),因此x的- __repr__属性对- repr(x)方法调用没有影响.出于同样的原因,实例的- __getattr__属性不会破坏常规的属性访问规则.



 
		 
		

还没有评论,来说两句吧...