- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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.4 定义一个特性工厂函数
我们将定义一个名为 quantity 的特性工厂函数,取这个名字是因为,在这个应用中要管理的属性表示不能为负数或零的量。示例 19-23 是 LineItem 类的简洁版,用到了 quantity 特性的两个实例:一个用于管理 weight 属性,另一个用于管理 price 属性。
示例 19-23 bulkfood_v2prop.py:使用特性工厂函数 quantity
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 ➍
❶ 使用工厂函数把第一个自定义的特性 weight 定义为类属性。
❷ 第二次调用,构建另一个自定义的特性,price。
❸ 这里,特性已经激活,确保不能把 weight 设为负数或零。
❹ 这里也用到了特性,使用特性获取实例中存储的值。
前文说过,特性是类属性。构建各个 quantity 特性对象时,要传入 LineItem 实例属性的名称,让特性管理。可惜,这一行要两次输入单词 weight:
weight = quantity('weight')
这里很难避免重复输入,因为特性根本不知道要绑定哪个类属性名。记住,赋值语句的右边先计算,因此调用 quantity() 时,weight 类属性还不存在。
如果想改进 quantity 特性,避免用户重复输入属性名,那么对元编程来说是个挑战。第 20 章会介绍一种变通方法,真正的解决方法在第 21 章说明,因为要么得使用类装饰器,要么得使用元类。
示例 19-24 列出 quantity 特性工厂函数的实现。13
13这段代码改编自 David Beazley 与 Brian K. Jones 的《Python Cookbook(第 3 版)中文版》一书中的“9.21 避免出现重复的属性方法”一节。
示例 19-24 bulkfood_v2prop.py:quantity 特性工厂函数
def quantity(storage_name): ➊ def qty_getter(instance): ➋ return instance.__dict__[storage_name] ➌ def qty_setter(instance, value): ➍ if value > 0: instance.__dict__[storage_name] = value ➎ else: raise ValueError('value must be > 0') return property(qty_getter, qty_setter) ➏
❶ storage_name 参数确定各个特性的数据存储在哪儿;对 weight 特性来说,存储的名称是 'weight'。
❷ qty_getter 函数的第一个参数可以命名为 self,但是这么做很奇怪,因为 qty_getter 函数不在类定义体中;instance 指代要把属性存储其中的 LineItem 实例。
❸ qty_getter 引用了 storage_name,把它保存在这个函数的闭包里;值直接从 instance.__dict__ 中获取,为的是跳过特性,防止无限递归。
❹ 定义 qty_setter 函数,第一个参数也是 instance。
❺ 值直接存到 instance.__dict__ 中,这也是为了跳过特性。
❻ 构建一个自定义的特性对象,然后将其返回。
示例 19-24 中值得仔细分析的代码是与 storage_name 变量相关的部分。使用传统方式定义特性时,用于存储值的属性名硬编码在读值方法和设值方法中。但是,这里的 qty_getter 和 qty_setter 函数是通用的,要依靠 storage_name 变量判断从 __dict__ 中获取哪个属性,或者设置哪个属性。每次调用 quantity 工厂函数构建属性时,都要把 storage_name 参数设为独一无二的值。
在工厂函数的最后一行,我们使用 property 对象包装 qty_getter 和 qty_setter 函数。需要运行这两个函数时,它们会从闭包中读取 storage_name,确定从哪里获取属性的值,或者在哪里存储属性的值。
在示例 19-25 中,我创建并审查了一个 LineItem 示例,说明存储值的是哪个属性。
示例 19-25 bulkfood_v2prop.py:quantity 特性工厂函数
>>> nutmeg = LineItem('Moluccan nutmeg', 8, 13.95) >>> nutmeg.weight, nutmeg.price ➊ (8, 13.95) >>> sorted(vars(nutmeg).items()) ➋ [('description', 'Moluccan nutmeg'), ('price', 13.95), ('weight', 8)]
➊ 通过特性读取 weight 和 price,这会遮盖同名实例属性。
➋ 使用 vars 函数审查 nutmeg 实例,查看真正用于存储值的实例属性。
注意,工厂函数构建的特性利用了 19.3.1 节所述的行为:weight 特性覆盖了 weight 实例属性,因此对 self.weight 或 nutmeg.weight 的每个引用都由特性函数处理,只有直接存取 __dict__ 属性才能跳过特性的处理逻辑。
示例 19-25 中的代码有点难理解,不过够简洁,与示例 19-17 中使用装饰器声明读值方法和设值方法的代码行数一样,但是那里只定义了 weight 特性。示例 19-23 中定义的 LineItem 类没有干扰人的读值方法和设值方法,看起来舒服多了。
在真实的系统中,分散在多个类中的多个字段可能要做同样的验证,此时最好把 quantity 工厂函数放在实用工具模块中,以便重复使用。最终可能要重构那个简单的工厂函数,改成更易扩展的描述符类,然后使用专门的子类执行不同的验证。在第 20 章中,我们会这么做。
下面要分析删除属性的问题,以此结束对特性的讨论。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论