Vector 类中的大多数特殊方法在第 9 章定义的 Vector2d 类中也有,因此前一章给出的延伸阅读材料同样适合本章。
强大的高阶函数 reduce 也叫合拢、累计、聚合、压缩和注入。更多信息参见维基百科中的“Fold (higher-order function)”词条(https://en.wikipedia.org/wiki/Fold_(higher-order_function))。这篇文章展示了高阶函数的用途,着重说明了具有递归数据结构的函数式语言。这篇文章中还有一个表格,列出了很多编程语言中起合拢作用的函数。
杂谈
把协议当作非正式的接口
协议不是 Python 发明的。Smalltalk 团队,也就是“面向对象”的发明者,使用“协议”这个词表示现在我们称之为接口的特性。某些 Smalltalk 编程环境允许程序员把一组方法标记为协议,但这只不过是一种文档,用于辅助导航,语言不对其施加特定措施。因此,向熟悉正式(而且编译器会施加措施)接口的人解释“协议”时,我会简单地说它是“非正式的接口”。
动态类型语言中的既定协议会自然进化。所谓动态类型是指在运行时检查类型,因为方法签名和变量没有静态类型信息。Ruby 是一门重要的面向对象动态类型语言,它也使用协议。
在 Python 文档中,如果看到“文件类对象”这样的表述,通常说的就是协议。这是一种简短的说法,意思是:“行为基本与文件一致,实现了部分文件接口,满足上下文相关需求的东西。”
你可能觉得只实现协议的一部分不够严谨,但是这样做的优点是简单。“Data Model”一章的 3.3 节(https://docs.python.org/3/reference/datamodel.html#special-method-names)建议:
模仿内置类型实现类时,记住一点:模仿的程度对建模的对象来说合理即可。例如,有些序列可能只需要获取单个元素,而不必提取切片。
——Python 语言参考手册中“Data Model”一章
不要为了满足过度设计的接口契约和让编译器开心,而去实现不需要的方法,我们要遵守 KISS 原则(http://en.wikipedia.org/wiki/KISS_principle)。
第 11 章还会讨论协议和接口,这正是那一章的主要话题。
鸭子类型的起源
我相信 Ruby 社区在“鸭子类型”这个术语的推广过程中起了主要作用,因为他们向大量 Java 使用者宣扬了这个说法。但是,在 Ruby 或 Python 流行起来之前,Python 就使用这个术语了。根据维基百科,在面向对象编程中较早使用鸭子作比喻的人是 Alex Martelli,在他于 2000 年 7 月 26 日发到 Python-list 中的一篇文章里:“polymorphism (was Re: Type checking in python?)”(https://mail.python.org/pipermail/python-list/2000-July/046184.html)。本章开头引用的那句话就出自那篇文章。如果你想知道“鸭子类型”这个术语的真正起源,以及很多编程语言对这个面向对象概念的运用,请阅读维基百科中的“Duck typing”词条(http://en.wikipedia.org/wiki/Duck_typing)。
安全的
__format__方法,增强可用性实现
__format__方法时,我们没有采取措施防范Vector实例拥有大量分量,不过在__repr__方法中我们使用reprlib做了预防。这是因为repr()函数用于调试和记录日志,所以必须生成可用的输出;而__format__方法用于向最终用户显示输出,他们大概想看到整个Vector。如果你觉得这样做危险,可以再为格式规范微语言实现一个扩展。如果是我,我会这么做:默认情况下,格式化的
Vector实例显示有限个分量,比如说 30 个。如果元素数量超过上限,默认的行为是像reprlib那样,截断超出的部分,使用...表示。然而,如果格式说明符后面有特殊的*代码(意思是“全部”),那么就不限制显示的元素数量。因此,用户在不知情的情况下不会被特别长的输出吓到。如果默认的上限碍事,那么...的存在对用户是个提醒,用户研究文档后会发现*格式代码。如果你实现了,请向本书的 GitHub 仓库(https://github.com/fluentpython/example-code)发一个拉取请求。
寻找符合 Python 风格的求和方式
就像“什么是美”没有确切的答案一样,“什么是 Python 风格”也没有标准答案。如果回答“地道的 Python”(我通常会这样说),不能让人 100% 满意,因为对你来说是“地道的”,在我看来却可能不是。但我可以肯定的是,“地道”并不是指使用最鲜为人知的语言特性。
Python-list(https://mail.python.org/mailman/listinfo/python-list)中有一篇发表于 2003 年 4 月的话题,题为“Pythonic Way to Sum n-th List Element?”(https://mail.python.org/pipermail/python-list/2003-April/218568.html)。这个话题与本章讨论的
reduce函数有关。该话题的发起人 Guy Middleton 说他不喜欢使用
lambda表达式,问下面这个方案有没有办法改进:9>>> my_list = [[1, 2, 3], [40, 50, 60], [9, 8, 7]] >>> import functools >>> functools.reduce(lambda a, b: a+b, [sub[1] for sub in my_list]) 60这段代码有很多习惯用法:
lambda、reduce和列表推导。最终,这可能会变成人气竞赛,因为它冒犯了讨厌lambda的人和看不上列表推导的人——这两种人都很多。如果使用
lambda,或许就不应该使用列表推导——过滤除外,但这不是过滤。下面是我给出的方案,这能讨得
lambda拥护者的欢心:>>> functools.reduce(lambda a, b: a + b[1], my_list, 0) 60我没有参与那个话题,而且我不会在真实的代码中使用上述方案,因为我非常不喜欢
lambda表达式。这里只是为了举例说明不使用列表推导怎么做。第一个答案是 Fernando Perez 给出的,他是 IPython 的创建者,他的答案强调了 NumPy 支持 n 维数组和 n 维切片:
>>> import numpy as np >>> my_array = np.array(my_list) >>> np.sum(my_array[:, 1]) 60我觉得 Perez 的方案很棒,不过 Guy Middleton 推崇 Paul Rubin 和 Skip Montanaro 给出的下述方案:
>>> import operator >>> functools.reduce(operator.add, [sub[1] for sub in my_list], 0) 60随后,Evan Simpson 问道:“这样做有什么错?”
>>> total = 0 >>> for sub in my_list: ... total += sub[1] >>> total 60许多人都觉得这也很符合 Python 风格。Alex Martelli 甚至说,Guido 或许就会这么做。
我喜欢 Evan Simpson 的代码,不过也喜欢 David Eppstein 对此给出的评论:
如果你想计算列表中各个元素的和,写出的代码应该看起来像是在“计算元素之和”,而不是“迭代元素,维护一个变量 t,再执行一系列求和操作”。如果不能站在一定高度上表明意图,让语言去关注低层操作,那么要高级语言干嘛?
之后 Alex Martelli 又建议:
求和操作经常需要,我不介意 Python 提供一个这样的内置函数。但是,在我看来,“reduce(operator.add, ...”不是好方法(作为一名 APL 老程序员和 FP 语言的爱好者,我应该喜欢,但是我并不喜欢)。
随后,Alex 建议提供并实现了
sum()函数。这次讨论之后三个月,Python 2.3 就内置了这个函数。因此,Alex 喜欢的句法变成了标准:>>> sum([sub[1] for sub in my_list]) 60下一年年末(2004 年 11 月),Python 2.4 发布了,这一版引入了生成器表达式。因此,在我看来,Guy Middleton 那个问题目前最符合 Python 风格的答案是:
>>> sum(sub[1] for sub in my_list) 60这样写不仅比使用
reduce函数更易阅读,而且还能避免空序列导致的陷阱:sum([])的结果是0,就这么简单。在这次讨论中,Alex Martelli 指出,Python 2 内置的
reduce函数成事不足败事有余,因为它推荐的地道编程方式难以理解。他的观点最有说服力:Python 3 把reduce函数移到functools模块中了。当然,
functools.reduce函数仍有它的作用。实现Vector.__hash__方法时我就用了它,我觉得我的实现方式算得上符合 Python 风格。
9为了在此展示,我稍微修改了这段代码,因为在 2003 年,reduce 是内置函数,而在 Python 3 中要导入。此外,我把 x 和 y 换成了 my_list 和 sub(表示子串)。