Python 动态属性和特性(特性全解析)
特性全解析
虽然内置的 property 经常用作装饰器,但它其实是一个类。在 Python
中,函数和类通常可以互换,因为二者都是可调用的对象,而且没有实
例化对象的 new 运算符,所以调用构造方法与调用工厂函数没有区别。
此外,只要能返回新的可调用对象,代替被装饰的函数,二者都可以用
作装饰器。
property 构造方法的完整签名如下:
property(fget=None, fset=None, fdel=None, doc=None)
所有参数都是可选的,如果没有把函数传给某个参数,那么得到的特性
对象就不允许执行相应的操作。
property 类型在 Python 2.2 中引入,但是直到 Python 2.4 才出现 @ 装饰
器句法,因此有那么几年,若想定义特性,则只能把存取函数传给前两
个参数。
不使用装饰器定义特性的“经典”句法如示例 19-18 所示。
示例 19-18 bulkfood_v2b.py:效果与示例 19-17 一样,只不过没
使用装饰器
class LineItem:def __init__(self, description, weight, price):self.description = descriptionself.weight = weightself.price = pricedef subtotal(self):return self.weight * self.pricedef get_weight(self): ➊return self.__weightdef set_weight(self, value): ➋if value > 0:self.__weight = valueelse:raise ValueError('value must be > 0')weight = property(get_weight, set_weight) ➌
❶ 普通的读值方法。
❷ 普通的设值方法。
❸ 构建 property 对象,然后赋值给公开的类属性。
某些情况下,这种经典形式比装饰器句法好;稍后讨论的特性工厂函数
就是一例。但是,在方法众多的类定义体中使用装饰器的话,一眼就能
看出哪些是读值方法,哪些是设值方法,而不用按照惯例,在方法名的
前面加上 get 和 set。
类中的特性能影响实例属性的寻找方式,而一开始这种方式可能会让人
觉得意外。下一节会详细说明。
特性会覆盖实例属性
特性都是类属性,但是特性管理的其实是实例属性的存取。
9.9 节说过,如果实例和所属的类有同名数据属性,那么实例属性会覆
盖(或称遮盖)类属性——至少通过那个实例读取属性时是这样。示例
19-19 阐明了这一点。
示例 19-19 实例属性遮盖类的数据属性
>>> 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.data # ➐
'the class data attr'
❶ 定义 Class 类,这个类有两个类属性:data 数据属性和 prop 特
性。
❷ vars 函数返回 obj 的 __dict__
属性,表明没有实例属性。
❸ 读取 obj.data,获取的是 Class.data 的值。
❹ 为 obj.data 赋值,创建一个实例属性。
❺ 审查实例,查看实例属性。
❻ 现在读取 obj.data,获取的是实例属性的值。从 obj 实例中读取属
性时,实例属性 data 会遮盖类属性 data。
❼ Class.data 属性的值完好无损。
下面尝试覆盖 obj 实例的 prop 特性。接着前面的控制台会话,输入示
例 19-20 中的代码。
示例 19-20 实例属性不会遮盖类特性(接续示例 19-19)
>>> Class.prop # ➊
<property object at 0x1072b7408>
>>> obj.prop # ➋
'the prop value'
>>> obj.prop = 'foo' # ➌
Traceback (most recent call last):
...
AttributeError: can't set attribute
>>> obj.__dict__['prop'] = 'foo' # ➍
>>> vars(obj) # ➎
{ 'data': 'bar','prop': 'foo'}
>>> obj.prop # ➏
'the prop value'
>>> Class.prop = 'baz' # ➐
>>> obj.prop # ➑
'foo'
❶ 直接从 Class 中读取 prop 特性,获取的是特性对象本身,不会运行
特性的读值方法。
❷ 读取 obj.prop 会执行特性的读值方法。
❸ 尝试设置 prop 实例属性,结果失败。
❹ 但是可以直接把 ‘prop’ 存入 obj.__dict__
。
❺ 可以看到,obj 现在有两个实例属性:data 和 prop。
❻ 然而,读取 obj.prop 时仍会运行特性的读值方法。特性没被实例属
性遮盖。
❼ 覆盖 Class.prop 特性,销毁特性对象。
❽ 现在,obj.prop 获取的是实例属性。Class.prop 不是特性了,因
此不会再覆盖 obj.prop。
最后再举一个例子,为 Class 类新添一个特性,覆盖实例属性。示例
19-21 接续示例 19-20。
示例 19-21 新添的类特性遮盖现有的实例属性(接续示例 19-
20)
>>> obj.data # ➊
'bar'
>>> Class.data # ➋
'the class data attr'
>>> Class.data = property(lambda self: 'the "data" prop value') # ➌
>>> obj.data # ➍
'the "data" prop value'
>>> del Class.data # ➎
>>> obj.data # ➏
'bar'
❶ obj.data 获取的是实例属性 data。
❷ Class.data 获取的是类属性 data。
❸ 使用新特性覆盖 Class.data。
❹ 现在,obj.data 被 Class.data 特性遮盖了。
❺ 删除特性。
❻ 现在恢复原样,obj.data 获取的是实例属性 data。
本节的主要观点是,obj.attr 这样的表达式不会从 obj 开始寻找
attr,而是从 obj.class 开始,而且,仅当类中没有名为 attr
的特性时,Python 才会在 obj 实例中寻找。这条规则不仅适用于特性,
还适用于一整类描述符——覆盖型描述符(overriding descriptor)。第
20 章会进一步讨论描述符,那时你会发现,特性其实是覆盖型描述
符。
现在回到特性。各种 Python 代码单元(模块、函数、类和方法)都可以
有文档字符串。下一节说明如何把文档依附到特性上。
特性的文档
控制台中的 help() 函数或 IDE 等工具需要显示特性的文档时,会从特
性的 __doc__
属性中提取信息。
如果使用经典调用句法,为 property 对象设置文档字符串的方法是传
入 doc 参数:
weight = property(get_weight, set_weight, doc='weight in kilograms')
使用装饰器创建 property 对象时,读值方法(有 @property 装饰器
的方法)的文档字符串作为一个整体,变成特性的文档。图 19-2 显示的是从示例 19-22 里的代码中生成的帮助界面。
图 19-2:在 Python 控制台中执行 help(Foo.bar) 和 help(Foo) 命
令时的截图;源码在示例 19-22 中
示例 19-22 特性的文档
class Foo:@propertydef bar(self):'''The bar attribute'''return self.__dict__['bar']@bar.setterdef bar(self, value):self.__dict__['bar'] = value
至此,我们介绍了特性的重要知识。下面回过头来解决前面遇到的问
题:保护 LineItem 对象的 weight 和 price 属性,只允许设为大于零
的值;但是,不用手动实现两对几乎一样的读值方法和设值方法。