- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
19.3 特性全解析
虽然内置的 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 = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price def get_weight(self): ➊ return self.__weight def set_weight(self, value): ➋ if value > 0: self.__weight = value else: raise ValueError('value must be > 0') weight = property(get_weight, set_weight) ➌
❶ 普通的读值方法。
❷ 普通的设值方法。
❸ 构建 property 对象,然后赋值给公开的类属性。
某些情况下,这种经典形式比装饰器句法好;稍后讨论的特性工厂函数就是一例。但是,在方法众多的类定义体中使用装饰器的话,一眼就能看出哪些是读值方法,哪些是设值方法,而不用按照惯例,在方法名的前面加上 get 和 set。
类中的特性能影响实例属性的寻找方式,而一开始这种方式可能会让人觉得意外。下一节会详细说明。
19.3.1 特性会覆盖实例属性
特性都是类属性,但是特性管理的其实是实例属性的存取。
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 代码单元(模块、函数、类和方法)都可以有文档字符串。下一节说明如何把文档依附到特性上。
19.3.2 特性的文档
控制台中的 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: @property def bar(self): '''The bar attribute''' return self.__dict__['bar'] @bar.setter def bar(self, value): self.__dict__['bar'] = value
至此,我们介绍了特性的重要知识。下面回过头来解决前面遇到的问题:保护 LineItem 对象的 weight 和 price 属性,只允许设为大于零的值;但是,不用手动实现两对几乎一样的读值方法和设值方法。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论