流畅的 Python 第二版(GPT 重译)(一)(1)

简介: 流畅的 Python 第二版(GPT 重译)(一)

前言

计划是这样的:当有人使用你不理解的特性时,直接开枪打死他们。这比学习新东西要容易得多,不久之后,活下来的程序员只会用一个容易理解的、微小的 Python 0.9.6 子集来编写代码 。¹

Tim Peters,传奇的核心开发者,Python 之禅的作者

"Python 是一种易于学习、功能强大的编程语言。"这是官方 Python 3.10 教程的开篇词。这是真的,但有一个问题:因为这门语言易学易用,许多实践中的 Python 程序员只利用了它强大特性的一小部分。

有经验的程序员可能在几个小时内就开始编写有用的 Python 代码。当最初富有成效的几个小时变成几周和几个月时,许多开发人员会继续用之前学过的语言的强烈口音编写 Python 代码。即使 Python 是你的第一门语言,在学术界和入门书籍中,它通常被小心地避开语言特定的特性来呈现。

作为一名向有其他语言经验的程序员介绍 Python 的老师,我看到了这本书试图解决的另一个问题:我们只会错过我们知道的东西。来自另一种语言,任何人都可能猜测 Python 支持正则表达式,并在文档中查找。但是,如果你以前从未见过元组解包或描述符,你可能不会搜索它们,最终可能不会使用这些特性,只是因为它们是 Python 特有的。

本书不是 Python 的 A 到 Z 详尽参考。它强调 Python 独有的或在许多其他流行语言中找不到的语言特性。这也主要是一本关于核心语言及其一些库的书。我很少会谈论不在标准库中的包,尽管 Python 包索引现在列出了超过 60,000 个库,其中许多非常有用。

本书适合的读者

本书是为想要精通 Python 3 的在职 Python 程序员编写的。我在 Python 3.10 中测试了这些示例,大部分也在 Python 3.9 和 3.8 中测试过。如果某个示例需要 Python 3.10,会有明确标注。

如果你不确定自己是否有足够的 Python 知识来跟上,请复习官方 Python 教程的主题。除了一些新特性外,本书不会解释教程中涉及的主题。

本书不适合的读者

如果你刚开始学习 Python,这本书可能很难理解。不仅如此,如果你在 Python 学习之旅的早期阶段阅读它,可能会给你一种印象,认为每个 Python 脚本都应该利用特殊方法和元编程技巧。过早的抽象和过早的优化一样糟糕。

五合一的书

我建议每个人都阅读第一章,“Python 数据模型”。本书的核心读者在阅读完第一章后,应该不会有什么困难直接跳到本书的任何部分,但我经常假设你已经阅读了每个特定部分的前面章节。可以把第一部分到第五部分看作是书中之书。

我试图强调在讨论如何构建自己的东西之前先使用现有的东西。例如,在第一部分中,第二章涵盖了现成可用的序列类型,包括一些不太受关注的类型,如collections.deque。用户自定义序列直到第三部分才会讲到,在那里我们还会看到如何利用collections.abc中的抽象基类(ABC)。创建自己的 ABC 要更晚在第三部分中讨论,因为我认为在编写自己的 ABC 之前,熟悉使用现有的 ABC 很重要。

这种方法有几个优点。首先,知道什么是现成可用的,可以避免你重新发明轮子。我们使用现有的集合类比实现自己的集合类更频繁,并且我们可以通过推迟讨论如何创建新类,而将更多注意力放在可用工具的高级用法上。我们也更有可能从现有的 ABC 继承,而不是从头开始创建新的 ABC。最后,我认为在你看到这些抽象的实际应用之后,更容易理解它们。

这种策略的缺点是章节中散布着前向引用。我希望现在你知道我为什么选择这条路,这些引用会更容易容忍。

本书的组织方式

以下是本书各部分的主要主题:

第 I 部分,“数据结构”

第一章介绍了 Python 数据模型,并解释了为什么特殊方法(例如,__repr__)是所有类型的对象行为一致的关键。本书将更详细地介绍特殊方法。本部分的其余章节涵盖了集合类型的使用:序列、映射和集合,以及strbytes的分离——这给 Python 3 用户带来了许多欢呼,而让迁移代码库的 Python 2 用户感到痛苦。还介绍了标准库中的高级类构建器:命名元组工厂和@dataclass装饰器。第二章、第三章和第五章中的部分介绍了 Python 3.10 中新增的模式匹配,分别讨论了序列模式、映射模式和类模式。第 I 部分的最后一章是关于对象的生命周期:引用、可变性和垃圾回收。

第 II 部分,“作为对象的函数”

在这里,我们讨论作为语言中一等对象的函数:这意味着什么,它如何影响一些流行的设计模式,以及如何通过利用闭包来实现函数装饰器。还涵盖了 Python 中可调用对象的一般概念、函数属性、内省、参数注解以及 Python 3 中新的nonlocal声明。第八章介绍了函数签名中类型提示的主要新主题。

第 III 部分,“类和协议”

现在的重点是"手动"构建类——而不是使用第五章中介绍的类构建器。与任何面向对象(OO)语言一样,Python 有其特定的功能集,这些功能可能存在也可能不存在于你和我学习基于类的编程的语言中。这些章节解释了如何构建自己的集合、抽象基类(ABC)和协议,以及如何处理多重继承,以及如何在有意义时实现运算符重载。第十五章继续介绍类型提示。

第 IV 部分,“控制流”

这一部分涵盖了超越传统的使用条件、循环和子程序的控制流的语言构造和库。我们从生成器开始,然后访问上下文管理器和协程,包括具有挑战性但功能强大的新 yield from 语法。第十八章包含一个重要的示例,在一个简单但功能齐全的语言解释器中使用模式匹配。第十九章,"Python 中的并发模型"是一个新章节,概述了 Python 中并发和并行处理的替代方案、它们的局限性以及软件架构如何允许 Python 在网络规模下运行。我重写了关于异步编程的章节,强调核心语言特性,例如 awaitasync devasync forasync with,并展示了它们如何与 asyncio 和其他框架一起使用。

第五部分,“元编程”

这一部分从回顾用于构建具有动态创建属性以处理半结构化数据(如 JSON 数据集)的类的技术开始。接下来,我们介绍熟悉的属性机制,然后深入探讨 Python 中对象属性访问如何在较低级别使用描述符工作。解释了函数、方法和描述符之间的关系。在第五部分中,逐步实现字段验证库,揭示了微妙的问题,这些问题导致了最后一章中的高级工具:类装饰器和元类。

动手实践的方法

我们经常会使用交互式 Python 控制台来探索语言和库。我觉得强调这种学习工具的力量很重要,尤其是对那些有更多使用静态编译语言经验而没有提供读取-求值-打印循环(REPL)的读者而言。

标准 Python 测试包之一 doctest,通过模拟控制台会话并验证表达式是否得出所示的响应来工作。我用 doctest 检查了本书中的大部分代码,包括控制台列表。你不需要使用甚至了解 doctest 就可以跟随:doctests 的关键特性是它们看起来像是交互式 Python 控制台会话的记录,所以你可以轻松地自己尝试这些演示。

有时,我会在编写使其通过的代码之前,通过展示 doctest 来解释我们想要完成的任务。在考虑如何做之前牢固地确立要做什么,有助于集中我们的编码工作。先编写测试是测试驱动开发(TDD)的基础,我发现它在教学时也很有帮助。如果你不熟悉 doctest,请查看其文档和本书的示例代码仓库

我还使用 pytest 为一些较大的示例编写了单元测试——我发现它比标准库中的 unittest 模块更易于使用且功能更强大。你会发现,通过在操作系统的命令行 shell 中键入 python3 -m doctest example_script.pypytest,可以验证本书中大多数代码的正确性。示例代码仓库根目录下的 pytest.ini 配置确保 doctests 被 pytest 命令收集和执行。

皂盒:我的个人观点

从 1998 年开始,我一直在使用、教授和探讨 Python,我喜欢研究和比较编程语言、它们的设计以及背后的理论。在一些章节的末尾,我添加了"皂盒"侧边栏,其中包含我自己对 Python 和其他语言的看法。如果你不喜欢这样的讨论,请随意跳过。它们的内容完全是可选的。

配套网站:fluentpython.com

为了涵盖新特性(如类型提示、数据类和模式匹配),第二版的内容比第一版增加了近 30%。为了保持书本的便携性,我将一些内容移至 fluentpython.com。你会在几个章节中找到我在那里发表的文章的链接。配套网站上也有一些示例章节。完整文本可在 O’Reilly Learning 订阅服务的在线版本中获得。示例代码仓库在 GitHub 上。

本书中使用的约定

本书使用以下排版惯例:

Italic

表示新术语、URL、电子邮件地址、文件名和文件扩展名。

Constant width

用于程序清单,以及在段落内引用程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。

请注意,当换行符出现在 constant_width 术语中时,不会添加连字符,因为它可能被误解为术语的一部分。

Constant width bold

显示用户应按字面意思键入的命令或其他文本。

Constant width italic

显示应由用户提供的值或由上下文确定的值替换的文本。

提示

此元素表示提示或建议。

注意

此元素表示一般注释。

警告

此元素表示警告或注意事项。

使用代码示例

书中出现的每个脚本和大多数代码片段都可在 GitHub 上的 Fluent Python 代码仓库中找到,网址为 https://fpy.li/code

如果你有技术问题或使用代码示例的问题,请发送电子邮件至 bookquestions@oreilly.com

这本书旨在帮助你完成工作。一般来说,如果本书提供了示例代码,你可以在程序和文档中使用它。除非你要复制大量代码,否则无需联系我们征得许可。例如,编写一个使用本书多个代码片段的程序不需要许可。出售或分发 O’Reilly 图书中的示例需要获得许可。通过引用本书和引用示例代码来回答问题不需要许可。将本书中大量示例代码合并到你的产品文档中确实需要许可。

我们感谢但通常不要求注明出处。出处通常包括标题、作者、出版商和 ISBN,例如:“Fluent Python,第 2 版,Luciano Ramalho 著(O’Reilly)。2022 Luciano Ramalho 版权所有,978-1-492-05635-5。”

如果你认为你对代码示例的使用超出了合理使用范围或上述许可范围,请随时通过 permissions@oreilly.com 与我们联系。

致谢

我没想到五年后更新一本 Python 书会是如此重大的任务,但事实如此。我挚爱的妻子 Marta Mello 总是在我需要她的时候出现。我亲爱的朋友 Leonardo Rochael 从最早的写作到最后的技术审核都一直帮助我,包括整合和复核其他技术审核人员、读者和编辑的反馈。说实话,如果没有你们的支持,Marta 和 Leo,我不知道自己是否能做到。非常感谢你们!

Jürgen Gmach、Caleb Hattingh、Jess Males、Leonardo Rochael 和 Miroslav Šedivý 是第二版的杰出技术审查团队。他们审阅了整本书。Bill Behrman、Bruce Eckel、Renato Oliveira 和 Rodrigo Bernardo Pimentel 审阅了特定章节。他们从不同角度提出的许多建议使本书变得更好。

在早期发布阶段,许多读者发送了更正或做出了其他贡献,包括:Guilherme Alves、Christiano Anderson、Konstantin Baikov、K. Alex Birch、Michael Boesl、Lucas Brunialti、Sergio Cortez、Gino Crecco、Chukwuerika Dike、Juan Esteras、Federico Fissore、Will Frey、Tim Gates、Alexander Hagerman、Chen Hanxiao、Sam Hyeong、Simon Ilincev、Parag Kalra、Tim King、David Kwast、Tina Lapine、Wanpeng Li、Guto Maia、Scott Martindale、Mark Meyer、Andy McFarland、Chad McIntire、Diego Rabatone Oliveira、Francesco Piccoli、Meredith Rawls、Michael Robinson、Federico Tula Rovaletti、Tushar Sadhwani、Arthur Constantino Scardua、Randal L. Schwartz、Avichai Sefati、Guannan Shen、William Simpson、Vivek Vashist、Jerry Zhang、Paul Zuradzki 以及其他不愿透露姓名的人,在我交稿后发送了更正,或者因为我没有记录他们的名字而被遗漏——抱歉。

在研究过程中,我在与 Michael Albert、Pablo Aguilar、Kaleb Barrett、David Beazley、J.S.O. Bueno、Bruce Eckel、Martin Fowler、Ivan Levkivskyi、Alex Martelli、Peter Norvig、Sebastian Rittau、Guido van Rossum、Carol Willing 和 Jelle Zijlstra 的互动中了解了类型、并发、模式匹配和元编程。

O’Reilly 编辑 Jeff Bleiel、Jill Leonard 和 Amelia Blevins 提出的建议在许多地方改善了本书的流畅度。Jeff Bleiel 和制作编辑 Danny Elfanbaum 在整个漫长的马拉松中都一直支持我。

他们每个人的见解和建议都让这本书变得更好、更准确。不可避免地,最终产品中仍然会有我自己制造的错误。我提前表示歉意。

最后,我要向我在 Thoughtworks 巴西的同事们表示衷心的感谢,尤其是我的赞助人 Alexey Bôas,他们一直以多种方式支持这个项目。

当然,每一个帮助我理解 Python 并编写第一版的人现在都应该得到双倍的感谢。没有成功的第一版就不会有第二版。

第一版致谢

Josef Hartwig 设计的包豪斯国际象棋是优秀设计的典范:美观、简洁、清晰。建筑师之子、字体设计大师之弟 Guido van Rossum 创造了一部语言设计的杰作。我喜欢教授 Python,因为它美观、简洁、清晰。

Alex Martelli 和 Anna Ravenscroft 是最早看到本书大纲并鼓励我将其提交给 O’Reilly 出版的人。他们的书教会了我地道的 Python,是技术写作在清晰、准确和深度方面的典范。Alex 在 Stack Overflow 上的 6,200 多个帖子是语言及其正确使用方面的见解源泉。

Martelli 和 Ravenscroft 也是本书的技术评审,还有 Lennart Regebro 和 Leonardo Rochael。这个杰出的技术评审团队中的每个人都有至少 15 年的 Python 经验,对与社区中其他开发人员密切联系的高影响力 Python 项目做出了许多贡献。他们一起给我发来了数百条修正、建议、问题和意见,为本书增添了巨大的价值。Victor Stinner 友好地审阅了第二十一章,将他作为 asyncio 维护者的专业知识带到了技术评审团队中。能在过去的几个月里与他们合作,我感到非常荣幸和愉快。

编辑 Meghan Blanchette 是一位杰出的导师,帮助我改进了本书的组织和流程,让我知道什么时候它变得无聊,并阻止我进一步拖延。Brian MacDonald 在 Meghan 不在时编辑了第二部分的章节。我很高兴与他们以及我在 O’Reilly 联系过的每个人合作,包括 Atlas 开发和支持团队(Atlas 是 O’Reilly 的图书出版平台,我很幸运能使用它来写这本书)。

Mario Domenech Goulart 从第一个早期版本开始就提供了大量详细的建议。我还收到了 Dave Pawson、Elias Dorneles、Leonardo Alexandre Ferreira Leite、Bruce Eckel、J.S. Bueno、Rafael Gonçalves、Alex Chiaranda、Guto Maia、Lucas Vido 和 Lucas Brunialti 的宝贵反馈。

多年来,许多人敦促我成为一名作家,但最有说服力的是 Rubens Prates、Aurelio Jargas、Rudá Moura 和 Rubens Altimari。Mauricio Bussab 为我打开了许多大门,包括我第一次真正尝试写书。Renzo Nuccitelli 一路支持这个写作项目,即使这意味着我们在 python.pro.br 的合作起步缓慢。

美妙的巴西 Python 社区知识渊博、慷慨大方、充满乐趣。Python Brasil 小组有数千人,我们的全国会议汇聚了数百人,但在我的 Pythonista 旅程中最具影响力的是 Leonardo Rochael、Adriano Petrich、Daniel Vainsencher、Rodrigo RBP Pimentel、Bruno Gola、Leonardo Santagada、Jean Ferri、Rodrigo Senra、 J.S. Bueno、David Kwast、Luiz Irber、Osvaldo Santana、Fernando Masanori、Henrique Bastos、Gustavo Niemayer、Pedro Werneck、Gustavo Barbieri、Lalo Martins、Danilo Bellini 和 Pedro Kroger。

Dorneles Tremea 是一位伟大的朋友(他慷慨地奉献时间和知识),一位了不起的黑客,也是巴西 Python 协会最鼓舞人心的领导者。他离开得太早了。

多年来,我的学生通过他们的提问、见解、反馈和创造性的问题解决方案教会了我很多东西。Érico Andrei 和 Simples Consultoria 让我第一次能够专注于当一名 Python 老师。

Martijn Faassen 是我的 Grok 导师,与我分享了关于 Python 和尼安德特人的宝贵见解。他以及 Paul Everitt、Chris McDonough、Tres Seaver、Jim Fulton、Shane Hathaway、Lennart Regebro、Alan Runyan、Alexander Limi、Martijn Pieters、Godefroid Chapelle 等来自 Zope、Plone 和 Pyramid 星球的人的工作对我的职业生涯起到了决定性作用。多亏了 Zope 和冲浪第一波网络浪潮,我能够从 1998 年开始以 Python 谋生。José Octavio Castro Neves 是我在巴西第一家以 Python 为中心的软件公司的合伙人。

在更广泛的 Python 社区中,我有太多的大师无法一一列举,但除了已经提到的那些,我还要感谢 Steve Holden、Raymond Hettinger、A.M. Kuchling、David Beazley、Fredrik Lundh、Doug Hellmann、Nick Coghlan、Mark Pilgrim、Martijn Pieters、Bruce Eckel、Michele Simionato、Wesley Chun、Brandon Craig Rhodes、Philip Guo、Daniel Greenfeld、Audrey Roy 和 Brett Slatkin,感谢他们教会我新的更好的 Python 教学方式。

这些页面大部分是在我的家庭办公室和两个实验室写的:CoffeeLab 和 Garoa Hacker Clube。CoffeeLab 是位于巴西圣保罗 Vila Madalena 的咖啡因极客总部。Garoa Hacker Clube 是一个向所有人开放的黑客空间:一个社区实验室,任何人都可以自由尝试新想法。

Garoa 社区提供了灵感、基础设施和宽松的环境。我想 Aleph 会喜欢这本书。

我的母亲 Maria Lucia 和父亲 Jairo 总是全力支持我。我希望他能在这里看到这本书,我很高兴能与她分享。

我的妻子 Marta Mello 忍受了 15 个月总是在工作的丈夫,但她仍然保持支持,并在我担心可能会退出这个马拉松项目的一些关键时刻给予我指导。

谢谢你们,感谢一切。

¹ 2002 年 12 月 23 日在 comp.lang.python Usenet 小组的留言:“Acrimony in c.l.p”。

第一部分:数据结构

第一章:Python 数据模型

Guido 在语言设计美学方面的感觉令人惊叹。我遇到过许多优秀的语言设计师,他们能构建理论上漂亮但无人会使用的语言,但 Guido 是为数不多的能够构建一门理论上略微欠缺但编写程序时充满乐趣的语言的人。

Jim Hugunin,Jython 的创建者,AspectJ 的联合创建者,以及.Net DLR¹的架构师

Python 最大的优点之一是其一致性。使用 Python 一段时间后,你能够开始对新接触到的特性做出有根据的、正确的猜测。

然而,如果你在学 Python 之前学过其他面向对象语言,你可能会觉得使用len(collection)而不是collection.len()很奇怪。这个明显的奇怪之处只是冰山一角,一旦正确理解,它就是我们称之为Pythonic的一切的关键。这个冰山被称为 Python 数据模型,它是我们用来使自己的对象与最符合语言习惯的特性很好地配合的 API。

你可以将数据模型视为对 Python 作为框架的描述。它规范了语言本身的构建块的接口,例如序列、函数、迭代器、协程、类、上下文管理器等。

使用框架时,我们会花费大量时间编写被框架调用的方法。在利用 Python 数据模型构建新类时也会发生同样的情况。Python 解释器调用特殊方法来执行基本的对象操作,通常由特殊语法触发。特殊方法名总是以双下划线开头和结尾。例如,语法obj[key]__getitem__特殊方法支持。为了计算my_collection[key],解释器会调用my_collection.__getitem__(key)

当我们希望对象支持并与基本语言结构交互时,我们会实现特殊方法,例如:

  • 集合
  • 属性访问
  • 迭代(包括使用async for进行的异步迭代)
  • 运算符重载
  • 函数和方法调用
  • 字符串表示和格式化
  • 使用await进行异步编程
  • 对象的创建和销毁
  • 使用withasync with语句管理上下文

Magic 和 Dunder

“魔术方法"是特殊方法的俚语,但我们如何谈论像__getitem__这样的特定方法呢?我从作者和教师 Steve Holden 那里学会了说"dunder-getitem”。"Dunder"是"前后双下划线"的缩写。这就是为什么特殊方法也被称为dunder 方法Python 语言参考"词法分析"章节警告说,“在任何上下文中,任何不遵循明确记录的__*__名称的使用都可能在没有警告的情况下被破坏。”

本章的新内容

本章与第一版相比变化不大,因为它是对 Python 数据模型的介绍,而数据模型相当稳定。最重要的变化是:

  • 支持异步编程和其他新特性的特殊方法,已添加到"特殊方法概述"的表格中。
  • 图 1-2 展示了在"集合 API"中特殊方法的使用,包括 Python 3.6 中引入的collections.abc.Collection抽象基类。

此外,在第二版中,我采用了 Python 3.6 引入的f-string语法,它比旧的字符串格式化表示法(str.format()方法和%运算符)更具可读性,通常也更方便。

提示

仍然使用 my_fmt.format() 的一个原因是,当 my_fmt 的定义必须在代码中与格式化操作需要发生的地方不同的位置时。例如,当 my_fmt 有多行并且最好在常量中定义时,或者当它必须来自配置文件或数据库时。这些都是真正的需求,但不会经常发生。

Python 风格的纸牌

示例 1-1 很简单,但它展示了仅实现两个特殊方法 __getitem____len__ 的强大功能。

示例 1-1. 一副扑克牌序列
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()
    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]
    def __len__(self):
        return len(self._cards)
    def __getitem__(self, position):
        return self._cards[position]

首先要注意的是使用 collections.namedtuple 构造一个简单的类来表示单个牌。我们使用namedtuple 来构建只有属性而没有自定义方法的对象类,就像数据库记录一样。在示例中,我们使用它为牌组中的牌提供了一个很好的表示,如控制台会话所示:

>>> beer_card = Card('7', 'diamonds')
>>> beer_card
Card(rank='7', suit='diamonds')

但这个例子的重点是 FrenchDeck 类。它很短,但却很有冲击力。首先,像任何标准 Python 集合一样,牌组响应 len() 函数并返回其中的牌数:

>>> deck = FrenchDeck()
>>> len(deck)
52

读取牌组中的特定牌(例如第一张或最后一张)很容易,这要归功于 __getitem__ 方法:

>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[-1]
Card(rank='A', suit='hearts')

我们应该创建一个方法来随机抽取一张牌吗?没有必要。Python 已经有一个从序列中获取随机项的函数:random.choice。我们可以在一个 deck 实例上使用它:

>>> from random import choice
>>> choice(deck)
Card(rank='3', suit='hearts')
>>> choice(deck)
Card(rank='K', suit='spades')
>>> choice(deck)
Card(rank='2', suit='clubs')

我们刚刚看到了利用 Python 数据模型使用特殊方法的两个优点:

  • 你的类的用户不必记住标准操作的任意方法名称。(“如何获得项目数?是 .size().length() 还是什么?”)
  • 从丰富的 Python 标准库中受益并避免重新发明轮子更容易,比如 random.choice 函数。

但它变得更好了。

因为我们的 __getitem__ 将工作委托给 self._cards[] 运算符,所以我们的牌组自动支持切片。以下是我们如何查看全新牌组中的前三张牌,然后从索引 12 开始每次跳过 13 张牌来选出四张 A:

>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]

只需实现 __getitem__ 特殊方法,我们的牌组也是可迭代的:

>>> for card in deck:  # doctest: +ELLIPSIS
...   print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
...

我们还可以反向迭代牌组:

>>> for card in reversed(deck):  # doctest: +ELLIPSIS
...   print(card)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
...

doctest 中的省略号

只要有可能,我就会从 doctest 中提取本书中的 Python 控制台列表以确保准确性。当输出太长时,省略部分用省略号(...)标记,就像前面代码中的最后一行。在这种情况下,我使用 # doctest: +ELLIPSIS 指令来使 doctest 通过。如果你在交互式控制台中尝试这些示例,你可以完全忽略 doctest 注释。

迭代通常是隐式的。如果一个集合没有 __contains__ 方法,in 运算符会进行顺序扫描。恰好:in 适用于我们的 FrenchDeck 类,因为它是可迭代的。看看这个:

>>> Card('Q', 'hearts') in deck
True
>>> Card('7', 'beasts') in deck
False

那么排序呢?一个常见的牌的排名系统是先按点数(A 最高),然后按花色顺序:黑桃(最高)、红心、方块和梅花(最低)。这是一个函数,它根据该规则对牌进行排名,梅花 2 返回0,黑桃 A 返回51

suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)
def spades_high(card):
    rank_value = FrenchDeck.ranks.index(card.rank)
    return rank_value * len(suit_values) + suit_values[card.suit]

有了 spades_high,我们现在可以按点数递增的顺序列出我们的牌组:

>>> for card in sorted(deck, key=spades_high):  # doctest: +ELLIPSIS
...      print(card)
Card(rank='2', suit='clubs')
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts')
... (46 cards omitted)
Card(rank='A', suit='diamonds')
Card(rank='A', suit='hearts')
Card(rank='A', suit='spades')

虽然 FrenchDeck 隐式继承自 object 类,但其大部分功能不是继承而来的,而是通过利用数据模型和组合来实现的。通过实现特殊方法 __len____getitem__,我们的 FrenchDeck 表现得像一个标准的 Python 序列,允许它从核心语言特性(例如迭代和切片)和标准库中受益,如使用 random.choicereversedsorted 的示例所示。得益于组合,__len____getitem__ 实现可以将所有工作委托给一个 list 对象 self._cards

那么洗牌呢?

到目前为止,FrenchDeck无法被洗牌,因为它是不可变的:卡片及其位置不能被改变,除非违反封装并直接处理_cards属性。在第十三章中,我们将通过添加一行__setitem__方法来解决这个问题。

特殊方法的使用方式

关于特殊方法需要知道的第一件事是,它们是由 Python 解释器调用的,而不是由你调用的。你不会写my_object.__len__()。你写的是len(my_object),如果my_object是一个用户定义类的实例,那么 Python 会调用你实现的__len__方法。

但是当处理内置类型如liststrbytearray,或者像 NumPy 数组这样的扩展类型时,解释器会采取一种快捷方式。用 C 语言编写的可变长度 Python 集合包括一个名为PyVarObject的结构体²,其中有一个ob_size字段,用于保存集合中的项数。因此,如果my_object是这些内置类型之一的实例,那么len(my_object)会直接获取ob_size字段的值,这比调用一个方法要快得多。

通常情况下,特殊方法的调用是隐式的。例如,语句for i in x:实际上会调用iter(x),如果x__iter__()方法,则会调用它,否则会像FrenchDeck示例那样使用x.__getitem__()

通常,你的代码不应该有太多直接调用特殊方法的地方。除非你在做大量的元编程,否则你应该更多地实现特殊方法,而不是显式地调用它们。唯一经常被用户代码直接调用的特殊方法是__init__,用于在你自己的__init__实现中调用超类的初始化方法。

如果你需要调用一个特殊方法,通常最好调用相关的内置函数(例如leniterstr等)。这些内置函数会调用相应的特殊方法,但通常还提供其他服务,并且对于内置类型来说,比方法调用更快。例如,参见第十七章中的"与可调用对象一起使用 iter"。

在接下来的部分中,我们将看到特殊方法的一些最重要的用途:

  • 模拟数值类型
  • 对象的字符串表示
  • 对象的布尔值
  • 实现集合类

模拟数值类型

几个特殊方法允许用户对象响应诸如+之类的运算符。我们将在第十六章中更详细地介绍这一点,但这里我们的目标是通过另一个简单的例子来进一步说明特殊方法的使用。

我们将实现一个类来表示二维向量——即数学和物理中使用的欧几里得向量(参见图 1-1)。

小贴士

内置的complex类型可以用来表示二维向量,但我们的类可以扩展为表示n维向量。我们将在第十七章中实现这一点。

图 1-1. 二维向量加法示例;Vector(2, 4) + Vector(2, 1) 的结果是 Vector(4, 5)。

我们将通过编写一个模拟控制台会话来开始设计这个类的 API,稍后我们可以将其用作文档测试。下面的代码片段测试了图 1-1 中所示的向量加法:

>>> v1 = Vector(2, 4)
>>> v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5)

请注意+运算符如何生成一个新的Vector,并以友好的格式显示在控制台上。

内置函数abs返回整数和浮点数的绝对值,以及complex数的模,所以为了保持一致,我们的 API 也使用abs来计算向量的模:

>>> v = Vector(3, 4)
>>> abs(v)
5.0

我们还可以实现*运算符来执行标量乘法(即,将一个向量乘以一个数来得到一个新的向量,其方向相同,但大小被乘以该数):

>>> v * 3
Vector(9, 12)
>>> abs(v * 3)
15.0

示例 1-2 是一个Vector类,通过使用特殊方法__repr____abs____add____mul__实现了刚才描述的操作。

示例 1-2. 一个简单的二维向量类
"""
vector2d.py: a simplistic class demonstrating some special methods
It is simplistic for didactic reasons. It lacks proper error handling,
especially in the ``__add__`` and ``__mul__`` methods.
This example is greatly expanded later in the book.
Addition::
 >>> v1 = Vector(2, 4)
 >>> v2 = Vector(2, 1)
 >>> v1 + v2
 Vector(4, 5)
Absolute value::
 >>> v = Vector(3, 4)
 >>> abs(v)
 5.0
Scalar multiplication::
 >>> v * 3
 Vector(9, 12)
 >>> abs(v * 3)
 15.0
"""
import math
class Vector:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    def __repr__(self):
        return f'Vector({self.x!r}, {self.y!r})'
    def __abs__(self):
        return math.hypot(self.x, self.y)
    def __bool__(self):
        return bool(abs(self))
    def __add__(self, other):
        x = self.x + other.x
        y = self.y + other.y
        return Vector(x, y)
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

除了熟悉的__init__之外,我们还实现了五个特殊方法。请注意,在类中或 doctests 所说明的类的典型用法中,没有一个方法是直接调用的。如前所述,Python 解释器是大多数特殊方法的唯一频繁调用者。

示例 1-2 实现了两个操作符:+*,以展示__add____mul__的基本用法。在这两种情况下,方法都会创建并返回Vector的新实例,而不会修改任何一个操作数——selfother只是被读取。这是中缀操作符的预期行为:创建新对象而不接触其操作数。我将在第十六章中对此有更多说明。

警告

按照实现,示例 1-2 允许Vector乘以一个数,但不允许数乘以Vector,这违反了标量乘法的交换律。我们将在第十六章中用特殊方法__rmul__来解决这个问题。

在接下来的部分中,我们将讨论Vector中的其他特殊方法。

字符串表示

内置的repr函数会调用特殊方法__repr__来获取对象的字符串表示,以便检查。如果没有自定义__repr__,Python 控制台会显示Vector实例。

交互式控制台和调试器对计算结果调用repr,经典的%操作符格式化中的%r占位符以及f-strings中新的格式字符串语法使用的!r转换字段中的str.format方法也是如此。

请注意,我们__repr__中的f-string使用!r来获取要显示的属性的标准表示。这是个好习惯,因为它展示了Vector(1, 2)Vector('1', '2')之间的关键区别——在这个例子中,后者不起作用,因为构造函数的参数应该是数字,而不是str

__repr__返回的字符串应该是明确的,如果可能的话,应该与重新创建所表示对象所需的源代码相匹配。这就是为什么我们的Vector表示看起来像调用类的构造函数(例如Vector(3, 4))。

相比之下,内置的str()函数会调用__str__print函数也会隐式地使用它。它应该返回一个适合向终端用户显示的字符串。

有时__repr__返回的相同字符串对用户友好,你不需要编写__str__,因为从object类继承的实现会调用__repr__作为后备。示例 5-2 是本书中有自定义__str__的几个示例之一。

提示

有其他语言toString方法使用经验的程序员倾向于实现__str__而不是__repr__。如果你在 Python 中只实现这两个特殊方法之一,选择__repr__

"Python 中__str____repr__有什么区别?"是一个 Stack Overflow 的问题,Python 专家 Alex Martelli 和 Martijn Pieters 对此做出了精彩的贡献。

自定义类型的布尔值

尽管 Python 有bool类型,但它在布尔上下文中接受任何对象,例如控制ifwhile语句的表达式,或者作为andornot的操作数。为了确定一个值xtruthy还是falsy,Python 会应用bool(x),它返回TrueFalse

默认情况下,用户定义类的实例被视为真值,除非实现了__bool____len__。基本上,bool(x)调用x.__bool__()并使用结果。如果没有实现__bool__,Python 会尝试调用x.__len__(),如果返回零,bool返回False。否则bool返回True

我们对__bool__的实现在概念上很简单:如果向量的大小为零,则返回False,否则返回True。我们使用bool(abs(self))将大小转换为布尔值,因为__bool__期望返回布尔值。在__bool__方法之外,很少需要显式调用bool(),因为任何对象都可以用在布尔上下文中。

注意特殊方法__bool__如何允许你的对象遵循Python 标准库文档的"内置类型"章节中定义的真值测试规则。

注意

Vector.__bool__的更快实现是:

def __bool__(self):
        return bool(self.x or self.y)

这更难阅读,但避免了通过abs__abs__、平方和平方根的旅程。需要显式转换为bool,因为__bool__必须返回布尔值,而or会原样返回任一操作数:如果x为真值,则x or y求值为x,否则结果为y,无论是什么。

Collection API

图 1-2 展示了该语言中基本集合类型的接口。图中所有的类都是抽象基类(ABC)。第十三章涵盖了 ABC 和collections.abc模块。本节的目标是全面概览 Python 最重要的集合接口,展示它们是如何由特殊方法构建而成的。

图 1-2. 包含基本集合类型的 UML 类图。斜体方法名是抽象的,因此必须由具体子类如listdict实现。其余方法有具体实现,因此子类可以继承它们。

每个顶层 ABC 都有一个单独的特殊方法。Collection ABC(Python 3.6 新增)统一了每个集合应该实现的三个基本接口:

  • Iterable支持for解包和其他形式的迭代
  • Sized支持内置函数len
  • Container支持in运算符

Python 并不要求具体类实际继承任何这些 ABC。任何实现了__len__的类都满足Sized接口。

Collection的三个非常重要的特化是:

  • Sequence,形式化了liststr等内置类型的接口
  • Mapping,由dictcollections.defaultdict等实现。
  • Set,内置类型setfrozenset的接口

只有SequenceReversible的,因为序列支持任意顺序的内容,而映射和集合则不支持。

注意

从 Python 3.7 开始,dict类型正式"有序",但这只意味着保留了键的插入顺序。你不能随意重新排列dict中的键。

Set ABC 中的所有特殊方法都实现了中缀运算符。例如,a & b计算集合ab的交集,在__and__特殊方法中实现。

接下来两章将详细介绍标准库序列、映射和集合。

现在让我们考虑 Python 数据模型中定义的主要特殊方法类别。

流畅的 Python 第二版(GPT 重译)(一)(2)https://developer.aliyun.com/article/1484365

相关文章
|
7天前
|
数据采集 存储 人工智能
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
21 0
|
12天前
|
机器学习/深度学习 人工智能 自然语言处理
总结几个GPT的超实用之处【附带Python案例】
总结几个GPT的超实用之处【附带Python案例】
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(3)
JavaScript 权威指南第七版(GPT 重译)(七)
32 0
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
60 0
|
12天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
90 2
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
12天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
55 4
|
12天前
|
JSON 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(五)(2)
JavaScript 权威指南第七版(GPT 重译)(五)
36 5
|
12天前
|
JSON JavaScript 前端开发
JavaScript 权威指南第七版(GPT 重译)(四)(4)
JavaScript 权威指南第七版(GPT 重译)(四)
67 6
|
12天前
|
Web App开发 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(四)(1)
JavaScript 权威指南第七版(GPT 重译)(四)
35 2
|
12天前
|
存储 JavaScript 前端开发
JavaScript 权威指南第七版(GPT 重译)(三)(3)
JavaScript 权威指南第七版(GPT 重译)(三)
41 1