返回介绍

10.9 延伸阅读

发布于 2024-02-05 21:59:47 字数 4858 浏览 0 评论 0 收藏 0

Vector 类中的大多数特殊方法在第 9 章定义的 Vector2d 类中也有,因此前一章给出的延伸阅读材料同样适合本章。

强大的高阶函数 reduce 也叫合拢、累计、聚合、压缩和注入。更多信息参见维基百科中的“Fold (higher-order function)”词条。这篇文章展示了高阶函数的用途,着重说明了具有递归数据结构的函数式语言。这篇文章中还有一个表格,列出了很多编程语言中起合拢作用的函数。

杂谈

把协议当作非正式的接口

协议不是 Python 发明的。Smalltalk 团队,也就是“面向对象”的发明者,使用“协议”这个词表示现在我们称之为接口的特性。某些 Smalltalk 编程环境允许程序员把一组方法标记为协议,但这只不过是一种文档,用于辅助导航,语言不对其施加特定措施。因此,向熟悉正式(而且编译器会施加措施)接口的人解释“协议”时,我会简单地说它是“非正式的接口”。

动态类型语言中的既定协议会自然进化。所谓动态类型是指在运行时检查类型,因为方法签名和变量没有静态类型信息。Ruby 是一门重要的面向对象动态类型语言,它也使用协议。

在 Python 文档中,如果看到“文件类对象”这样的表述,通常说的就是协议。这是一种简短的说法,意思是:“行为基本与文件一致,实现了部分文件接口,满足上下文相关需求的东西。”

你可能觉得只实现协议的一部分不够严谨,但是这样做的优点是简单。“Data Model”一章的 3.3 节建议:

模仿内置类型实现类时,记住一点:模仿的程度对建模的对象来说合理即可。例如,有些序列可能只需要获取单个元素,而不必提取切片。

——Python 语言参考手册中“Data Model”一章

不要为了满足过度设计的接口契约和让编译器开心,而去实现不需要的方法,我们要遵守 KISS 原则

第 11 章还会讨论协议和接口,这正是那一章的主要话题。

鸭子类型的起源

我相信 Ruby 社区在“鸭子类型”这个术语的推广过程中起了主要作用,因为他们向大量 Java 使用者宣扬了这个说法。但是,在 Ruby 或 Python 流行起来之前,Python 就使用这个术语了。根据维基百科,在面向对象编程中较早使用鸭子作比喻的人是 Alex Martelli,在他于 2000 年 7 月 26 日发到 Python-list 中的一篇文章里:“polymorphism (was Re: Type checking in python?)”。本章开头引用的那句话就出自那篇文章。如果你想知道“鸭子类型”这个术语的真正起源,以及很多编程语言对这个面向对象概念的运用,请阅读维基百科中的“Duck typing”词条。

安全的 __format__ 方法,增强可用性

实现 __format__ 方法时,我们没有采取措施防范 Vector 实例拥有大量分量,不过在 __repr__ 方法中我们使用 reprlib 做了预防。这是因为 repr() 函数用于调试和记录日志,所以必须生成可用的输出;而 __format__ 方法用于向最终用户显示输出,他们大概想看到整个 Vector。如果你觉得这样做危险,可以再为格式规范微语言实现一个扩展。

如果是我,我会这么做:默认情况下,格式化的 Vector 实例显示有限个分量,比如说 30 个。如果元素数量超过上限,默认的行为是像 reprlib 那样,截断超出的部分,使用 ... 表示。然而,如果格式说明符后面有特殊的 * 代码(意思是“全部”),那么就不限制显示的元素数量。因此,用户在不知情的情况下不会被特别长的输出吓到。如果默认的上限碍事,那么 ... 的存在对用户是个提醒,用户研究文档后会发现 * 格式代码。

如果你实现了,请向本书的 GitHub 仓库发一个拉取请求。

寻找符合 Python 风格的求和方式

就像“什么是美”没有确切的答案一样,“什么是 Python 风格”也没有标准答案。如果回答“地道的 Python”(我通常会这样说),不能让人 100% 满意,因为对你来说是“地道的”,在我看来却可能不是。但我可以肯定的是,“地道”并不是指使用最鲜为人知的语言特性。

Python-list 中有一篇发表于 2003 年 4 月的话题,题为“Pythonic Way to Sum n-th List Element?”。这个话题与本章讨论的 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(表示子串)。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文