属性处理和内置的内省函数的官方文档在 Python 标准库文档的第 2 章中,题为“Built-in Functions”(https://docs.python.org/3/library/functions.html)。相关的特殊方法和特殊的 __slots__ 属性在 Python 语言参考手册中的“3.3.2. Customizing attribute access”一节(https://docs.python.org/3/reference/datamodel.html#customizing-attribute-access)里说明。调用特殊方法会跳过实例的语意原因在“3.3.9. Special method lookup”一节(https://docs.python.org/3/reference/datamodel.html#special-method-lookup)中说明。在 Python 标准库文档的第 4 章“Built-in Types”里,“4.13. Special Attributes”一节(https://docs.python.org/3/library/stdtypes.html#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 的维基(http://c2.com/cgi/wiki?WelcomeVisitors)对软件工程方面的话题有很多独到的见解,他对统一访问原则的功过也做了富有洞察力的论述(http://c2.com/cgi/wiki?UniformAccessPrinciple)。在面向对象编程语言中,是否遵守统一访问原则通常体现在句法上:究竟是读取公开的数据属性,还是调用读值方法和设值方法。
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(http://www.pingo.io/docs/,3.4.2 节提过)中,多数用户级别的 API 都基于特性实现。例如,为了读取模拟针脚的当前值,用户要编写
pin.value;为了设置数字针脚的模式,要写成pin.mode = OUT。在背后,读取模拟针脚的值或设置数字针脚的模式可能涉及大量代码,这取决于具体的主板驱动。我们决定在 Pingo 中使用特性,是因为我们想让 API 用起来舒服,即便是在 iPython Notebook(http://ipython.org/notebook.html)等交互环境中也是如此,而且我们觉得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”(https://www.python.org/dev/peps/pep-0008/#class-names)不推荐类名使用驼峰式(
CamelCase),那么函数与类的对偶性更易于使用。不过,标准库中有很多类的名称是小写的(例如property、str、defaultdict,等等)。因此,使用小写的类名可能是个特色,而不是缺陷。但是,不管怎么看,Python 标准库在类名大小写上的不一致会导致可用性问题。虽然调用函数与调用类没有区别,但是最好知道哪个是哪个,因为类还有一个功能:子类化。因此,我编写的每个类都使用驼峰式名称,而且希望 Python 标准库中的所有类也使用这一约定。我在盯着你呢,
collections.OrderedDict和collections.defaultdict。
15包括没有名称的默认级别,Java 教程(http://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html)称其为“包级私有”。
16《Python 技术手册(第 2 版)》第 101 页。
17我将要提到的原因出自 Jonathan Amsterdam 发布在 Dr. Dobbs Journal 中的一篇文章,题为“Java's new Considered Harmful”(http://www.drdobbs.com/javas-new-considered-harmful/184405016),以及 Joshua Bloch 写的获奖图书 Effective Java 中的第一条,“考虑用静态工厂方法代替构造函数”。