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'
2
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
3
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
4
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
5
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
6
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
7
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
8
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
9
obj = Class() vars(obj)
0
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
4
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
5
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
2
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
3
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
4
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
5
1.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
{}
9
obj.data
0
obj.data
1
obj.data
2
obj.data
3
obj.data
4
obj.data
5
obj.data
6
obj.data
7
obj.data
8
1.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'
6
1.7. 特性工厂函数
我们的weight
和price
有相似的特点,都不能为负.如果一个类有很多这样的特性,那一个一个写特性会很麻烦,因此可以使用特性工厂函数来产生一样特点的特性.
我们将定义一个名为quantity
的特性工厂函数,取这个名字是因为,在这个应用中要管理的属性表示不能为负数或零的量.下例是LineItem
类的简洁版,用到了quantity
特性的两个实例:
- 一个用于管理
weight
属性, - 另一个用于管理
price
属性。
'the class data attr'
7
'the class data attr'
8
'the class data attr'
9
obj.data = 'bar' vars(obj)
0
obj.data = 'bar' vars(obj)
1
obj.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)
4
obj.data = 'bar' vars(obj)
5
obj.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)
7
obj.data = 'bar' vars(obj)
8
obj.data = 'bar' vars(obj)
9
{'data': 'bar'}
0
{'data': 'bar'}
1
{'data': 'bar'}
2
{'data': 'bar'}
1
1.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'}
8
obj.data = 'bar' vars(obj)
9
{'data': 'bar'}
0
{'data': 'bar'}
1
obj.data
2
obj.data
3
obj.data
4
obj.data
5
1.9. 覆盖型与非覆盖型描述符
Python存取属性的方式特别不对等.通过实例读取属性时,通常返回的是实例中定义的属性;但是如果实例中没有指定的属性,那么会获取类属性.而为实例中的属性赋值时,通常会在实例中创建属性,根本不影响类.这种不对等的处理方式对描述符也有影响.其实根据是否定义__set__
方法,描述符可分为两大类.其中覆盖型又可以分为2小类.
覆盖型
定义
__set__
,描述符的__set__
方法使用托管实例中的同名属性覆盖(即插手接管)了要设置的属性,这种类型描述符的典型用途是管理数据属性没有
__get__
方法的覆盖型描述符通常,覆盖型描述符既会实现
__set__
方法,也会实现__get__
方法,不过也可以只实现__set__
方法.此时,只有写操作由描述符处理.通过实例读取描述符会返回描述符对象本身,因为没有处理读操作的__get__
方法.如果直接通过实例的__dict__
属性创建同名实例属性,以后再设置那个属性时,仍会由__set__
方法插手接管,但是读取那个属性的话,就会直接从实例中返回新赋予的值,而不会返回描述符对象.也就是说实例属性会遮盖描述符,不过只有读操作是如此
非覆盖型
没有实现
__set__
方法的描述符是非覆盖型描述符.如果设置了同名的实例属性,描述符会被遮盖,致使描述符无法处理那个实例的那个属性.方法是以非覆盖型描述符实现的
我们通过下面的例子观察这两类描述符的行为差异
obj.data
6
obj.data
7
obj.data
8
obj.data
9
'bar'
0
1.9.1. 覆盖型描述符的行为
上面的例子都是覆盖型描述符
'bar'
1
'bar'
2
'bar'
3
'bar'
4
'bar'
5
'bar'
6
'bar'
7
'bar'
2
'bar'
3
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
00
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
9
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
02
'bar'
2
'bar'
3
1.9.2. 没有__get__的覆盖型描述符的行为
只有写操作由描述符处理.通过实例读取描述符会返回描述符对象本身
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
05
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
06
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
07
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
06
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
09
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
10
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
05
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
06
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
13
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
05
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
15
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
09
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
10
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
05
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
15
1.9.3. 非覆盖型描述符的行为
如果设置了同名的实例属性,描述符会被遮盖,致使描述符无法处理那个实例的那个属性
'bar'
1
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
21
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
22
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
23
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
21
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
25
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
26
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
27
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
28
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
21
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
22
1.10. 在类中覆盖描述符
依附在类上的描述符无法控制为类属性赋值的操作.其实,这意味着为类属性赋值能覆盖描述符属性.这是一种猴子补丁技术,不过在下例中,我们把描述符替换成了整数,这其实会导致依赖描述符的类不能正确地执行操作.
'bar'
1
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
32
class 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'}
8
obj.data
2
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
37
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
38
class Class: data = 'the class data attr' @property def prop(self): return 'the prop value'
39
1.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__
属性不会破坏常规的属性访问规则.
还没有评论,来说两句吧...