广州万户网站公司wordpress英文美食主题
news/
2025/9/29 11:00:56/
文章来源:
广州万户网站公司,wordpress英文美食主题,做国际贸易做什么网站,网站页面自动还原代码前言 计划是这样的#xff1a;当有人使用你不理解的特性时#xff0c;直接开枪打死他们。这比学习新东西要容易得多#xff0c;不久之后#xff0c;活下来的程序员只会用一个容易理解的、微小的 Python 0.9.6 子集来编写代码 。 Tim Peters#xff0c;传奇的核心开发者当有人使用你不理解的特性时直接开枪打死他们。这比学习新东西要容易得多不久之后活下来的程序员只会用一个容易理解的、微小的 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__是所有类型的对象行为一致的关键。本书将更详细地介绍特殊方法。本部分的其余章节涵盖了集合类型的使用序列、映射和集合以及str与bytes的分离——这给 Python 3 用户带来了许多欢呼而让迁移代码库的 Python 2 用户感到痛苦。还介绍了标准库中的高级类构建器命名元组工厂和dataclass装饰器。第二章、第三章和第五章中的部分介绍了 Python 3.10 中新增的模式匹配分别讨论了序列模式、映射模式和类模式。第 I 部分的最后一章是关于对象的生命周期引用、可变性和垃圾回收。
第 II 部分“作为对象的函数”
在这里我们讨论作为语言中一等对象的函数这意味着什么它如何影响一些流行的设计模式以及如何通过利用闭包来实现函数装饰器。还涵盖了 Python 中可调用对象的一般概念、函数属性、内省、参数注解以及 Python 3 中新的nonlocal声明。第八章介绍了函数签名中类型提示的主要新主题。
第 III 部分“类和协议”
现在的重点是手动构建类——而不是使用第五章中介绍的类构建器。与任何面向对象OO语言一样Python 有其特定的功能集这些功能可能存在也可能不存在于你和我学习基于类的编程的语言中。这些章节解释了如何构建自己的集合、抽象基类ABC和协议以及如何处理多重继承以及如何在有意义时实现运算符重载。第十五章继续介绍类型提示。
第 IV 部分“控制流”
这一部分涵盖了超越传统的使用条件、循环和子程序的控制流的语言构造和库。我们从生成器开始然后访问上下文管理器和协程包括具有挑战性但功能强大的新 yield from 语法。第十八章包含一个重要的示例在一个简单但功能齐全的语言解释器中使用模式匹配。第十九章Python 中的并发模型是一个新章节概述了 Python 中并发和并行处理的替代方案、它们的局限性以及软件架构如何允许 Python 在网络规模下运行。我重写了关于异步编程的章节强调核心语言特性例如 await、async dev、async for 和 async with并展示了它们如何与 asyncio 和其他框架一起使用。
第五部分“元编程”
这一部分从回顾用于构建具有动态创建属性以处理半结构化数据如 JSON 数据集的类的技术开始。接下来我们介绍熟悉的属性机制然后深入探讨 Python 中对象属性访问如何在较低级别使用描述符工作。解释了函数、方法和描述符之间的关系。在第五部分中逐步实现字段验证库揭示了微妙的问题这些问题导致了最后一章中的高级工具类装饰器和元类。
动手实践的方法
我们经常会使用交互式 Python 控制台来探索语言和库。我觉得强调这种学习工具的力量很重要尤其是对那些有更多使用静态编译语言经验而没有提供读取-求值-打印循环REPL的读者而言。
标准 Python 测试包之一 doctest通过模拟控制台会话并验证表达式是否得出所示的响应来工作。我用 doctest 检查了本书中的大部分代码包括控制台列表。你不需要使用甚至了解 doctest 就可以跟随doctests 的关键特性是它们看起来像是交互式 Python 控制台会话的记录所以你可以轻松地自己尝试这些演示。
有时我会在编写使其通过的代码之前通过展示 doctest 来解释我们想要完成的任务。在考虑如何做之前牢固地确立要做什么有助于集中我们的编码工作。先编写测试是测试驱动开发TDD的基础我发现它在教学时也很有帮助。如果你不熟悉 doctest请查看其文档和本书的示例代码仓库。
我还使用 pytest 为一些较大的示例编写了单元测试——我发现它比标准库中的 unittest 模块更易于使用且功能更强大。你会发现通过在操作系统的命令行 shell 中键入 python3 -m doctest example_script.py 或 pytest可以验证本书中大多数代码的正确性。示例代码仓库根目录下的 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。
如果你有技术问题或使用代码示例的问题请发送电子邮件至 bookquestionsoreilly.com。
这本书旨在帮助你完成工作。一般来说如果本书提供了示例代码你可以在程序和文档中使用它。除非你要复制大量代码否则无需联系我们征得许可。例如编写一个使用本书多个代码片段的程序不需要许可。出售或分发 O’Reilly 图书中的示例需要获得许可。通过引用本书和引用示例代码来回答问题不需要许可。将本书中大量示例代码合并到你的产品文档中确实需要许可。
我们感谢但通常不要求注明出处。出处通常包括标题、作者、出版商和 ISBN例如“Fluent Python第 2 版Luciano Ramalho 著O’Reilly。2022 Luciano Ramalho 版权所有978-1-492-05635-5。”
如果你认为你对代码示例的使用超出了合理使用范围或上述许可范围请随时通过 permissionsoreilly.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 HuguninJython 的创建者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进行异步编程 对象的创建和销毁 使用with或async 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 collectionsCard 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.suitsfor 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(rank7, suitdiamonds)但这个例子的重点是 FrenchDeck 类。它很短但却很有冲击力。首先像任何标准 Python 集合一样牌组响应 len() 函数并返回其中的牌数 deck FrenchDeck()len(deck)
52读取牌组中的特定牌例如第一张或最后一张很容易这要归功于 __getitem__ 方法 deck[0]
Card(rank2, suitspades)deck[-1]
Card(rankA, suithearts)我们应该创建一个方法来随机抽取一张牌吗没有必要。Python 已经有一个从序列中获取随机项的函数random.choice。我们可以在一个 deck 实例上使用它 from random import choicechoice(deck)
Card(rank3, suithearts)choice(deck)
Card(rankK, suitspades)choice(deck)
Card(rank2, suitclubs)我们刚刚看到了利用 Python 数据模型使用特殊方法的两个优点 你的类的用户不必记住标准操作的任意方法名称。“如何获得项目数是 .size()、.length() 还是什么” 从丰富的 Python 标准库中受益并避免重新发明轮子更容易比如 random.choice 函数。
但它变得更好了。
因为我们的 __getitem__ 将工作委托给 self._cards 的 [] 运算符所以我们的牌组自动支持切片。以下是我们如何查看全新牌组中的前三张牌然后从索引 12 开始每次跳过 13 张牌来选出四张 A deck[:3]
[Card(rank2, suitspades), Card(rank3, suitspades),
Card(rank4, suitspades)]deck[12::13]
[Card(rankA, suitspades), Card(rankA, suitdiamonds),
Card(rankA, suitclubs), Card(rankA, suithearts)]只需实现 __getitem__ 特殊方法我们的牌组也是可迭代的 for card in deck: # doctest: ELLIPSIS
... print(card)
Card(rank2, suitspades)
Card(rank3, suitspades)
Card(rank4, suitspades)
...我们还可以反向迭代牌组 for card in reversed(deck): # doctest: ELLIPSIS
... print(card)
Card(rankA, suithearts)
Card(rankK, suithearts)
Card(rankQ, suithearts)
...doctest 中的省略号
只要有可能我就会从 doctest 中提取本书中的 Python 控制台列表以确保准确性。当输出太长时省略部分用省略号...标记就像前面代码中的最后一行。在这种情况下我使用 # doctest: ELLIPSIS 指令来使 doctest 通过。如果你在交互式控制台中尝试这些示例你可以完全忽略 doctest 注释。
迭代通常是隐式的。如果一个集合没有 __contains__ 方法in 运算符会进行顺序扫描。恰好in 适用于我们的 FrenchDeck 类因为它是可迭代的。看看这个 Card(Q, hearts) in deck
TrueCard(7, beasts) in deck
False那么排序呢一个常见的牌的排名系统是先按点数A 最高然后按花色顺序黑桃最高、红心、方块和梅花最低。这是一个函数它根据该规则对牌进行排名梅花 2 返回0黑桃 A 返回51
suit_values dict(spades3, hearts2, diamonds1, clubs0)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, keyspades_high): # doctest: ELLIPSIS
... print(card)
Card(rank2, suitclubs)
Card(rank2, suitdiamonds)
Card(rank2, suithearts)
... (46 cards omitted)
Card(rankA, suitdiamonds)
Card(rankA, suithearts)
Card(rankA, suitspades)虽然 FrenchDeck 隐式继承自 object 类但其大部分功能不是继承而来的而是通过利用数据模型和组合来实现的。通过实现特殊方法 __len__ 和 __getitem__我们的 FrenchDeck 表现得像一个标准的 Python 序列允许它从核心语言特性例如迭代和切片和标准库中受益如使用 random.choice、reversed 和 sorted 的示例所示。得益于组合__len__ 和 __getitem__ 实现可以将所有工作委托给一个 list 对象 self._cards。
那么洗牌呢
到目前为止FrenchDeck无法被洗牌因为它是不可变的卡片及其位置不能被改变除非违反封装并直接处理_cards属性。在第十三章中我们将通过添加一行__setitem__方法来解决这个问题。
特殊方法的使用方式
关于特殊方法需要知道的第一件事是它们是由 Python 解释器调用的而不是由你调用的。你不会写my_object.__len__()。你写的是len(my_object)如果my_object是一个用户定义类的实例那么 Python 会调用你实现的__len__方法。
但是当处理内置类型如list、str、bytearray或者像 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__实现中调用超类的初始化方法。
如果你需要调用一个特殊方法通常最好调用相关的内置函数例如len、iter、str等。这些内置函数会调用相应的特殊方法但通常还提供其他服务并且对于内置类型来说比方法调用更快。例如参见第十七章中的与可调用对象一起使用 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 methodsIt 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 v2Vector(4, 5)Absolute value:: v Vector(3, 4) abs(v)5.0Scalar multiplication:: v * 3Vector(9, 12) abs(v * 3)15.0import mathclass Vector:def __init__(self, x0, y0):self.x xself.y ydef __repr__(self):return fVector({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.xy self.y other.yreturn Vector(x, y)def __mul__(self, scalar):return Vector(self.x * scalar, self.y * scalar)除了熟悉的__init__之外我们还实现了五个特殊方法。请注意在类中或 doctests 所说明的类的典型用法中没有一个方法是直接调用的。如前所述Python 解释器是大多数特殊方法的唯一频繁调用者。
示例 1-2 实现了两个操作符和*以展示__add__和__mul__的基本用法。在这两种情况下方法都会创建并返回Vector的新实例而不会修改任何一个操作数——self或other只是被读取。这是中缀操作符的预期行为创建新对象而不接触其操作数。我将在第十六章中对此有更多说明。
警告
按照实现示例 1-2 允许Vector乘以一个数但不允许数乘以Vector这违反了标量乘法的交换律。我们将在第十六章中用特殊方法__rmul__来解决这个问题。
在接下来的部分中我们将讨论Vector中的其他特殊方法。
字符串表示
内置的repr函数会调用特殊方法__repr__来获取对象的字符串表示以便检查。如果没有自定义__repr__Python 控制台会显示Vector实例Vector object at 0x10e100070。
交互式控制台和调试器对计算结果调用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类型但它在布尔上下文中接受任何对象例如控制if或while语句的表达式或者作为and、or和not的操作数。为了确定一个值x是truthy还是falsyPython 会应用bool(x)它返回True或False。
默认情况下用户定义类的实例被视为真值除非实现了__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 类图。斜体方法名是抽象的因此必须由具体子类如list和dict实现。其余方法有具体实现因此子类可以继承它们。
每个顶层 ABC 都有一个单独的特殊方法。Collection ABCPython 3.6 新增统一了每个集合应该实现的三个基本接口 Iterable支持for、解包和其他形式的迭代 Sized支持内置函数len Container支持in运算符
Python 并不要求具体类实际继承任何这些 ABC。任何实现了__len__的类都满足Sized接口。
Collection的三个非常重要的特化是 Sequence形式化了list和str等内置类型的接口 Mapping由dict、collections.defaultdict等实现。 Set内置类型set和frozenset的接口
只有Sequence是Reversible的因为序列支持任意顺序的内容而映射和集合则不支持。
注意
从 Python 3.7 开始dict类型正式有序但这只意味着保留了键的插入顺序。你不能随意重新排列dict中的键。
Set ABC 中的所有特殊方法都实现了中缀运算符。例如a b计算集合a和b的交集在__and__特殊方法中实现。
接下来两章将详细介绍标准库序列、映射和集合。
现在让我们考虑 Python 数据模型中定义的主要特殊方法类别。
特殊方法概览
Python 语言参考的数据模型章节列出了 80 多个特殊方法名。其中一半以上实现了算术、位运算和比较运算符。关于可用内容的概览请参见下表。
表 1-1 展示了特殊方法名不包括用于实现中缀运算符或核心数学函数如abs的方法名。本书将涵盖其中大部分方法包括最新增加的异步特殊方法如 __anext__Python 3.5 新增以及类定制钩子 __init_subclass__Python 3.6 新增。
表 1-1. 特殊方法名不包括运算符
类别方法名字符串/字节表示__repr__ __str__ __format__ __bytes__ __fspath__转换为数字__bool__ __complex__ __int__ __float__ __hash__ __index__模拟集合__len__ __getitem__ __setitem__ __delitem__ __contains__迭代__iter__ __aiter__ __next__ __anext__ __reversed__可调用对象或协程执行__call__ __await__上下文管理__enter__ __exit__ __aexit__ __aenter__实例创建和销毁__new__ __init__ __del__属性管理__getattr__ __getattribute__ __setattr__ __delattr__ __dir__属性描述符__get__ __set__ __delete__ __set_name__抽象基类__instancecheck__ __subclasscheck__类元编程__prepare__ __init_subclass__ __class_getitem__ __mro_entries__
表 1-2 列出了中缀和数值运算符支持的特殊方法。其中最新的名称是 __matmul__、__rmatmul__ 和 __imatmul__于 Python 3.5 新增用于支持 作为矩阵乘法的中缀运算符我们将在第十六章看到。
表 1-2. 运算符的特殊方法名和符号
运算符类别符号方法名一元数值运算- abs()__neg__ __pos__ __abs__富比较 ! __lt__ __le__ __eq__ __ne__ __gt__ __ge__算术运算 - * / // % divmod() round() ** pow()__add__ __sub__ __mul__ __truediv__ __floordiv__ __mod__ __matmul__ __divmod__ __round__ __pow__反向算术运算交换运算数的算术运算符__radd__ __rsub__ __rmul__ __rtruediv__ __rfloordiv__ __rmod__ __rmatmul__ __rdivmod__ __rpow__增强赋值算术运算 - * / // % **__iadd__ __isub__ __imul__ __itruediv__ __ifloordiv__ __imod__ __imatmul__ __ipow__位运算 #124; ^ ~__and__ __or__ __xor__ __lshift__ __rshift__ __invert__反向位运算交换运算数的位运算符__rand__ __ror__ __rxor__ __rlshift__ __rrshift__增强赋值位运算 #124; ^ __iand__ __ior__ __ixor__ __ilshift__ __irshift__
注意
当第一个操作数的相应特殊方法无法使用时Python 会在第二个操作数上调用反向运算符特殊方法。增强赋值是将中缀运算符与变量赋值组合的简写形式例如 a b。
第十六章详细解释了反向运算符和增强赋值。
为什么 len 不是一个方法
我在 2013 年向核心开发者 Raymond Hettinger 提出了这个问题他回答的关键是引用了Python 之禅中的一句话实用性胜过纯粹性。在特殊方法的使用方式中我描述了当 x 是内置类型的实例时len(x) 的运行速度非常快。对于 CPython 的内置对象不调用任何方法长度直接从 C 结构体中的一个字段读取。获取集合中的项数是一种常见操作必须为 str、list、memoryview 等基本且多样的类型高效地工作。
换句话说len 之所以不作为方法调用是因为它作为 Python 数据模型的一部分与 abs 一样得到特殊对待。但是借助特殊方法 __len__你也可以让 len 适用于你自己的自定义对象。这在内置对象的效率需求和语言的一致性之间取得了合理的平衡。正如Python 之禅所言“特例不足以打破规则。”
注意
如果你认为 abs 和 len 是一元运算符那么相比于在面向对象语言中期望的方法调用语法你可能更倾向于原谅它们的函数外观和感觉。事实上ABC 语言Python 的直接祖先开创了其许多特性有一个相当于 len 的 # 运算符你会写成 #s。当用作中缀运算符时写作 x#s它会计算 x 在 s 中出现的次数在 Python 中对于任何序列 s都可以用 s.count(x) 获得。
章节总结
通过实现特殊方法你的对象可以表现得像内置类型一样从而实现社区认为 Pythonic 的表达性编码风格。
Python 对象的一个基本要求是提供自身的可用字符串表示一个用于调试和日志记录另一个用于呈现给终端用户。这就是为什么数据模型中存在特殊方法 __repr__ 和 __str__ 的原因。
如 FrenchDeck 示例所展示的模拟序列是特殊方法最常见的用途之一。例如数据库库通常以类序列集合的形式返回查询结果。第二章的主题是充分利用现有的序列类型。第十二章将介绍如何实现自己的序列届时我们将创建 Vector 类的多维扩展。
得益于运算符重载Python 提供了丰富的数值类型选择从内置类型到 decimal.Decimal、fractions.Fraction都支持中缀算术运算符。NumPy 数据科学库支持对矩阵和张量使用中缀运算符。第十六章将通过增强 Vector 示例来演示如何实现运算符包括反向运算符和增强赋值。
本书贯穿始终介绍了 Python 数据模型中大多数剩余特殊方法的使用和实现。
延伸阅读
数据模型一章摘自Python 语言参考手册是本章以及本书大部分内容的权威来源。
Alex Martelli、Anna Ravenscroft 和 Steve Holden 合著的Python in a Nutshell, 3rd ed.O’Reilly 出版对数据模型有极佳的阐述。除了实际的 CPython C 源代码外他们对属性访问机制的描述是我所见过最权威的。Martelli 也是 Stack Overflow 上的高产贡献者贴出了超过 6,200 个答案。可以在 Stack Overflow 上看到他的用户资料。
David Beazley 有两本书在 Python 3 的背景下详细介绍了数据模型Python Essential Reference第 4 版Addison-Wesley 出版以及与 Brian K. Jones 合著的Python Cookbook第 3 版O’Reilly 出版。
Gregor Kiczales、Jim des Rivieres 和 Daniel G. Bobrow 合著的The Art of the Metaobject ProtocolMIT 出版社解释了元对象协议的概念Python 数据模型就是其中一个例子。
¹ “Jython 的故事”作为 Samuele Pedroni 和 Noel Rappin 合著的 Jython EssentialsO’Reilly 出版的前言。
² C 结构体是一种带有命名字段的记录类型。
第二章序列之阵 你可能已经注意到提到的几个操作同样适用于文本、列表和表格。文本、列表和表格统称为 “序列”。[…] FOR 命令也可以通用地作用于序列。 Leo Geurts、Lambert Meertens 和 Steven PembertonmABC Programmer’s Handbook¹ 在创建 Python 之前Guido 曾是 ABC 语言的贡献者——一个为初学者设计编程环境的 10 年研究项目。ABC 引入了许多我们现在认为 “Pythonic” 的想法对不同类型序列的通用操作、内置元组和映射类型、缩进结构、无需变量声明的强类型等等。Python 如此用户友好并非偶然。
Python 从 ABC 继承了对序列的统一处理。字符串、列表、字节序列、数组、XML 元素和数据库结果共享一组丰富的通用操作包括迭代、切片、排序和连接。
了解 Python 中可用的各种序列可以节省我们重复发明轮子的时间它们的通用接口激励我们创建正确支持和利用现有和未来序列类型的 API。
本章大部分讨论适用于一般的序列从熟悉的 list 到 Python 3 中新增的 str 和 bytes 类型。这里还涵盖了列表、元组、数组和队列的具体主题但 Unicode 字符串和字节序列的详细信息出现在 第四章。此外这里的想法是涵盖已准备好使用的序列类型。创建你自己的序列类型是 第十二章 的主题。
本章将主要涵盖以下主题 列表推导式和生成器表达式基础 将元组用作记录与将元组用作不可变列表 序列解包和序列模式 从切片读取和向切片写入 专门的序列类型如数组和队列
本章的更新内容
本章最重要的更新是 “使用序列进行模式匹配”。这是 Python 3.10 的新模式匹配特性在第二版中首次出现。
其他变化不是更新而是对第一版的改进 序列内部结构的新图和描述对比容器和扁平序列 简要比较 list 和 tuple 的性能和存储特性 包含可变元素的元组的注意事项以及如何在需要时检测它们
我将命名元组的介绍移至 第五章 的 “经典命名元组”在那里它们与 typing.NamedTuple 和 dataclass 进行了比较。
注意
为了给新内容腾出空间并将页数控制在合理范围内第一版中的 “使用 Bisect 管理有序序列” 一节现在是 fluentpython.com 配套网站中的一篇文章。
内置序列概述
标准库提供了丰富的用 C 实现的序列类型选择
容器序列
可以容纳不同类型的项目包括嵌套容器。一些示例list、tuple 和 collections.deque。
扁平序列
持有一种简单类型的项目。一些示例str、bytes 和 array.array。
容器序列存储对其所包含的对象的引用这些对象可以是任何类型而扁平序列则在其自身的内存空间中存储其内容的值而不是作为独立的 Python 对象。参见图 2-1。 图 2-1. 一个tuple和一个array的简化内存图每个包含三个项目。灰色单元格表示每个 Python 对象的内存头——没有按比例绘制。tuple有一个对其项目的引用数组。每个项目都是一个单独的 Python 对象可能包含对其他 Python 对象的引用比如那个两个项目的列表。相比之下Python array是一个单一的对象包含一个 C 语言的三个 double 数组。
因此扁平序列更紧凑但它们仅限于保存字节、整数和浮点数等原始机器值。
注意
内存中的每个 Python 对象都有一个带有元数据的头部。最简单的 Python 对象float有一个值字段和两个元数据字段 ob_refcnt对象的引用计数 ob_type指向对象类型的指针 ob_fval一个 C double用于保存float的值
在 64 位 Python 构建中这些字段中的每一个都占用 8 个字节。这就是为什么一个浮点数组比一个浮点元组更紧凑数组是一个单一的对象包含浮点数的原始值而元组由多个对象组成——元组本身和其中包含的每个float对象。
对序列类型进行分组的另一种方式是按可变性
可变序列
例如list、bytearray、array.array和collections.deque。
不可变序列
例如tuple、str和bytes。
图 2-2 有助于可视化可变序列如何继承不可变序列的所有方法并实现几个额外的方法。内置的具体序列类型实际上并没有子类化Sequence和MutableSequence抽象基类ABC但它们是注册到这些 ABC 的虚拟子类——我们将在第十三章中看到。作为虚拟子类tuple和list通过了这些测试 from collections import abcissubclass(tuple, abc.Sequence)
Trueissubclass(list, abc.MutableSequence)
True图 2-2.collections.abc 中一些类的简化 UML 类图超类在左侧继承箭头从子类指向超类斜体名称是抽象类和抽象方法。
记住这些共同特征可变与不可变容器与扁平。它们有助于将你对一种序列类型的了解推广到其他类型。
最基本的序列类型是list一个可变容器。我希望你非常熟悉列表所以我们将直接进入列表推导式这是一种构建列表的强大方式但有时会因为语法一开始看起来不寻常而被低估。掌握列表推导式为生成器表达式打开了大门生成器表达式除了其他用途外还可以生成元素来填充任何类型的序列。这两者都是下一节的主题。
列表推导式和生成器表达式
构建序列的一个快速方法是使用列表推导式如果目标是list或生成器表达式对于其他类型的序列。如果你没有每天使用这些语法形式我敢打赌你正在错失编写更易读且通常更快的代码的机会。
如果你怀疑我声称这些构造更具可读性请继续阅读。我会试着说服你。
提示
为了简洁起见许多 Python 程序员将列表推导式称为listcomps将生成器表达式称为genexps。我也会使用这些词。
列表推导式和可读性
这里有一个测试你觉得示例 2-1 和示例 2-2 哪个更易读
示例 2-1. 从字符串构建 Unicode 码点列表 symbols $¢£¥€¤codes []for symbol in symbols:
... codes.append(ord(symbol))
...codes
[36, 162, 163, 165, 8364, 164]示例 2-2. 使用列表推导式从字符串构建 Unicode 码点列表 symbols $¢£¥€¤codes [ord(symbol) for symbol in symbols]codes
[36, 162, 163, 165, 8364, 164]任何稍微了解 Python 的人都可以读懂示例 2-1。然而在学习了列表推导式之后我发现示例 2-2 更具可读性因为它的意图很明确。
for循环可用于执行许多不同的事情扫描序列以计数或选择项目、计算聚合总和、平均值或任何其他任务。示例 2-1 中的代码正在构建一个列表。相比之下列表推导式更加明确。它的目标总是构建一个新列表。
当然也可能滥用列表推导式来编写真正难以理解的代码。我见过 Python 代码其中列表推导式仅用于重复代码块以产生副作用。如果你不对生成的列表做任何事情就不应该使用该语法。此外尽量保持简短。如果列表推导式跨越两行以上最好将其拆开或重写为普通的for循环。运用你的最佳判断对于 Python就像对于英语一样没有明确的清晰写作规则。
语法提示
在 Python 代码中在[]、{}或()对之间的换行符会被忽略。因此你可以构建多行列表、列表推导式、元组、字典等而无需使用\换行转义符如果不小心在其后键入空格它将不起作用。此外当这些分隔符对用于定义包含以逗号分隔的一系列项的字面量时尾随逗号将被忽略。因此例如在编写多行列表字面量时在最后一项后面加上逗号是很周到的这会让下一个编码者更容易向该列表添加一个项目并在阅读差异时减少噪音。
列表推导式通过过滤和转换项目从序列或任何其他可迭代类型构建列表。内置的filter和map可以组合起来做同样的事情但可读性会受到影响我们接下来会看到。
列表推导式与 map 和 filter 的对比
列表推导式可以完成map和filter函数所做的一切而无需功能受限的 Python lambda的扭曲。考虑示例 2-3。
示例 2-3. 通过列表推导式和 map/filter 组合构建的相同列表 symbols $¢£¥€¤beyond_ascii [ord(s) for s in symbols if ord(s) 127]beyond_ascii
[162, 163, 165, 8364, 164]beyond_ascii list(filter(lambda c: c 127, map(ord, symbols)))beyond_ascii
[162, 163, 165, 8364, 164]我曾经认为map和filter比等效的列表推导式更快但 Alex Martelli 指出事实并非如此——至少在前面的示例中不是。Fluent Python代码仓库中的02-array-seq/listcomp_speed.py脚本是一个简单的速度测试比较了列表推导式与filter/map。
在第七章中我将对map和filter进行更多说明。现在我们来看看如何使用列表推导式计算笛卡尔积一个包含由两个或多个列表中所有项构建的元组的列表。
笛卡尔积
列表推导式可以从两个或多个可迭代对象的笛卡尔积构建列表。构成笛卡尔积的项是由每个输入可迭代对象的项构成的元组。结果列表的长度等于输入可迭代对象的长度相乘。参见图 2-3。 图 2-3. 3 个牌面和 4 个花色的笛卡尔积是由 12 对组成的序列。
例如假设你需要生成一个包含两种颜色和三种尺寸的 T 恤列表。示例 2-4 展示了如何使用列表推导式生成该列表。结果有六个项目。
示例 2-4. 使用列表推导式的笛卡尔积 colors [black, white]sizes [S, M, L]tshirts [(color, size) for color in colors for size in sizes] # ①tshirts
[(black, S), (black, M), (black, L), (white, S),(white, M), (white, L)] for color in colors: # ②
... for size in sizes:
... print((color, size))
...
(black, S) (black, M) (black, L) (white, S) (white, M) (white, L) tshirts (color, size) for size in sizes ![3
... for color in colors]tshirts
[(black, S), (white, S), (black, M), (white, M),(black, L), (white, L)]①
这会生成一个按颜色再按大小排列的元组列表。
②
注意结果列表的排列方式就好像for循环按照它们在列表推导式中出现的顺序嵌套一样。
③
要按大小再按颜色排列项目只需重新排列for子句在列表推导式中添加一个换行可以更容易地看出结果的排序方式。
在示例 1-1第一章中我使用以下表达式初始化一副由 4 种花色的 13 种牌面组成的 52 张牌的扑克牌按花色和点数排序 self._cards [Card(rank, suit) for suit in self.suitsfor rank in self.ranks]列表推导式是一招鲜吃遍天它们构建列表。要为其他序列类型生成数据生成器表达式是不二之选。下一节将简要介绍在构建非列表序列的上下文中使用生成器表达式。
生成器表达式
要初始化元组、数组和其他类型的序列你也可以从列表推导式开始但生成器表达式可以节省内存因为它使用迭代器协议一个接一个地产生项目而不是构建一个完整的列表来馈送另一个构造函数。
生成器表达式使用与列表推导式相同的语法但用括号括起来而不是方括号。
示例 2-5 展示了使用生成器表达式构建元组和数组的基本用法。
示例 2-5. 从生成器表达式初始化元组和数组 symbols $¢£¥€¤tuple(ord(symbol) for symbol in symbols) # ①
(36, 162, 163, 165, 8364, 164) import arrayarray.array(I, (ord(symbol) for symbol in symbols)) # ②
array(I, [36, 162, 163, 165, 8364, 164])①
如果生成器表达式是函数调用中的唯一参数则不需要复制括号。
②
array 构造函数接受两个参数因此生成器表达式周围的括号是必需的。array 构造函数的第一个参数定义了用于数组中数字的存储类型我们将在数组中看到。
示例 2-6 使用笛卡尔积中的生成器表达式打印出三种尺寸两种颜色的 T 恤衫名册。与示例 2-4 相比这里从未在内存中构建六个 T 恤衫的列表生成器表达式每次产生一个项目来馈送 for 循环。如果笛卡尔积中使用的两个列表每个都有一千个项目使用生成器表达式就可以节省构建一个包含一百万个项目的列表的成本而这个列表只是用来馈送 for 循环。
示例 2-6. 生成器表达式中的笛卡尔积 colors [black, white]sizes [S, M, L]for tshirt in (f{c} {s} for c in colors for s in sizes): # ①
... print(tshirt)
...
black S black M black L white S white M white L①
生成器表达式一个接一个地产生项目在此示例中从未生成包含所有六种 T 恤衫变体的列表。
注意
第十七章详细解释了生成器的工作原理。这里的想法只是展示如何使用生成器表达式来初始化列表以外的序列或生成不需要保存在内存中的输出。
现在我们继续讨论 Python 中另一个基本的序列类型元组。
元组不仅仅是不可变的列表
一些介绍 Python 的入门文本将元组描述为不可变的列表但这并没有充分利用它们。元组具有双重功能它们可以用作不可变列表也可以用作没有字段名的记录。这种用法有时会被忽略所以我们将从这里开始。
元组作为记录
元组保存记录元组中的每一项保存一个字段的数据项目的位置赋予了它含义。
如果将元组视为不可变列表则根据上下文项目的数量和顺序可能重要也可能不重要。但是在将元组用作字段集合时项目的数量通常是固定的它们的顺序始终很重要。
示例 2-7 显示了用作记录的元组。请注意在每个表达式中对元组进行排序都会破坏信息因为每个字段的含义由其在元组中的位置给出。
示例 2-7. 元组用作记录 lax_coordinates (33.9425, -118.408056) # ①city, year, pop, chg, area (Tokyo, 2003, 32_450, 0.66, 8014) # ②traveler_ids (USA, 31195855), (BRA, CE342567), ![3
... (ESP, XDA205856)]for passport in sorted(traveler_ids): # ④
... print(%s/%s % passport) # ⑤
...
BRA/CE342567 ESP/XDA205856 USA/31195855 for country, _ in traveler_ids: # ⑥
... print(country)
...
USA BRA ESP①
洛杉矶国际机场的纬度和经度。
②
关于东京的数据名称、年份、人口千人、人口变化%和面积平方公里。
③
形式为 (country_code, passport_number) 的元组列表。
④
当我们遍历列表时passport绑定到每个元组。
⑤
%格式化运算符理解元组并将每个项视为单独的字段。
⑥
for循环知道如何分别检索元组的项这称为解包。这里我们对第二个项不感兴趣所以将其赋值给虚拟变量_。
提示
通常使用_作为虚拟变量只是一种约定。它只是一个奇怪但有效的变量名。但是在match/case语句中_是一个通配符可以匹配任何值但不会绑定到一个值。参见使用序列进行模式匹配。在 Python 控制台中前一个命令的结果被赋值给_除非结果是None。
我们通常认为记录是具有命名字段的数据结构。第五章介绍了两种创建具有命名字段的元组的方法。
但通常没有必要费力创建一个类来命名字段尤其是如果你利用解包并避免使用索引访问字段。在示例 2-7 中我们在一条语句中将(Tokyo, 2003, 32_450, 0.66, 8014)赋值给city, year, pop, chg, area。然后%运算符将passport元组中的每一项分配给print参数中格式字符串的相应位置。这是元组解包的两个例子。
注意
术语元组解包被 Pythonista 广泛使用但可迭代解包正在获得关注如PEP 3132 — 扩展可迭代解包的标题所示。
解包序列和可迭代对象不仅详细介绍了元组的解包还包括序列和可迭代对象的解包。
现在让我们将tuple类视为list类的不可变变体。
元组作为不可变列表
Python 解释器和标准库广泛使用元组作为不可变列表你也应该这样做。这带来了两个主要好处
清晰度
当你在代码中看到tuple时你知道它的长度永远不会改变。
性能
与相同长度的list相比tuple使用更少的内存并允许 Python 进行一些优化。
但是请注意tuple的不可变性仅适用于它所包含的引用。元组中的引用不能被删除或替换。但是如果其中一个引用指向一个可变对象并且该对象发生了变化那么tuple的值就会改变。下面的代码片段通过创建两个最初相等的元组a和b来说明这一点。图 2-4 表示内存中b元组的初始布局。 图 2-4。元组本身的内容是不可变的但这只意味着元组持有的引用将始终指向相同的对象。但是如果其中一个引用对象是可变的如列表其内容可能会发生变化。
当b中的最后一项发生变化时b和a变得不同 a (10, alpha, [1, 2])b (10, alpha, [1, 2])a b
Trueb[-1].append(99)a b
Falseb
(10, alpha, [1, 2, 99])包含可变项的元组可能是 bug 的根源。正如我们将在什么是可哈希的中看到的一个对象只有在其值不能改变时才是可哈希的。不可哈希的元组不能插入为dict键或set元素。
如果你想明确确定一个元组或任何对象是否具有固定值可以使用内置的hash创建一个fixed函数如下所示 def fixed(o):
... try:
... hash(o)
... except TypeError:
... return False
... return True
...tf (10, alpha, (1, 2))tm (10, alpha, [1, 2])fixed(tf)
Truefixed(tm)
False我们在元组的相对不可变性中进一步探讨了这个问题。
尽管有这个警告元组仍然被广泛用作不可变列表。Python 核心开发者 Raymond Hettinger 在 StackOverflow 回答在 Python 中元组比列表更高效吗时解释了元组提供的一些性能优势。总结一下Hettinger 写道 为了评估元组字面量Python 编译器在一个操作中为元组常量生成字节码但是对于列表字面量生成的字节码将每个元素作为单独的常量推送到数据栈然后构建列表。 给定元组ttuple(t)只是返回对同一个t的引用。没有必要复制。相比之下给定列表llist(l)构造函数必须创建l的新副本。 由于具有固定长度tuple实例分配它需要的确切内存空间。另一方面list的实例分配时会留有余地以分摊将来追加的成本。 元组中元素的引用存储在元组结构中的数组中而列表在其他地方保存指向引用数组的指针。当列表增长超过当前分配的空间时Python 需要重新分配引用数组以腾出空间因此需要间接寻址。额外的间接寻址使 CPU 缓存效率降低。
比较元组和列表方法
当使用元组作为list的不可变变体时了解它们的 API 有多相似是很好的。如表 2-1 所示除了一个例外tuple支持所有不涉及添加或删除元素的list方法——tuple缺少__reversed__方法。但是这只是为了优化reversed(my_tuple)可以在没有它的情况下工作。
表 2-1. 在list或tuple中找到的方法和属性为简洁起见省略了对象实现的方法
listtuples.__add__(s2)●●s s2—连接s.__iadd__(s2)●s s2—原地连接s.append(e)●在最后追加一个元素s.clear()●删除所有元素s.__contains__(e)●●e in ss.copy()●列表的浅拷贝s.count(e)●●计算元素出现的次数s.__delitem__(p)●移除位置p处的元素s.extend(it)●从可迭代对象it追加元素s.__getitem__(p)●●s[p]—获取位置p处的元素s.__getnewargs__()●支持使用pickle进行优化的序列化s.index(e)●●查找e第一次出现的位置s.insert(p, e)●在位置p的元素之前插入元素es.__iter__()●●获取迭代器s.__len__()●●len(s)—元素的数量s.__mul__(n)●●s * n—重复连接s.__imul__(n)●s * n—原地重复连接s.__rmul__(n)●●n * s—反向重复连接^(a)s.pop([p])●移除并返回最后一个元素或位置p处的可选元素s.remove(e)●按值移除元素e的第一次出现s.reverse()●原地反转元素的顺序s.__reversed__()●获取从最后到第一个元素的迭代器s.__setitem__(p, e)●s[p] e—将e放在位置p覆盖现有元素^(b)s.sort([key], [reverse])●原地排序可选关键字参数key和reverse(a)反向运算符在第十六章中解释。(b)也用于覆盖子序列。参见赋值给切片。
现在让我们切换到 Python 编程中一个重要的主题元组、列表和可迭代对象解包。
解包序列和可迭代对象
解包很重要因为它避免了不必要的和容易出错的使用索引从序列中提取元素。此外解包可以与任何可迭代对象作为数据源一起使用包括不支持索引表示法([])的迭代器。唯一的要求是可迭代对象在接收端为每个变量只产生一个项除非你使用星号(*)来捕获多余的项如使用*捕获多余的项中所解释的。
解包最明显的形式是并行赋值也就是说将可迭代对象中的项赋值给一个元组变量如下例所示 lax_coordinates (33.9425, -118.408056)latitude, longitude lax_coordinates # unpackinglatitude
33.9425longitude
-118.408056解包的一个优雅应用是在不使用临时变量的情况下交换变量的值 b, a a, b解包的另一个例子是在调用函数时在参数前面加上* divmod(20, 8)
(2, 4)t (20, 8)divmod(*t)
(2, 4)quotient, remainder divmod(*t)quotient, remainder
(2, 4)前面的代码展示了解包的另一个用途允许函数以一种对调用者很方便的方式返回多个值。另一个例子是os.path.split()函数从文件系统路径构建一个元组(path, last_part) import os_, filename os.path.split(/home/luciano/.ssh/id_rsa.pub)filename
id_rsa.pub另一种在解包时只使用部分项的方式是使用*语法我们马上就会看到。
使用*捕获多余的项
使用*args定义函数参数以捕获任意多余的参数是 Python 的一个经典特性。
在 Python 3 中这个想法也被扩展到并行赋值 a, b, *rest range(5)a, b, rest
(0, 1, [2, 3, 4])a, b, *rest range(3)a, b, rest
(0, 1, [2])a, b, *rest range(2)a, b, rest
(0, 1, [])在并行赋值的上下文中*前缀只能应用于一个变量但它可以出现在任何位置 a, *body, c, d range(5)a, body, c, d
(0, [1, 2], 3, 4)*head, b, c, d range(5)head, b, c, d
([0, 1], 2, 3, 4)在函数调用和序列字面量中使用*解包
PEP 448—Additional Unpacking Generalizations引入了更灵活的可迭代对象解包语法在What’s New In Python 3.5中总结得最好。
在函数调用中我们可以多次使用* def fun(a, b, c, d, *rest):
... return a, b, c, d, rest
...fun(*[1, 2], 3, *range(4, 7))
(1, 2, 3, 4, (5, 6))在定义list、tuple或set字面量时也可以使用*如What’s New In Python 3.5中的这些例子所示 *range(4), 4
(0, 1, 2, 3, 4)[*range(4), 4]
[0, 1, 2, 3, 4]{*range(4), 4, *(5, 6, 7)}
{0, 1, 2, 3, 4, 5, 6, 7}PEP 448 为**引入了类似的新语法我们将在Unpacking Mappings中看到。
最后元组解包的一个强大功能是它可以与嵌套结构一起使用。
嵌套解包
解包的目标可以使用嵌套例如(a, b, (c, d))。如果值具有相同的嵌套结构Python 会做正确的事情。示例 2-8 展示了嵌套解包的实际应用。
示例 2-8.解包嵌套元组以访问经度
metro_areas (Tokyo, JP, 36.933, (35.689722, 139.691667)), ![1(Delhi NCR, IN, 21.935, (28.613889, 77.208889)),(Mexico City, MX, 20.142, (19.433333, -99.133333)),(New York-Newark, US, 20.104, (40.808611, -74.020386)),(São Paulo, BR, 19.649, (-23.547778, -46.635833)),
]def main():print(f{:15} | {latitude:9} | {longitude:9})for name, _, _, (lat, lon) in metro_areas: # ②if lon 0: # ③print(f{name:15} | {lat:9.4f} | {lon:9.4f})if __name__ __main__:main()①
每个元组都包含一个有四个字段的记录最后一个字段是一对坐标。
②
通过将最后一个字段赋值给嵌套元组我们解包了坐标。
③
lon 0:测试只选择西半球的城市。
示例 2-8 的输出是 | latitude | longitude
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
São Paulo | -23.5478 | -46.6358解包赋值的目标也可以是一个列表但好的用例很少见。这是我知道的唯一一个如果你有一个数据库查询只返回一条记录例如SQL 代码中有一个LIMIT 1子句那么你可以解包并同时确保只有一个结果代码如下 [record] query_returning_single_row()如果记录只有一个字段你可以直接获取它像这样 [[field]] query_returning_single_row_with_single_field()这两种情况都可以用元组来写但不要忘记单项元组必须用尾随逗号来写的语法怪癖。所以第一个目标应该是(record,)第二个应该是((field,),)。在这两种情况下如果你忘记了逗号你会得到一个无声的错误。³
现在让我们研究模式匹配它支持更强大的序列解包方式。
序列模式匹配
Python 3.10 中最明显的新特性是PEP 634—Structural Pattern Matching: Specification中提议的带有match/case语句的模式匹配。
注意
Python 核心开发者 Carol Willing 在 “What’s New In Python 3.10” 的 “Structural Pattern Matching” 部分中写了关于模式匹配的精彩介绍。你可能需要阅读那个快速概述。在本书中我选择根据模式类型将模式匹配的内容分散在不同的章节中“Pattern Matching with Mappings” 和 “Pattern Matching Class Instances”。一个扩展示例在 “Pattern Matching in lis.py: A Case Study” 中。
这是一个 match/case 处理序列的第一个例子。想象你正在设计一个机器人它接受以单词和数字序列发送的命令如 BEEPER 440 3。在分割成部分并解析数字后你会得到一条像 [BEEPER, 440, 3] 这样的消息。你可以使用如下方法来处理这样的消息
示例 2-9. 一个虚构的 Robot 类的方法 def handle_command(self, message):match message: # ①case [BEEPER, frequency, times]: # ②self.beep(times, frequency)case [NECK, angle]: # ③self.rotate_neck(angle)case [LED, ident, intensity]: # ④self.leds[ident].set_brightness(ident, intensity)case [LED, ident, red, green, blue]: # ⑤self.leds[ident].set_color(ident, red, green, blue)case _: # ⑥raise InvalidCommand(message)①
match 关键字后面的表达式是主题。主题是 Python 将尝试与每个 case 子句中的模式匹配的数据。
②
这个模式匹配任何包含三个元素的序列主题。第一个元素必须是字符串 BEEPER。第二个和第三个元素可以是任何内容它们将按顺序绑定到变量 frequency 和 times。
③
这将匹配任何包含两个元素的主题第一个元素是 NECK。
④
这将匹配一个以 LED 开头的三个元素的主题。如果元素数量不匹配Python 将继续执行下一个 case。
⑤
另一个以 LED 开头的序列模式现在有五个元素包括常量 LED。
⑥
这是默认的 case。它将匹配任何没有匹配前面模式的主题。_ 变量是特殊的我们很快就会看到。
从表面上看match/case 可能类似于 C 语言中的 switch/case 语句 —— 但那只是故事的一半。⁴ match 相对于 switch 的一个关键改进是解构 —— 一种更高级的解包形式。解构是 Python 词汇表中的一个新词但在支持模式匹配的语言如 Scala 和 Elixir的文档中常用。
作为解构的第一个示例示例 2-10 展示了用 match/case 重写的 示例 2-8 的一部分。
示例 2-10. 解构嵌套元组 —— 需要 Python ≥ 3.10
metro_areas [(Tokyo, JP, 36.933, (35.689722, 139.691667)),(Delhi NCR, IN, 21.935, (28.613889, 77.208889)),(Mexico City, MX, 20.142, (19.433333, -99.133333)),(New York-Newark, US, 20.104, (40.808611, -74.020386)),(São Paulo, BR, 19.649, (-23.547778, -46.635833)),
]def main():print(f{:15} | {latitude:9} | {longitude:9})for record in metro_areas:match record: # ①case [name, _, _, (lat, lon)] if lon 0: # ②print(f{name:15} | {lat:9.4f} | {lon:9.4f})①
这个 match 的主题是 record即 metro_areas 中的每个元组。
②
case 子句有两个部分一个模式和一个可选的带有 if 关键字的守卫。
通常序列模式在以下情况下匹配主题 主题是一个序列并且 主题和模式具有相同数量的元素并且 每个对应的元素都匹配包括嵌套元素。
例如示例 2-10 中的模式 [name, _, _, (lat, lon)] 匹配一个包含四个元素的序列最后一个元素必须是一个包含两个元素的序列。
序列模式可以写成元组或列表或者任何嵌套元组和列表的组合但使用哪种语法并不重要在序列模式中方括号和括号的含义相同。我将模式写成带有嵌套 2 元组的列表只是为了避免在 示例 2-10 中重复使用括号。
序列模式可以匹配collections.abc.Sequence的大多数实际或虚拟子类的实例但str、bytes和bytearray除外。
警告
在match/case的上下文中str、bytes和bytearray的实例不会被处理为序列。这些类型的match主题被视为原子值——就像整数 987 被视为一个值而不是一个数字序列。将这三种类型视为序列可能会因意外匹配而导致错误。如果要将这些类型的对象视为序列主题请在match子句中进行转换。例如请参见以下内容中的tuple(phone) match tuple(phone):case [1, *rest]: # North America and Caribbean...case [2, *rest]: # Africa and some territories...case [3 | 4, *rest]: # Europe...在标准库中这些类型与序列模式兼容
list memoryview array.array
tuple range collections.deque与解包不同模式不会解构非序列的可迭代对象如迭代器。
_符号在模式中很特殊它匹配该位置的任何单个项但永远不会绑定到匹配项的值。此外_是唯一可以在模式中多次出现的变量。
你可以使用as关键字将模式的任何部分绑定到一个变量 case [name, _, _, (lat, lon) as coord]:给定主题[Shanghai, CN, 24.9, (31.1, 121.3)]前面的模式将匹配并设置以下变量
变量设置值nameShanghailat31.1lon121.3coord(31.1, 121.3)
我们可以通过添加类型信息来使模式更具体。例如以下模式匹配与前面示例相同的嵌套序列结构但第一项必须是str的实例而 2 元组中的两个项都必须是float的实例 case [str(name), _, _, (float(lat), float(lon))]:提示
表达式str(name)和float(lat)看起来像构造函数调用我们会用它们将name和lat转换为str和float。但在模式的上下文中该语法执行运行时类型检查前面的模式将匹配一个四项序列其中第 0 项必须是str第 3 项必须是一对浮点数。此外第 0 项中的str将绑定到name变量第 3 项中的浮点数将分别绑定到lat和lon。所以尽管str(name)借用了构造函数调用的语法但在模式的上下文中语义完全不同。在模式匹配类实例中介绍了在模式中使用任意类。
另一方面如果我们想匹配任何以str开头并以两个浮点数的嵌套序列结尾的主题序列我们可以这样写 case [str(name), *_, (float(lat), float(lon))]:*_匹配任意数量的项而不将它们绑定到变量。使用*extra而不是*_会将项绑定到extra作为一个包含 0 个或多个项的list。
可选的以if开头的保护子句只在模式匹配时求值并且可以引用模式中绑定的变量如示例 2-10 所示 match record:case [name, _, _, (lat, lon)] if lon 0:print(f{name:15} | {lat:9.4f} | {lon:9.4f})只有在模式匹配且保护表达式为真时才会运行包含print语句的嵌套块。
提示
使用模式进行解构是如此富有表现力以至于有时只有一个case的match就可以使代码更简单。Guido van Rossum 有一个case/match示例集合其中有一个他标题为一个非常深层的可迭代对象和类型匹配与提取。
示例 2-10 并不比示例 2-8 有改进。它只是一个示例用于对比做同一件事的两种方式。下一个示例展示了模式匹配如何有助于清晰、简洁和有效的代码。
解释器中的序列模式匹配
斯坦福大学的 Peter Norvig 编写了lis.py一个 Lisp 编程语言的 Scheme 方言子集的解释器用 132 行优美、易读的 Python 代码实现。我采用了 Norvig 的 MIT 许可源代码并将其更新到 Python 3.10以展示模式匹配。在本节中我们将比较 Norvig 代码的一个关键部分使用if/elif和解包与使用match/case重写的版本。
lis.py的两个主要函数是parse和evaluate。⁵ 解析器接受 Scheme 的括号表达式并返回 Python 列表。这里有两个例子 parse((gcd 18 45))
[gcd, 18, 45]parse(
... (define double
... (lambda (n)
... (* n 2)))
... )
[define, double, [lambda, [n], [*, n, 2]]]求值器接受这样的列表并执行它们。第一个例子是用18和45作为参数调用gcd函数。求值时它计算参数的最大公约数9。第二个例子是定义一个名为double的函数带有一个参数n。函数体是表达式(* n 2)。在 Scheme 中调用函数的结果是函数体中最后一个表达式的值。
我们这里重点关注序列的解构所以我不会解释求值器的动作。想了解更多关于lis.py如何工作的信息请参阅lis.py 中的模式匹配一个案例研究。
示例 2-11 显示了 Norvig 的求值器经过略微修改仅显示序列模式。
示例 2-11. 不使用match/case匹配模式
def evaluate(exp: Expression, env: Environment) - Any:Evaluate an expression in an environment.if isinstance(exp, Symbol): # variable referencereturn env[exp]# ... lines omittedelif exp[0] quote: # (quote exp)(_, x) expreturn xelif exp[0] if: # (if test conseq alt)(_, test, consequence, alternative) expif evaluate(test, env):return evaluate(consequence, env)else:return evaluate(alternative, env)elif exp[0] lambda: # (lambda (parm…) body…)(_, parms, *body) expreturn Procedure(parms, body, env)elif exp[0] define:(_, name, value_exp) expenv[name] evaluate(value_exp, env)# ... more lines omitted注意每个elif子句是如何检查列表的第一个元素然后解包列表忽略第一个元素的。广泛使用解包表明 Norvig 是模式匹配的粉丝但他最初是为 Python 2 编写那段代码的尽管它现在适用于任何 Python 3。
使用 Python ≥ 3.10 中的match/case我们可以重构evaluate如示例 2-12 所示。
示例 2-12. 使用match/case进行模式匹配——需要 Python ≥ 3.10
def evaluate(exp: Expression, env: Environment) - Any:Evaluate an expression in an environment.match exp:# ... lines omittedcase [quote, x]: # ①return xcase [if, test, consequence, alternative]: # ②if evaluate(test, env):return evaluate(consequence, env)else:return evaluate(alternative, env)case [lambda, [*parms], *body] if body: # ③return Procedure(parms, body, env)case [define, Symbol() as name, value_exp]: # ④env[name] evaluate(value_exp, env)# ... more lines omittedcase _: # ⑤raise SyntaxError(lispstr(exp))①
匹配是否是以quote开头的两元素序列。
②
匹配是否是以if开头的四元素序列。
③
匹配是否是以lambda开头的三个或更多元素的序列。guard 确保body不为空。
④
匹配是否是以define开头的三元素序列后面跟着一个Symbol的实例。
⑤
将所有的case语句写一个兜底是一个很好的实践。在这个例子中如果exp不匹配任何模式表达式就是有问题的我会抛出SyntaxError。
如果没有兜底语句当主体不匹配任何 case 时整个match语句都不会执行任何操作——而这可能是一个静默的失败。
Norvig 故意避免在lis.py中进行错误检查以保持代码易于理解。使用模式匹配我们可以添加更多检查同时保持可读性。例如在define模式中原始代码不确保name是Symbol的实例——这需要一个if块、一个isinstance调用和更多代码。示例 2-12 比示例 2-11 更简洁、更安全。
lambda 的替代模式
这是 Scheme 中lambda的语法使用语法约定后缀…表示元素可能出现零次或多次
(lambda (parms…) body1 body2…)lambda case lambda的一个简单模式是 case [lambda, parms, *body] if body:然而这会匹配parms位置的任何值包括这个无效主体中的第一个x
[lambda, x, [*, x, 2]]Scheme 中lambda关键字后面的嵌套列表包含函数的形式参数名称即使它只有一个元素也必须是一个列表。如果函数不接受任何参数它也可以是一个空列表——就像 Python 的random.random()。
在 示例 2-12 中我使用嵌套序列模式使 lambda 模式更加安全 case [lambda, [*parms], *body] if body:return Procedure(parms, body, env)在序列模式中* 在每个序列中只能出现一次。这里我们有两个序列外部序列和内部序列。
在 parms 周围添加 [*] 字符使模式看起来更像它所处理的 Scheme 语法并为我们提供了额外的结构检查。
函数定义的简写语法
Scheme 有一种替代的 define 语法可以在不使用嵌套 lambda 的情况下创建命名函数。语法如下
(define (name parm…) body1 body2…)define 关键字后面跟着一个列表其中包含新函数的 name 以及零个或多个参数名称。在该列表之后是函数体其中包含一个或多个表达式。
将这两行添加到 match 中就可以完成实现 case [define, [Symbol() as name, *parms], *body] if body:env[name] Procedure(parms, body, env)我会将该 case 放在 示例 2-12 中另一个 define case 之后。在这个示例中define case 的顺序无关紧要因为没有主体可以同时匹配这两个模式在原始的 define case 中第二个元素必须是 Symbol但在用于函数定义的 define 简写中它必须是以 Symbol 开头的序列。
现在考虑一下如果没有 示例 2-11 中模式匹配的帮助为第二个 define 语法添加支持需要做多少工作。match 语句比类 C 语言中的 switch 做的事情要多得多。
模式匹配是声明式编程的一个例子代码描述了你想要匹配的什么而不是如何匹配。代码的形状遵循数据的形状如 表 2-2 所示。
表 2-2. 一些 Scheme 语法形式和用于处理它们的 case 模式
Scheme 语法序列模式(quote exp)[quote, exp](if test conseq alt)[if, test, conseq, alt](lambda (parms…) body1 body2…)[lambda, [*parms], *body] if body(define name exp)[define, Symbol() as name, exp](define (name parms…) body1 body2…)[define, [Symbol() as name, *parms], *body] if body
我希望用模式匹配重构 Norvig 的 evaluate 能让你相信 match/case 可以使你的代码更具可读性和安全性。
注意
在lis.py 中的模式匹配案例研究一节中当我们回顾 evaluate 中完整的 match/case 示例时我们将看到更多关于 lis.py 的内容。如果你想了解更多关于 Norvig 的 lis.py请阅读他精彩的文章如何用 Python 编写一个 Lisp 解释器。
以上就是我们对序列解包、解构和模式匹配的首次介绍。我们将在后面的章节中介绍其他类型的模式。
每个 Python 程序员都知道可以使用 s[a:b] 语法对序列进行切片。我们现在来看一些关于切片的鲜为人知的事实。
切片
Python 中 list、tuple、str 以及所有序列类型的一个共同特性是支持切片操作其功能比大多数人意识到的要强大得多。
在本节中我们描述了这些高级切片形式的使用。它们在用户定义类中的实现将在 第十二章中介绍这与我们在本书这一部分中介绍现成可用的类并在 第 III 部分中创建新类的理念保持一致。
为什么切片和范围要排除最后一项
在 Python、C 语言以及许多其他语言中使用的基于 0 的索引与 Python 中切片和范围排除最后一项的约定能够很好地配合。这个约定有一些方便的特性 当只给出停止位置时很容易看出切片或范围的长度range(3) 和 my_list[:3] 都会产生三个项目。 当给出起始位置和停止位置时计算切片或范围的长度很容易只需计算 stop - start。 在任意索引x处轻松将序列分为两部分不重叠只需获取my_list[:x]和my_list[x:]。例如 l [10, 20, 30, 40, 50, 60]l[:2] # split at 2
[10, 20]l[2:]
[30, 40, 50, 60]l[:3] # split at 3
[10, 20, 30]l[3:]
[40, 50, 60]这种约定的最佳论据是由荷兰计算机科学家 Edsger W. Dijkstra 撰写的请参阅“进一步阅读”中的最后一个参考文献。
现在让我们仔细看看 Python 如何解释切片表示法。
切片对象
这并不是秘密但值得重复一遍s[a:b:c]可用于指定步长或步进c导致生成的切片跳过项目。步长也可以是负数返回相反顺序的项目。三个示例清楚地说明了这一点 s bicycles[::3]
byes[::-1]
elcycibs[::-2]
eccb另一个示例在第一章中展示当我们使用deck[12::13]来获取未洗牌牌组中的所有 A 时 deck[12::13]
[Card(rankA, suitspades), Card(rankA, suitdiamonds),
Card(rankA, suitclubs), Card(rankA, suithearts)]符号a:b:c仅在作为索引或下标运算符使用时在[]内有效并产生一个切片对象slice(a, b, c)。正如我们将在“切片工作原理”中看到的为了评估表达式seq[start:stop:step]Python 调用seq.__getitem__(slice(start, stop, step))。即使您不是在实现自己的序列类型了解切片对象也是有用的因为它允许您为切片分配名称就像电子表格允许命名单元格范围一样。
假设您需要解析像示例 2-13 中显示的发票那样的平面文件数据。您可以为它们命名而不是在代码中填充硬编码切片。看看这如何使示例末尾的for循环变得更易读。
示例 2-13。来自平面文件发票的行项目 invoice
... 0.....6.................................40........52...55........
... 1909 Pimoroni PiBrella $17.50 3 $52.50
... 1489 6mm Tactile Switch x20 $4.95 2 $9.90
... 1510 Panavise Jr. - PV-201 $28.00 1 $28.00
... 1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95
... SKU slice(0, 6)DESCRIPTION slice(6, 40)UNIT_PRICE slice(40, 52)QUANTITY slice(52, 55)ITEM_TOTAL slice(55, None)line_items invoice.split(\n)[2:]for item in line_items:
... print(item[UNIT_PRICE], item[DESCRIPTION])
...$17.50 Pimoroni PiBrella$4.95 6mm Tactile Switch x20$28.00 Panavise Jr. - PV-201$34.95 PiTFT Mini Kit 320x240当我们讨论在“向量接收器#2可切片序列”中创建自己的集合时我们将回到slice对象。与此同时从用户角度来看切片包括额外的功能如多维切片和省略号...表示法。继续阅读。
多维切片和省略号
[]运算符还可以接受用逗号分隔的多个索引或切片。处理[]运算符的__getitem__和__setitem__特殊方法简单地将a[i, j]中的索引作为元组接收。换句话说为了评估a[i, j]Python 调用a.__getitem__((i, j))。
例如在外部 NumPy 包中使用可以使用语法a[i, j]获取二维numpy.ndarray的项目并使用表达式a[m:n, k:l]获取二维切片。本章后面的示例 2-22 展示了此表示法的用法。
除了memoryviewPython 中的内置序列类型是一维的因此它们仅支持一个索引或切片而不是它们的元组。⁶
省略号——用三个完整的句号...而不是…Unicode U2026编写——被 Python 解析器识别为一个标记。它是Ellipsis对象的别名ellipsis类的单个实例。⁷因此它可以作为参数传递给函数并作为切片规范的一部分如f(a, ..., z)或a[i:...]。NumPy 在对许多维度的数组进行切片时使用...作为快捷方式例如如果x是一个四维数组则x[i, ...]是x[i, :, :, :,]的快捷方式。查看“NumPy 快速入门”以了解更多信息。
在撰写本文时我不知道 Python 标准库中使用Ellipsis或多维索引和切片的用途。如果您发现了请告诉我。这些语法特性存在是为了支持用户定义的类型和扩展如 NumPy。
切片不仅有助于从序列中提取信息它们还可以用于就地更改可变序列即不需要从头开始重建它们。
分配到切片
可变序列可以通过在赋值语句的左侧使用切片表示法或作为del语句的目标来进行嫁接、切除和其他修改。接下来的几个示例展示了这种表示法的强大之处 l list(range(10))l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] l[2:5] [20, 30]l
[0, 1, 20, 30, 5, 6, 7, 8, 9] del l[5:7]l
[0, 1, 20, 30, 5, 8, 9] l[3::2] [11, 22]l
[0, 1, 20, 11, 5, 22, 9] l[2:5] 100 # ①
Traceback (most recent call last):File stdin, line 1, in module
TypeError: can only assign an iterablel[2:5] [100]l
[0, 1, 100, 22, 9]①
当赋值的目标是一个切片时右侧必须是一个可迭代对象即使它只有一个项目。
每个程序员都知道连接序列是一种常见操作。Python 入门教程解释了如何使用和*来实现这一目的但它们的工作原理有一些微妙之处我们接下来会详细介绍。
使用和*处理序列
Python 程序员期望序列支持和*。通常的两个操作数必须是相同的序列类型并且它们都不会被修改但作为连接结果会创建一个相同类型的新序列。
要连接同一序列的多个副本可以将其乘以一个整数。同样会创建一个新序列 l [1, 2, 3]l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]5 * abcd
abcdabcdabcdabcdabcd和*都会创建一个新对象并且不会改变它们的操作数。
警告
当包含可变项的序列a尝试执行a * n时要小心因为结果可能会让你感到惊讶。例如尝试将一个列表的列表初始化为my_list [[]] * 3将导致一个包含对同一内部列表的三个引用的列表这可能不是你想要的。
下一节将介绍尝试使用*初始化列表的陷阱。
构建列表的列表
有时我们需要使用一定数量的嵌套列表来初始化一个列表例如将学生分配到团队列表中或表示游戏棋盘上的方块。最好的方法是使用列表推导式就像示例 2-14 中那样。
示例 2-14. 一个包含三个长度为 3 的列表的列表可以表示一个井字棋棋盘 board [[_] * 3 for i in range(3)] # ①board
[[_, _, _], [_, _, _], [_, _, _]] board[1][2] X # ②board
[[_, _, _], [_, _, X], [_, _, _]]①
创建一个包含三个每个三个项目的列表的列表。检查结构。
②
在第 1 行第 2 列放置一个标记然后查看结果。
一个诱人但错误的快捷方式是像示例 2-15 那样做。
示例 2-15. 一个包含对同一列表的三个引用的列表是无用的 weird_board [[_] * 3] * 3 # ①weird_board
[[_, _, _], [_, _, _], [_, _, _]] weird_board[1][2] O # ②weird_board
[[_, _, O], [_, _, O], [_, _, O]]①
外部列表由三个对同一内部列表的引用组成。当它保持不变时一切似乎都正确。
②
在第 1 行第 2 列放置一个标记揭示所有行都是指向同一对象的别名。
示例 2-15 的问题在于本质上它的行为类似于以下代码
row [_] * 3
board []
for i in range(3):board.append(row) # ①①
相同的row被三次附加到board上。
另一方面来自示例 2-14 的列表推导式等同于以下代码 board []for i in range(3):
... row [_] * 3 # ①
... board.append(row)
...board
[[_, _, _], [_, _, _], [_, _, _]]board[2][0] Xboard # ②
[[_, _, _], [_, _, _], [X, _, _]]①
每次迭代都会构建一个新的row并将其附加到board上。
②
只有第 2 行被更改这是预期的结果。
提示
如果本节中的问题或解决方案对您来说不清楚请放心。第六章旨在澄清引用和可变对象的机制和陷阱。
到目前为止我们已经讨论了在序列中使用普通的和*运算符但还有和*运算符它们会根据目标序列的可变性产生非常不同的结果。接下来的部分将解释其工作原理。
序列的增强赋值
增强赋值运算符和*的行为取决于第一个操作数。为了简化讨论我们将首先关注增强加法()但这些概念也适用于*和其他增强赋值运算符。
使 生效的特殊方法是 __iadd__代表“就地加法”。
然而如果未实现 __iadd__Python 将退而求其次调用 __add__。考虑这个简单的表达式 a b如果 a 实现了 __iadd__那么将会调用它。对于可变序列例如 list、bytearray、array.arraya 将会就地更改即效果类似于 a.extend(b)。然而当 a 没有实现 __iadd__ 时表达式 a b 的效果与 a a b 相同首先计算表达式 a b产生一个新对象然后将其绑定到 a。换句话说取决于是否有 __iadd__绑定到 a 的对象的标识可能会改变或不会改变。
一般来说对于可变序列可以肯定会实现 __iadd__并且 会就地发生。对于不可变序列显然不可能发生这种情况。
我刚刚写的关于 的内容也适用于 *它是通过 __imul__ 实现的。__iadd__ 和 __imul__ 特殊方法在 第十六章 中有讨论。这里展示了对可变序列和不可变序列使用 * 的演示 l [1, 2, 3]id(l)
4311953800 # ①l * 2l
[1, 2, 3, 1, 2, 3] id(l)
4311953800 # ②t (1, 2, 3)id(t)
4312681568 # ③t * 2id(t)
4301348296 # ④①
初始列表的 ID。
②
经过乘法运算后列表仍然是同一个对象只是附加了新项。
③
初始元组的 ID。
④
经过乘法运算后创建了一个新的元组。
对不可变序列的重复连接是低效的因为解释器不仅仅是附加新项还必须复制整个目标序列以创建一个新的序列其中包含新附加的项。⁸
我们已经看到了 的常见用法。下一节将展示一个引人入胜的特殊情况突显了在元组上“不可变”在实际中意味着什么。
A 赋值谜题
尝试在不使用控制台的情况下回答评估 示例 2-16 中的两个表达式的结果是什么⁹
示例 2-16. 一个谜题 t (1, 2, [30, 40])t[2] [50, 60]接下来会发生什么选择最佳答案 t 变成了 (1, 2, [30, 40, 50, 60])。 引发 TypeError消息为 tuple object does not support item assignment。 无。 A 和 B 都是。
当我看到这个时我非常确定答案是 B但实际上是 D“A 和 B 都是”示例 2-17 是来自 Python 3.9 控制台的实际输出。¹⁰
示例 2-17. 意外结果项目 t2 被更改 并且 引发异常 t (1, 2, [30, 40])t[2] [50, 60]
Traceback (most recent call last):File stdin, line 1, in module
TypeError: tuple object does not support item assignmentt
(1, 2, [30, 40, 50, 60])在线 Python Tutor 是一个很棒的在线工具可以详细展示 Python 的工作原理。图 2-5 是两个截图的组合显示了来自 示例 2-17 的元组 t 的初始状态和最终状态。 图 2-5. 元组赋值谜题的初始状态和最终状态由在线 Python Tutor 生成的图表。
当你查看 Python 为表达式 s[a] b 生成的字节码时示例 2-18就会清楚这是如何发生的。
示例 2-18. 表达式 s[a] b 的字节码 dis.dis(s[a] b)1 0 LOAD_NAME 0 (s) 3 LOAD_NAME 1 (a) 6 DUP_TOP_TWO 7 BINARY_SUBSCR # ①8 LOAD_NAME 2 (b) 11 INPLACE_ADD # ②12 ROT_THREE 13 STORE_SUBSCR # ③14 LOAD_CONST 0 (None) 17 RETURN_VALUE①
将 s[a] 的值放在 TOS栈顶上。
②
执行 TOS b。如果 TOS 指向一个可变对象就像在 示例 2-17 中的列表那么这将成功。
③
将 s[a] TOS。如果 s 是不可变的例如 示例 2-17 中的元组 t则此操作失败。
这个例子是一个非常特殊的情况在使用 Python 20 年中我从未见过这种奇怪的行为实际上影响到任何人。
我从中得到了三个教训 避免将可变项放入元组中。 增强赋值不是一个原子操作——我们刚刚看到它在完成部分工作后抛出异常。 检查 Python 字节码并不太困难而且可以帮助我们了解底层发生了什么。
在见识了使用和*进行连接的微妙之后我们可以将话题转向另一个与序列相关的重要操作排序。
list.sort与内置的sorted的比较
list.sort方法原地对列表进行排序即不创建副本。它返回None以提醒我们它改变了接收者¹¹并且没有创建新列表。这是一个重要的 Python API 约定在原地更改对象的函数或方法应该返回None以明确告诉调用者接收者已被更改没有创建新对象。例如random.shuffle(s)函数也表现出类似的行为它原地对可变序列s进行洗牌并返回None。
注意
返回None以表示原地更改的约定存在一个缺点我们无法级联调用这些方法。相反返回新对象的方法例如所有str方法可以以流畅接口风格级联。请参阅维基百科的“流畅接口”条目以进一步描述这个主题。
相反内置函数sorted创建一个新列表并返回它。它接受任何可迭代对象作为参数包括不可变序列和生成器参见第十七章。无论给sorted的可迭代对象的类型是什么它总是返回一个新创建的列表。
list.sort和sorted都接受两个可选的、仅限关键字的参数
reverse
如果为True则按降序即通过反转项目的比较返回项目。默认值为False。
key
一个参数函数将被应用于每个项目以生成其排序键。例如当对字符串列表进行排序时可以使用keystr.lower执行不区分大小写的排序keylen将按字符长度对字符串进行排序。默认是恒等函数即比较项目本身。
提示
您还可以在min()和max()内置函数以及标准库中的其他函数例如itertools.groupby()和heapq.nlargest()中使用可选的关键字参数key。
这里有一些示例来澄清这些函数和关键字参数的使用。这些示例还演示了 Python 的排序算法是稳定的即它保留了相等比较的项目的相对顺序¹² fruits [grape, raspberry, apple, banana]sorted(fruits)
[apple, banana, grape, raspberry] # ①fruits
[grape, raspberry, apple, banana] # ②sorted(fruits, reverseTrue)
[raspberry, grape, banana, apple] # ③sorted(fruits, keylen)
[grape, apple, banana, raspberry] # ④sorted(fruits, keylen, reverseTrue)
[raspberry, banana, grape, apple] # ⑤fruits
[grape, raspberry, apple, banana] # ⑥fruits.sort() # ⑦fruits
[apple, banana, grape, raspberry] # ⑧①
这将产生一个按字母顺序排序的新字符串列表。¹³
②
检查原始列表我们看到它没有改变。
③
这是之前的“字母顺序”但是反转了。
④
一个按长度排序的新字符串列表。由于排序算法是稳定的“葡萄”和“苹果”长度均为 5按原始顺序排列。
⑤
这些是按长度降序排序的字符串。这不是前一个结果的反转因为排序是稳定的所以“葡萄”再次出现在“苹果”之前。
⑥
到目前为止原始fruits列表的顺序没有改变。
⑦
这会原地对列表进行排序并返回None控制台省略了这一点。
⑧
现在fruits已经排序。
警告
默认情况下Python 按字符代码按字典顺序对字符串进行排序。这意味着 ASCII 大写字母将排在小写字母之前非 ASCII 字符不太可能以合理的方式排序。“对 Unicode 文本进行排序”介绍了按人类期望的方式对文本进行排序的正确方法。
一旦您的序列被排序它们可以被非常高效地搜索。Python 标准库的bisect模块中已经提供了二分搜索算法。该模块还包括bisect.insort函数您可以使用它来确保您的排序序列保持排序。您可以在fluentpython.com伴随网站的“使用 Bisect 管理有序序列”文章中找到bisect模块的图解介绍。
到目前为止在本章中所看到的大部分内容都适用于一般序列而不仅仅是列表或元组。Python 程序员有时会过度使用list类型因为它非常方便——我知道我曾经这样做过。例如如果您正在处理大量数字列表应考虑改用数组。本章的其余部分致力于列表和元组的替代方案。
当列表不是答案时
list类型灵活且易于使用但根据具体要求有更好的选择。例如当需要处理数百万个浮点值时array可以节省大量内存。另一方面如果您不断地向列表的两端添加和删除项目那么了解deque双端队列是一种更高效的 FIFO¹⁴数据结构是很有用的。
提示
如果您的代码经常检查集合中是否存在某个项目例如item in my_collection请考虑使用set代替my_collection特别是如果它包含大量项目。集合针对快速成员检查进行了优化。它们也是可迭代的但它们不是序列因为集合项的顺序是未指定的。我们将在第三章中介绍它们。
在本章的其余部分中我们将讨论可以在许多情况下替代列表的可变序列类型从数组开始。
数组
如果列表只包含数字array.array是更高效的替代品。数组支持所有可变序列操作包括.pop、.insert和.extend以及用于快速加载和保存的附加方法如.frombytes和.tofile。
Python 数组与 C 数组一样精简。如图 2-1 所示float值的array不保存完整的float实例而只保存代表其机器值的打包字节——类似于 C 语言中的double数组。创建array时您提供一个类型码一个用于确定数组中每个项目存储的基础 C 类型的字母。例如b是 C 中称为signed char的类型码一个范围从-128 到 127 的整数。如果创建一个array(b)那么每个项目将存储在一个字节中并解释为整数。对于大量数字序列这可以节省大量内存。Python 不会让您放入与数组类型不匹配的任何数字。
示例 2-19 展示了创建、保存和加载一个包含 1000 万个浮点随机数的数组。
示例 2-19. 创建、保存和加载大量浮点数的数组 from array import array # ①from random import randomfloats array(d, (random() for i in range(10**7))) # ②floats[-1] # ③
0.07802343889111107 fp open(floats.bin, wb)floats.tofile(fp) # ④fp.close()floats2 array(d) # ⑤fp open(floats.bin, rb)floats2.fromfile(fp, 10**7) # ⑥fp.close()floats2[-1] # ⑦
0.07802343889111107 floats2 floats # ⑧
True①
导入array类型。
②
从任何可迭代对象在本例中是生成器表达式创建双精度浮点数类型码d的数组。
③
检查数组中的最后一个数字。
④
将数组保存到二进制文件。
⑤
创建一个空的双精度数组。
⑥
从二进制文件中读取 1000 万个数字。
⑦
检查数组中的最后一个数字。
⑧
验证数组内容是否匹配。
如您所见array.tofile和array.fromfile非常易于使用。如果尝试示例您会注意到它们也非常快速。一个快速实验显示array.fromfile从使用array.tofile创建的二进制文件中加载 1000 万个双精度浮点数大约需要 0.1 秒。这几乎比从文本文件中读取数字快 60 倍后者还涉及使用内置的float解析每一行。使用array.tofile保存的速度大约比在文本文件中每行写一个浮点数快七倍。此外具有 1000 万个双精度浮点数的二进制文件的大小为 80000000 字节每个双精度浮点数 8 字节零开销而相同数据的文本文件大小为 181515739 字节。
对于表示二进制数据的数字数组的特定情况例如光栅图像Python 中有bytes和bytearray类型详见第四章。
我们通过表 2-3 总结了数组部分比较了list和array.array的特性。
表 2-3。list或array中找到的方法和属性为简洁起见省略了已弃用的数组方法和对象也实现的方法
列表数组s.__add__(s2)●●s s2—连接s.__iadd__(s2)●●s s2—原地连接s.append(e)●●在最后一个元素后追加一个元素s.byteswap()●交换数组中所有项目的字节以进行字节顺序转换s.clear()●删除所有项目s.__contains__(e)●●e in ss.copy()●列表的浅拷贝s.__copy__()●支持copy.copys.count(e)●●计算元素的出现次数s.__deepcopy__()●优化支持copy.deepcopys.__delitem__(p)●●移除位置p处的项目s.extend(it)●●从可迭代对象it中追加项目s.frombytes(b)●从字节序列中解释为打包的机器值追加项目s.fromfile(f, n)●从解释为打包的机器值的二进制文件f追加n个项目s.fromlist(l)●从列表追加项目如果一个导致TypeError则不追加任何项目s.__getitem__(p)●●s[p]—获取位置处的项目或切片s.index(e)●●查找e的第一个出现位置s.insert(p, e)●●在位置p的项目之前插入元素es.itemsize●每个数组项的字节长度s.__iter__()●●获取迭代器s.__len__()●●len(s)—项目数s.__mul__(n)●●s * n—重复连接s.__imul__(n)●●s * n—原地重复连接s.__rmul__(n)●●n * s—反向重复连接^(a)s.pop([p])●●移除并返回位置p处的项目默认为最后一个s.remove(e)●●通过值删除元素e的第一个出现s.reverse()●●原地反转项目的顺序s.__reversed__()●获取从最后到第一个扫描项目的迭代器s.__setitem__(p, e)●●s[p] e—将e放在位置p覆盖现有项目或切片s.sort([key], [reverse])●使用可选关键字参数key和reverse原地对项目进行排序s.tobytes()●以bytes对象的形式返回打包的机器值s.tofile(f)●将项目保存为打包的机器值到二进制文件fs.tolist()●以list中的数值对象形式返回项目s.typecode●用于标识项目的 C 类型的单字符字符串^(a) 反向运算符在 第十六章 中有解释。
提示
截至 Python 3.10array 类型没有像 list.sort() 那样的原地 sort 方法。如果需要对数组进行排序请使用内置的 sorted 函数重新构建数组
a array.array(a.typecode, sorted(a))要在向数组添加项目时保持已排序数组的排序请使用 bisect.insort 函数。
如果您经常使用数组并且不了解 memoryview那么您会错过很多。请看下一个主题。
内存视图
内置的 memoryview 类是一个共享内存序列类型允许您处理数组的切片而无需复制字节。它受到 NumPy 库的启发我们将在 “NumPy” 中讨论。NumPy 的首席作者 Travis Oliphant 对于何时应该使用 memoryview 的问题的回答是这样的“何时应该使用 memoryview” 内存视图本质上是 Python 中的一个广义 NumPy 数组结构不涉及数学。它允许您在不复制字节的情况下在数据结构之间共享内存例如 PIL 图像、SQLite 数据库、NumPy 数组等。这对于大型数据集非常重要。 使用类似于 array 模块的符号memoryview.cast 方法允许您更改多个字节的读取或写入方式而无需移动位。memoryview.cast 总是返回另一个共享相同内存的 memoryview 对象。
示例 2-20 展示了如何在相同的 6 个字节数组上创建替代视图以便将其视为 2×3 矩阵或 3×2 矩阵进行操作。
示例 2-20. 将 6 个字节的内存处理为 1×6、2×3 和 3×2 视图 from array import arrayoctets array(B, range(6)) # ①m1 memoryview(octets) # ②m1.tolist()
[0, 1, 2, 3, 4, 5] m2 m1.cast(B, [2, 3]) # ③m2.tolist()
[[0, 1, 2], [3, 4, 5]] m3 m1.cast(B, [3, 2]) # ④m3.tolist()
[[0, 1], [2, 3], [4, 5]] m2[1,1] 22 # ⑤m3[1,1] 33 # ⑥octets # ⑦
array(B, [0, 1, 2, 33, 22, 5])①
构建包含 6 个字节的数组类型码为 B。
②
从该数组构建 memoryview然后将其导出为列表。
③
从先前的 memoryview 创建新的 memoryview但具有 2 行和 3 列。
④
另一个 memoryview现在有 3 行和 2 列。
⑤
在 m2 的第 1 行、第 1 列覆盖字节为 22。
⑥
在 m3 的第 1 行、第 1 列覆盖字节为 33。
⑦
显示原始数组证明内存在 octets、m1、m2 和 m3 之间共享。
memoryview 的强大之处也可以用来损坏。示例 2-21 展示了如何更改 16 位整数数组中一个项目的单个字节。
示例 2-21. 通过修改一个字节来更改 16 位整数数组项的值 numbers array.array(h, [-2, -1, 0, 1, 2])memv memoryview(numbers) # ①len(memv)
5 memv[0] # ②
-2 memv_oct memv.cast(B) # ③memv_oct.tolist() # ④
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0] memv_oct[5] 4 # ⑤numbers
array(h, [-2, -1, 1024, 1, 2]) # ⑥①
从包含 5 个 16 位有符号整数的数组类型码为 h构建 memoryview。
②
memv 在数组中看到相同的 5 个项目。
③
通过将 memv 的元素转换为字节类型码为 B来创建 memv_oct。
④
将 memv_oct 的元素导出为包含 10 个字节的列表以供检查。
⑤
将值 4 分配给字节偏移 5。
⑥
注意 numbers 的变化2 字节无符号整数的最高有效字节中的 4 是 1024。
注意
您将在 fluentpython.com 上找到使用 struct 包检查 memoryview 的示例“使用 struct 解析二进制记录”。
同时如果您在数组中进行高级数值处理应该使用 NumPy 库。我们将立即简要介绍它们。
NumPy
在本书中我强调了 Python 标准库中已经存在的内容以便您能充分利用它。但是 NumPy 如此强大值得一提。
对于高级的数组和矩阵操作NumPy 是 Python 在科学计算应用中变得流行的原因。NumPy 实现了多维、同质数组和矩阵类型不仅保存数字还保存用户定义的记录并提供高效的逐元素操作。
SciPy 是一个库建立在 NumPy 之上提供许多来自线性代数、数值微积分和统计学的科学计算算法。SciPy 快速可靠因为它利用了来自Netlib Repository的广泛使用的 C 和 Fortran 代码库。换句话说SciPy 为科学家提供了最佳的两种选择交互式提示符和高级 Python API以及在 C 和 Fortran 中优化的工业强度数值计算函数。
作为一个非常简短的 NumPy 演示示例 2-22 展示了一些关于二维数组的基本操作。
示例 2-22。在numpy.ndarray中进行行和列的基本操作 import numpy as np # ①a np.arange(12) # ②a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) type(a)
class numpy.ndarray a.shape # ③
(12,) a.shape 3, 4 # ④a
array([[ 0, 1, 2, 3],[ 4, 5, 6, 7], [ 8, 9, 10, 11]]) a[2] # ⑤
array([ 8, 9, 10, 11]) a[2, 1] # ⑥
9 a[:, 1] # ⑦
array([1, 5, 9]) a.transpose() # ⑧
array([[ 0, 4, 8],[ 1, 5, 9], [ 2, 6, 10], [ 3, 7, 11]])①
导入 NumPy在安装后不在 Python 标准库中。按照惯例将numpy导入为np。
②
构建并检查一个包含整数0到11的numpy.ndarray。
③
检查数组的维度这是一个一维的包含 12 个元素的数组。
④
改变数组的形状增加一个维度然后检查结果。
⑤
获取索引为2的行。
⑥
获取索引为2, 1的元素。
⑦
获取索引为1的列。
⑧
通过转置交换列和行创建一个新的数组。
NumPy 还支持用于加载、保存和操作numpy.ndarray的高级操作 import numpyfloats numpy.loadtxt(floats-10M-lines.txt) # ①floats[-3:] # ②
array([ 3016362.69195522, 535281.10514262, 4566560.44373946]) floats * .5 # ③floats[-3:]
array([ 1508181.34597761, 267640.55257131, 2283280.22186973]) from time import perf_counter as pc # ④t0 pc(); floats / 3; pc() - t0 # ⑤
0.03690556302899495 numpy.save(floats-10M, floats) # ⑥floats2 numpy.load(floats-10M.npy, r) # ⑦floats2 * 6floats2[-3:] # ⑧
memmap([ 3016362.69195522, 535281.10514262, 4566560.44373946])①
从文本文件中加载 1000 万个浮点数。
②
使用序列切片表示法检查最后三个数字。
③
将floats数组中的每个元素乘以.5然后再次检查最后三个元素。
④
导入高分辨率性能测量计时器自 Python 3.3 起可用。
⑤
将每个元素除以3对于 1000 万个浮点数经过的时间不到 40 毫秒。
⑥
将数组保存为*.npy*二进制文件。
⑦
将数据作为内存映射文件加载到另一个数组中这允许对数组的切片进行高效处理即使它不能完全放入内存中。
⑧
将每个元素乘以6后检查最后三个元素。
这只是一个开胃菜。
NumPy 和 SciPy 是强大的库是其他出色工具的基础比如 Pandas — 实现了可以容纳非数值数据的高效数组类型并提供了许多不同格式的导入/导出功能如 .csv、.xls、SQL dumps、HDF5 等 — 以及 scikit-learn目前是最广泛使用的机器学习工具集。大多数 NumPy 和 SciPy 函数是用 C 或 C 实现的并且可以利用所有 CPU 核心因为它们释放了 Python 的 GIL全局解释器锁。Dask 项目支持在机器群集上并行处理 NumPy、Pandas 和 scikit-learn。这些包值得写一整本书来介绍。但这不是那本书。但是没有至少简要介绍 NumPy 数组的 Python 序列概述是不完整的。
在查看了平面序列 — 标准数组和 NumPy 数组之后我们现在转向一组完全不同的替代品用于替代普通的 list队列。
Deques 和其他队列
.append 和 .pop 方法使得 list 可以用作堆栈或队列如果使用 .append 和 .pop(0)则获得 FIFO 行为。但是在列表头部0 索引端插入和删除是昂贵的因为整个列表必须在内存中移动。
类 collections.deque 是一个线程安全的双端队列旨在快速从两端插入和删除。如果需要保留“最近看到的项目”列表或类似内容deque 也是一个不错的选择因为 deque 可以是有界的 — 即创建时具有固定的最大长度。如果有界 deque 已满在添加新项目时它会从相反端丢弃一个项目。示例 2-23 展示了在 deque 上执行的一些典型操作。
示例 2-23. 使用 deque from collections import dequedq deque(range(10), maxlen10) # ①dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen10) dq.rotate(3) # ②dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen10) dq.rotate(-4)dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen10) dq.appendleft(-1) # ③dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen10) dq.extend([11, 22, 33]) # ④dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen10) dq.extendleft([10, 20, 30, 40]) # ⑤dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen10)①
可选的 maxlen 参数设置了此 deque 实例中允许的最大项目数这将设置一个只读的 maxlen 实例属性。
②
使用 n 0 旋转会从右端获取项目并将其前置到左端当 n 0 时项目从左端获取并附加到右端。
③
向已满的 dequelen(d) d.maxlen添加元素会丢弃另一端的项目请注意下一行中的 0 被丢弃了。
④
向右侧添加三个项目会推出最左侧的 -1、1 和 2。
⑤
请注意extendleft(iter) 的工作方式是将 iter 参数的每个连续项目附加到 deque 的左侧因此项目的最终位置是反转的。
Table 2-4 比较了 list 和 deque 中特定的方法删除了也出现在 object 中的方法。
请注意deque 实现了大多数 list 方法并添加了一些特定于其设计的方法如 popleft 和 rotate。但是存在隐藏成本从 deque 中间删除项目不够快。它真正优化于从两端附加和弹出。
append 和 popleft 操作是原子性的因此在多线程应用程序中deque 可以安全地用作 FIFO 队列无需使用锁。
表 2-4. 在 list 或 deque 中实现的方法省略了那些也由 object 实现的方法
listdeques.__add__(s2)●s s2—连接s.__iadd__(s2)●●s s2—原地连接s.append(e)●●向右侧附加一个元素在最后之后s.appendleft(e)●向左侧附加一个元素在第一个之前s.clear()●●删除所有项目s.__contains__(e)●e in ss.copy()●列表的浅复制s.__copy__()●支持 copy.copy浅复制s.count(e)●●计算元素出现的次数s.__delitem__(p)●●删除位置 p 处的项目s.extend(i)●●将可迭代对象 i 中的项目添加到右侧s.extendleft(i)●将可迭代对象 i 中的项目添加到左侧s.__getitem__(p)●●s[p]—获取位置处的项目或切片s.index(e)●查找第一个出现的 e 的位置s.insert(p, e)●在位置 p 的项目之前插入元素 es.__iter__()●●获取迭代器s.__len__()●●len(s)—项目数量s.__mul__(n)●s * n—重复连接s.__imul__(n)●s * n—原地重复连接s.__rmul__(n)●n * s—反向重复连接s.pop()●●移除并返回最后一个项目s.popleft()●移除并返回第一个项目s.remove(e)●●按值删除第一个出现的元素 es.reverse()●●原地反转项目顺序s.__reversed__()●●获取迭代器以从后向前扫描项目s.rotate(n)●将 n 个项目从一端移动到另一端s.__setitem__(p, e)●●s[p] e—将 e 放在位置 p覆盖现有的项目或切片s.sort([key], [reverse])●使用可选关键字参数 key 和 reverse 原地对项目进行排序^(a) 反向操作符在 第十六章 中有解释。^(b) a_list.pop(p) 允许从位置 p 处移除项目但 deque 不支持该选项。
除了 deque其他 Python 标准库包实现了队列
queue
这提供了同步即线程安全的类 SimpleQueue、Queue、LifoQueue 和 PriorityQueue。这些可以用于线程之间的安全通信。除了 SimpleQueue 外通过向构造函数提供大于 0 的 maxsize 参数其他队列都可以被限制大小。然而它们不会像 deque 那样丢弃项目以腾出空间。相反当队列已满时插入新项目会被阻塞—即等待直到其他线程通过从队列中取出项目来腾出空间这对于限制活动线程数量很有用。
multiprocessing
实现了自己的无界 SimpleQueue 和有界 Queue与 queue 包中的类非常相似但设计用于进程间通信。提供了专门的 multiprocessing.JoinableQueue 用于任务管理。
asyncio
提供了受 queue 和 multiprocessing 模块中类启发的 Queue、LifoQueue、PriorityQueue 和 JoinableQueue但适用于管理异步编程中的任务。
heapq
与前三个模块不同heapq 不实现队列类而是提供函数如 heappush 和 heappop让您可以使用可变序列作为堆队列或优先队列。
这结束了我们对 list 类型的替代品以及对序列类型的一般探索—除了 str 和二进制序列的细节它们有自己的章节第四章。
章节总结
精通标准库的序列类型是编写简洁、高效和惯用的 Python 代码的先决条件。
Python 序列通常被归类为可变或不可变但考虑一个不同的维度也是有用的扁平序列和容器序列。前者更紧凑、更快速、更易于使用但仅限于存储数字、字符和字节等原子数据。容器序列更灵活但当它们持有可变对象时可能会让您感到惊讶因此您需要小心地在嵌套数据结构中正确使用它们。
不幸的是Python 没有绝对可靠的不可变容器序列类型即使“不可变”元组中包含可变项如列表或用户定义对象其值也可能被更改。
列表推导和生成器表达式是构建和初始化序列的强大表示法。如果您尚未熟悉它们请花时间掌握它们的基本用法。这并不难很快您就会上瘾。
在 Python 中元组扮演两个角色作为具有未命名字段的记录和作为不可变列表。当将元组用作不可变列表时请记住仅当其中所有项也是不可变时元组值才被保证固定。在元组上调用 hash(t) 是一种快速断言其值固定的方法。如果 t 包含可变项则会引发 TypeError。
当元组用作记录时元组解包是提取元组字段的最安全、最可读的方式。除了元组外* 在许多上下文中与列表和可迭代对象一起使用并且在 Python 3.5 中出现了一些用例其中包括 PEP 448—Additional Unpacking Generalizations。Python 3.10 引入了带有 match/case 的模式匹配支持更强大的解包称为解构。
序列切片是 Python 中一个受欢迎的语法特性比许多人意识到的要更强大。多维切片和省略号...符号如 NumPy 中使用的方式也可能受到用户定义序列的支持。对切片赋值是编辑可变序列的一种非常表达性的方式。
如 seq * n 中的重复连接很方便并且经过小心处理可以用于初始化包含不可变项的列表列表。对于可变和不可变序列使用 和 * 的增强赋值行为不同。在后一种情况下这些运算符必然构建新序列。但如果目标序列是可变的则通常会就地更改它但并非总是这取决于序列的实现方式。
sort 方法和 sorted 内置函数易于使用且灵活这要归功于可选的 key 参数用于计算排序标准的函数。顺便说一句key 也可以与 min 和 max 内置函数一起使用。
除了列表和元组外Python 标准库还提供了 array.array。虽然 NumPy 和 SciPy 不是标准库的一部分但如果您对大量数据进行任何类型的数值处理学习这些库的一小部分甚至可以让您走得更远。
我们最后讨论了多才多艺且线程安全的 collections.deque将其 API 与 list 在表 2-4 中进行了比较并提到了标准库中的其他队列实现。
进一步阅读
第一章“数据结构”来自Python Cookbook第 3 版O’Reilly作者是 David Beazley 和 Brian K. Jones其中包含许多关于序列的技巧包括“Recipe 1.11. 命名切片”我从中学到了将切片赋值给变量以提高可读性的技巧在我们的示例 2-13 中有所展示。
Python Cookbook 的第二版是为 Python 2.4 编写的但其中的许多代码也适用于 Python 3并且第五章和第六章中的许多技巧涉及序列。该书由 Alex Martelli、Anna Ravenscroft 和 David Ascher 编辑其中包括数十位 Python 爱好者的贡献。第三版是从头开始重写的更侧重于语言的语义特别是 Python 3 中发生了什么变化而旧版则更强调实用性即如何将语言应用于实际问题。尽管第二版的一些解决方案不再是最佳方法但我真诚地认为值得同时拥有Python Cookbook 的两个版本。
官方 Python “排序 HOW TO” 中有几个关于使用 sorted 和 list.sort 的高级技巧示例。
PEP 3132—扩展可迭代解包是阅读关于在并行赋值的左侧使用*extra语法的新用法的权威来源。如果你想一窥 Python 的发展“缺失*-解包泛化”是一个提出增强可迭代解包符号的 bug 跟踪器问题。PEP 448—额外解包泛化是从该问题的讨论中产生的。
正如我在“使用序列进行模式匹配”中提到的Carol Willing 的“结构化模式匹配”部分在“Python 3.10 有什么新特性”中是对这一重要新功能的很好介绍大约有 1400 字当 Firefox 从 HTML 生成 PDF 时这不到 5 页。PEP 636—结构化模式匹配教程也不错但更长。同样的 PEP 636 包括“附录 A—快速介绍”。它比 Willing 的介绍短因为它省略了关于为什么模式匹配对你有好处的高层考虑。如果你需要更多论据来说服自己或他人模式匹配对 Python 有好处那么阅读 22 页的PEP 635—结构化模式匹配动机和原理。
Eli Bendersky 的博客文章“使用缓冲区协议和 memoryviews 在 Python 中减少拷贝”包含了关于memoryview的简短教程。
市场上有许多涵盖 NumPy 的书籍许多书名中并未提及“NumPy”。两个例子是 Jake VanderPlas 的开放获取书籍Python 数据科学手册以及 Wes McKinney 的第二版Python 数据分析。
“NumPy 的全部内容都关乎向量化。”这是 Nicolas P. Rougier 的开放获取书籍从 Python 到 NumPy的开篇语句。向量化操作将数学函数应用于数组的所有元素而无需在 Python 中编写显式循环。它们可以并行操作使用现代 CPU 中的特殊向量指令利用多个核心或委托给 GPU具体取决于库。Rougier 的书中的第一个例子展示了通过将一个漂亮的 Python 类使用生成器方法重构为调用几个 NumPy 向量函数的精简函数后速度提高了 500 倍。
要学习如何使用deque以及其他集合请参阅 Python 文档中“容器数据类型”中的示例和实用配方。
Python 惯例中排除范围和切片中的最后一项的最佳辩护是由 Edsger W. Dijkstra 亲自撰写的标题为“为什么编号应该从零开始”的短备忘录。备忘录的主题是数学符号但与 Python 相关因为 Dijkstra 以严谨和幽默解释了为什么像 2, 3, …, 12 这样的序列应该始终表示为 2 ≤ i 13。所有其他合理的惯例都被驳斥以及让每个用户选择惯例的想法。标题指的是基于零的索引但备忘录实际上是关于为什么ABCDE[1:3]意味着BC而不是BCD以及为什么写range(2, 13)来生成 2, 3, 4, …, 12 是完全合理的。顺便说一句备忘录是一张手写的便条但它非常漂亮且完全可读。Dijkstra 的笔迹非常清晰以至于有人根据他的笔记创建了一个字体。
¹ Leo GeurtsLambert Meertens 和 Steven PembertonABC 程序员手册第 8 页。 (Bosko Books)。
² 感谢读者 Tina Lapine 指出这一点。
³ 感谢技术审阅员 Leonardo Rochael 提供此示例。
⁴ 在我看来一系列的if/elif/elif/.../else块是对switch/case的一个很好的替代。它不会受到一些语言设计者在几十年后仍然无谓地从 C 语言中复制的贯穿和悬空 else问题的困扰这些问题已经被广泛认为是导致无数错误的原因。
⁵ 后者在 Norvig 的代码中被命名为eval我将其重命名以避免与 Python 的eval内置函数混淆。
⁶ 在“内存视图”中我们展示了特别构造的内存视图可以具有多个维度。
⁷ 不我没有搞错ellipsis类名确实全小写而实例是一个名为Ellipsis的内置对象就像bool是小写但其实例是True和False一样。
⁸ str是这个描述的一个例外。因为在实际代码库中在循环中使用进行字符串构建是如此普遍CPython 对这种用例进行了优化。str的实例在内存中分配了额外的空间因此连接不需要每次都复制整个字符串。
⁹ 感谢 Leonardo Rochael 和 Cesar Kawakami 在 2013 年 PythonBrasil 大会上分享这个谜题。
¹⁰ 读者建议在示例中的操作可以用t[2].extend([50,60])来完成而不会出错。我知道这一点但我的目的是展示在这种情况下运算符的奇怪行为。
¹¹ 接收者是方法调用的目标是方法体中绑定到self的对象。
¹² Python 的主要排序算法以其创造者 Tim Peters 命名为 Timsort。有关 Timsort 的一些趣闻参见“讲台”。
¹³ 这个例子中的单词按字母顺序排序因为它们完全由小写 ASCII 字符组成。请参见示例后的警告。
¹⁴ 先进先出——队列的默认行为。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/921721.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!