- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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.8 延伸阅读
属性处理和内置的内省函数的官方文档在 Python 标准库文档的第 2 章中,题为“Built-in Functions”。相关的特殊方法和特殊的 __slots__ 属性在 Python 语言参考手册中的“3.3.2. Customizing attribute access”一节里说明。调用特殊方法会跳过实例的语意原因在“3.3.9. Special method lookup”一节中说明。在 Python 标准库文档的第 4 章“Built-in Types”里,“4.13. Special Attributes”一节说明了 __class__ 和 __dict__ 属性。
David Beazley 与 Brian K. Jones 的《Python Cookbook(第 3 版)中文版》一书中有几个诀窍涉及本章的话题,不过我要重点提出三个:“8.8 在子类中扩展属性”,解决了在继承自超类的特性中覆盖方法这个棘手问题;“8.15 委托属性的访问”,实现了一个代理类,展示了本书 19.6.3 节所列的大多数特殊方法;还有出色的“9.21 避免出现重复的属性方法”一节,示例 19-24 中定义的特性工厂函数就以那一节为基础。
Alex Martelli 写的《Python 技术手册(第 2 版)》只涵盖了 Python 2.5,不过基础知识也适用于 Python 3。他写书的风格严谨而客观,讲到特性时,只用了 3 页,但这是由于那本书采用了符合逻辑的行文方式:之前的 15 页已经对 Python 的类做了详尽的说明,包括描述符,而特性就是使用描述符实现的。因此讲到特性时,他可以在 3 页的篇幅中发表很多见解,例如本章开篇引用的那句话。
本章开头引用的统一访问原则定义出自 Bertrand Meyer 的优秀著作 Object-Oriented Software Construction, Second Edition(Prentice-Hall 出版社)。这本书超过 1250 页,我承认我没有读完,不过前六章对面向对象分析和设计相关概念的介绍是我见过最好的之一,第 11 章介绍了契约式设计(Meyer 发明了这种设计方法,创造了这个术语),第 35 章阐述了他对重要的面向对象语言的评价,包括 Simula、Smalltalk、CLOS(Lisp 的面向对象扩展)、 Objective-C、C++ 和 Java,还对其他语言做了简要评述。他还发明了伪伪代码(pseudo- pseudocode),直到那本书的最后一页他才披露,全书用于编写伪代码的句法其实出自 Eiffel 语言。
杂谈
站在美学的角度来看,Meyer 提出的统一访问原则(Unifrom Access Principle,喜欢简称的人有时称之为UAP)很吸引人。作为使用 API 的程序员,我不应该关心 coconut.price 只是获取数据属性还是执行计算。但是,作为消费者和公民,我应该关心:在电子商务发达的今天,coconut.price 的值通常取决于这个问题由谁提出,因此它绝不仅仅是个数据属性。其实,如果查询来自网店外部(例如比价引擎),价格通常会低一些。显然,这对喜欢浏览特定网店的忠实消费者来说,利益受到了损害。但是我不同意。
前一段离题了,可是却提出了与编程有关的问题:虽然统一访问原则在理想的世界中完全合理,但在现实中,API 的用户可能需要知道读取 coconut.price 是否太耗资源或时间。Ward Cunningham 的维基对软件工程方面的话题有很多独到的见解,他对统一访问原则的功过也做了富有洞察力的论述。
在面向对象编程语言中,是否遵守统一访问原则通常体现在句法上:究竟是读取公开的数据属性,还是调用读值方法和设值方法。
Smalltalk 和 Ruby 使用简单而优雅的方式解决这个问题:根本不支持公开的数据属性。在这两门语言中,所有实例属性都是私有的,因此必须通过方法来存取。不过,这两门语言的句法把这个过程变得毫不费力:在 Ruby 中,coconut.price 会调用读值方法 price;在 Smalltalk 中,只需使用 coconut price。
Java 采用的是另一种方式,让程序员在四种访问级别修饰符中选择。15 不过,普通大众并不认同 Java 设计者制定的这种句法。Java 世界的人都认为,属性应该是私有的,但是每一次都要写出 private,因为这不是默认的访问级别。如果所有属性都是私有的,那么从类外部访问属性就必须使用存取方法。Java IDE 提供了自动生成存取方法的快捷方式。但是,六个月后不得不阅读代码时,IDE 没有多大帮助。我们要在众多什么也没做的存取方法中找出所需的那一个,添加实现某些业务逻辑所需的值。
Alex Martelli 把存取方法称为“愚蠢的惯用法”,这道出了 Python 社区中大多数人的心声。他举了下面两个例子,外观差异很大,但是作用相同:16
someInstance.widgetCounter += 1 # 而不用…… someInstance.setWidgetCounter(someInstance.getWidgetCounter() + 1)
设计 API 时,我有时会想,能否把没有参数(除了 self)、返回一个值(除了 None)的纯函数(即没有副作用)替换成只读特性。在本章中,LineItem.subtotal 方法(如示例 19-23 所示)就可以替换成只读特性。当然,用于修改对象的方法(如 my_list.clear())不在此列。把这样的方法变成特性是个糟糕的想法,因为直接访问 my_list.clear 就会删除列表中的内容。
在 GPIO 库 Pingo.io(3.4.2 节提过)中,多数用户级别的 API 都基于特性实现。例如,为了读取模拟针脚的当前值,用户要编写 pin.value;为了设置数字针脚的模式,要写成 pin.mode = OUT。在背后,读取模拟针脚的值或设置数字针脚的模式可能涉及大量代码,这取决于具体的主板驱动。我们决定在 Pingo 中使用特性,是因为我们想让 API 用起来舒服,即便是在 iPython Notebook 等交互环境中也是如此,而且我们觉得 pin.mode = OUT 看起来和输入起来都比 pin.set_mode(OUT) 容易。
我觉得 Smalltalk 和 Ruby 的处理方式很简洁,但也认为 Python 的处理方式比 Java 更合理。一开始,我们可以从简单的方式入手,把数据成员定义为公开的属性,因为我们知道这些属性可以使用特性(或下一章讨论的描述符)来包装。
__new__ 方法比 new 运算符好
在 Python 中还有一处体现了统一访问原则(或者它的变体):函数调用和对象实例化使用相同的句法——my_obj = foo(),其中 foo 是类或其他可调用的对象。
受 C++ 句法影响的其他语言提供了 new 运算符,致使实例化不像是调用。大多数时候,API 的用户不关心 foo 是函数还是类。直到最近,我才意识到,property 是个函数。在常规的用法中,这没什么区别。
把构造方法替换成工厂方法有很多充足的理由。17 一个重要的原因是,通过返回之前构建的实例,限制实例的数量(体现了单例模式)。有个相关的功能是,缓存构建过程开销大的对象。此外,有时便于根据指定的参数返回不同类型的对象。
定义构造方法较为简单;提供工厂方法虽然增加了灵活性,但是要编写更多的代码。在有 new 运算符的语言中,API 的设计者必须提前决定:究竟是坚持使用简单的构造方法,还是投入工厂方法的怀抱。如果一开始选择错了,那么修正的代价可能很大——这一切都因为 new 是运算符。
有时可能更适合走另一条路,把简单的函数换成类。
在 Python 中,很多情况下类和函数可以互换。这不仅是因为 Python 没有 new 运算符,还因为有特殊的 __new__ 方法,可以把类变成工厂方法,生成不同类型的对象(如 19.1.3 节所述),或者返回事先构建好的实例,而不是每次都创建一个新实例。
如果“PEP 8—Style Guide for Python Code”不推荐类名使用驼峰式(CamelCase),那么函数与类的对偶性更易于使用。不过,标准库中有很多类的名称是小写的(例如 property、str、defaultdict,等等)。因此,使用小写的类名可能是个特色,而不是缺陷。但是,不管怎么看,Python 标准库在类名大小写上的不一致会导致可用性问题。
虽然调用函数与调用类没有区别,但是最好知道哪个是哪个,因为类还有一个功能:子类化。因此,我编写的每个类都使用驼峰式名称,而且希望 Python 标准库中的所有类也使用这一约定。我在盯着你呢,collections.OrderedDict 和 collections.defaultdict。
15包括没有名称的默认级别,Java 教程称其为“包级私有”。
16《Python 技术手册(第 2 版)》第 101 页。
17我将要提到的原因出自 Jonathan Amsterdam 发布在 Dr. Dobbs Journal 中的一篇文章,题为“Java's new Considered Harmful”,以及 Joshua Bloch 写的获奖图书 Effective Java 中的第一条,“考虑用静态工厂方法代替构造函数”。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论