diff --git a/translations/zh/README.md b/translations/zh/README.md new file mode 100644 index 0000000..19d207d --- /dev/null +++ b/translations/zh/README.md @@ -0,0 +1,7 @@ +# Python 编程思维第三版 + +> 来源:[`allendowney.github.io/ThinkPython/`](https://allendowney.github.io/ThinkPython/) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/) \ No newline at end of file diff --git a/translations/zh/SUMMARY.md b/translations/zh/SUMMARY.md new file mode 100644 index 0000000..380d905 --- /dev/null +++ b/translations/zh/SUMMARY.md @@ -0,0 +1,23 @@ ++ [Python 编程思维第三版](README.md) ++ [Think Python](tnkpy3e_01.md) ++ [前言](tnkpy3e_02.md) ++ [1\. 编程作为一种思维方式](tnkpy3e_03.md) ++ [2\. 变量与语句](tnkpy3e_04.md) ++ [3. 函数](tnkpy3e_05.md) ++ [4\. 函数与接口](tnkpy3e_06.md) ++ [5. 条件语句与递归](tnkpy3e_07.md) ++ [6. 返回值](tnkpy3e_08.md) ++ [7\. 迭代与搜索](tnkpy3e_09.md) ++ [8\. 字符串和正则表达式](tnkpy3e_10.md) ++ [9\. 列表](tnkpy3e_11.md) ++ [10\. 字典](tnkpy3e_12.md) ++ [11\. 元组](tnkpy3e_13.md) ++ [12\. 文本分析与生成](tnkpy3e_14.md) ++ [13\. 文件与数据库](tnkpy3e_15.md) ++ [14\. 类和函数](tnkpy3e_16.md) ++ [15\. 类与方法](tnkpy3e_17.md) ++ [16\. 类和对象](tnkpy3e_18.md) ++ [17. 继承](tnkpy3e_19.md) ++ [18\. Python 附加功能](tnkpy3e_20.md) ++ [19. 最后的思考](tnkpy3e_21.md) ++ [空白笔记本](tnkpy3e_22.md) \ No newline at end of file diff --git a/translations/zh/img/04840afbf96285b3c181f739f961b9d1.png b/translations/zh/img/04840afbf96285b3c181f739f961b9d1.png new file mode 100644 index 0000000..26c9b87 Binary files /dev/null and b/translations/zh/img/04840afbf96285b3c181f739f961b9d1.png differ diff --git a/translations/zh/img/0516eb6de9935e024c0b540a33ccc2f5.png b/translations/zh/img/0516eb6de9935e024c0b540a33ccc2f5.png new file mode 100644 index 0000000..a9ea625 Binary files /dev/null and b/translations/zh/img/0516eb6de9935e024c0b540a33ccc2f5.png differ diff --git a/translations/zh/img/137875fd94efd00be105057fca45cab8.png b/translations/zh/img/137875fd94efd00be105057fca45cab8.png new file mode 100644 index 0000000..211d70c Binary files /dev/null and b/translations/zh/img/137875fd94efd00be105057fca45cab8.png differ diff --git a/translations/zh/img/20c498abf4d329b6904ae1a2d4c15efa.png b/translations/zh/img/20c498abf4d329b6904ae1a2d4c15efa.png new file mode 100644 index 0000000..a6f0423 Binary files /dev/null and b/translations/zh/img/20c498abf4d329b6904ae1a2d4c15efa.png differ diff --git a/translations/zh/img/32a99d0796e7c6eb18f5b3038069d04e.png b/translations/zh/img/32a99d0796e7c6eb18f5b3038069d04e.png new file mode 100644 index 0000000..d5189d5 Binary files /dev/null and b/translations/zh/img/32a99d0796e7c6eb18f5b3038069d04e.png differ diff --git a/translations/zh/img/48dbb9e8e1d723217cd7f02dafdf41bb.png b/translations/zh/img/48dbb9e8e1d723217cd7f02dafdf41bb.png new file mode 100644 index 0000000..137f151 Binary files /dev/null and b/translations/zh/img/48dbb9e8e1d723217cd7f02dafdf41bb.png differ diff --git a/translations/zh/img/5dabd26c3a8ec6a56a8c98d2081490cb.png b/translations/zh/img/5dabd26c3a8ec6a56a8c98d2081490cb.png new file mode 100644 index 0000000..40f35c1 Binary files /dev/null and b/translations/zh/img/5dabd26c3a8ec6a56a8c98d2081490cb.png differ diff --git a/translations/zh/img/698ff08186ba3a8f135d7c7b88b97009.png b/translations/zh/img/698ff08186ba3a8f135d7c7b88b97009.png new file mode 100644 index 0000000..a4306a7 Binary files /dev/null and b/translations/zh/img/698ff08186ba3a8f135d7c7b88b97009.png differ diff --git a/translations/zh/img/73a457cfb8b88465b8d35a8d99cfe5fe.png b/translations/zh/img/73a457cfb8b88465b8d35a8d99cfe5fe.png new file mode 100644 index 0000000..abcec9d Binary files /dev/null and b/translations/zh/img/73a457cfb8b88465b8d35a8d99cfe5fe.png differ diff --git a/translations/zh/img/76dde745ad063790ee3d6fa39fb2bf64.png b/translations/zh/img/76dde745ad063790ee3d6fa39fb2bf64.png new file mode 100644 index 0000000..14cead7 Binary files /dev/null and b/translations/zh/img/76dde745ad063790ee3d6fa39fb2bf64.png differ diff --git a/translations/zh/img/77ecdbb368a95f38f325abb612a85465.png b/translations/zh/img/77ecdbb368a95f38f325abb612a85465.png new file mode 100644 index 0000000..18bb64f Binary files /dev/null and b/translations/zh/img/77ecdbb368a95f38f325abb612a85465.png differ diff --git a/translations/zh/img/7a76c4015360d2c859efe99a405dc889.png b/translations/zh/img/7a76c4015360d2c859efe99a405dc889.png new file mode 100644 index 0000000..0d02267 Binary files /dev/null and b/translations/zh/img/7a76c4015360d2c859efe99a405dc889.png differ diff --git a/translations/zh/img/7f7d1aba33d53ebfe1703790b81777d2.png b/translations/zh/img/7f7d1aba33d53ebfe1703790b81777d2.png new file mode 100644 index 0000000..8582506 Binary files /dev/null and b/translations/zh/img/7f7d1aba33d53ebfe1703790b81777d2.png differ diff --git a/translations/zh/img/8084970e2e60b78c27070840c99e88f3.png b/translations/zh/img/8084970e2e60b78c27070840c99e88f3.png new file mode 100644 index 0000000..c29b47c Binary files /dev/null and b/translations/zh/img/8084970e2e60b78c27070840c99e88f3.png differ diff --git a/translations/zh/img/90be22db2df1a3c8bf4e0ef5ba93a2a5.png b/translations/zh/img/90be22db2df1a3c8bf4e0ef5ba93a2a5.png new file mode 100644 index 0000000..cf7ea53 Binary files /dev/null and b/translations/zh/img/90be22db2df1a3c8bf4e0ef5ba93a2a5.png differ diff --git a/translations/zh/img/92d83d3a339d69adaf61582f205196f6.png b/translations/zh/img/92d83d3a339d69adaf61582f205196f6.png new file mode 100644 index 0000000..7b4b8b6 Binary files /dev/null and b/translations/zh/img/92d83d3a339d69adaf61582f205196f6.png differ diff --git a/translations/zh/img/9d23255e2fd43eab2ce7769453b1414b.png b/translations/zh/img/9d23255e2fd43eab2ce7769453b1414b.png new file mode 100644 index 0000000..0584a3c Binary files /dev/null and b/translations/zh/img/9d23255e2fd43eab2ce7769453b1414b.png differ diff --git a/translations/zh/img/a09d0d29930012ae176692b0cb80c9df.png b/translations/zh/img/a09d0d29930012ae176692b0cb80c9df.png new file mode 100644 index 0000000..98a10a5 Binary files /dev/null and b/translations/zh/img/a09d0d29930012ae176692b0cb80c9df.png differ diff --git a/translations/zh/img/b37eb9da251407831430919397bc7f9a.png b/translations/zh/img/b37eb9da251407831430919397bc7f9a.png new file mode 100644 index 0000000..95ecae7 Binary files /dev/null and b/translations/zh/img/b37eb9da251407831430919397bc7f9a.png differ diff --git a/translations/zh/img/cf84277cd6a15970041c403cff3b289f.png b/translations/zh/img/cf84277cd6a15970041c403cff3b289f.png new file mode 100644 index 0000000..8955f54 Binary files /dev/null and b/translations/zh/img/cf84277cd6a15970041c403cff3b289f.png differ diff --git a/translations/zh/img/d47075b35980a553f7e01bc56e1e6c1f.png b/translations/zh/img/d47075b35980a553f7e01bc56e1e6c1f.png new file mode 100644 index 0000000..9f76bc4 Binary files /dev/null and b/translations/zh/img/d47075b35980a553f7e01bc56e1e6c1f.png differ diff --git a/translations/zh/img/f12ff5120511a74083dc05a1bf762afb.png b/translations/zh/img/f12ff5120511a74083dc05a1bf762afb.png new file mode 100644 index 0000000..d852270 Binary files /dev/null and b/translations/zh/img/f12ff5120511a74083dc05a1bf762afb.png differ diff --git a/translations/zh/img/fa9acdcb0f0d6c848961d6dd6343293a.png b/translations/zh/img/fa9acdcb0f0d6c848961d6dd6343293a.png new file mode 100644 index 0000000..a1a7c51 Binary files /dev/null and b/translations/zh/img/fa9acdcb0f0d6c848961d6dd6343293a.png differ diff --git a/translations/zh/img/fcebfc419783d3fbf78fbf7d44c4e86e.png b/translations/zh/img/fcebfc419783d3fbf78fbf7d44c4e86e.png new file mode 100644 index 0000000..5edd07c Binary files /dev/null and b/translations/zh/img/fcebfc419783d3fbf78fbf7d44c4e86e.png differ diff --git a/translations/zh/tnkpy3e_01.md b/translations/zh/tnkpy3e_01.md new file mode 100644 index 0000000..cb6fed7 --- /dev/null +++ b/translations/zh/tnkpy3e_01.md @@ -0,0 +1,109 @@ +# Think Python + +> 原文:[`allendowney.github.io/ThinkPython/`](https://allendowney.github.io/ThinkPython/) + +*Think Python* 是一本为没有编程经验的人(或者曾经尝试过但觉得困难的人)介绍 Python 的书。你可以在 [Bookshop.org](https://bookshop.org/a/98697/9781098155438) 和 [Amazon](https://www.amazon.com/_/dp/1098155432?smid=ATVPDKIKX0DER&_encoding=UTF8&tag=oreilly20-20&_encoding=UTF8&tag=greenteapre01-20&linkCode=ur2&linkId=e2a529f94920295d27ec8a06e757dc7c&camp=1789&creative=9325) 购买第三版的纸质书和电子书。 + +![`raw.githubusercontent.com/AllenDowney/ThinkPython/v3/think_python_3e.jpg`](https://raw.githubusercontent.com/AllenDowney/ThinkPython/v3/think_python_3e.jpg) + +[这是本书在 Green Tea Press 的主页](https://greenteapress.com/wp/think-python-3rd-edition/)。 + +第三版的主要变化有: + ++ 本书现在完全使用 Jupyter 笔记本格式,你可以在同一个地方阅读文本、运行代码并完成练习。通过下面的链接,你可以在 Colab 上运行这些笔记本,所以你无需安装任何东西即可开始。 + ++ 这本书的内容经过了大量修订,部分章节顺序有所调整。现在有更多的练习,我认为许多练习都更好。 + ++ 每一章的结尾都有一些建议,推荐使用像 ChatGPT 和 Colab AI 这样的工具来深入学习并帮助完成练习。 + +## 这些笔记本 + +**第一章:将编程作为一种思维方式** + ++ [点击这里在 Colab 上运行第一章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap01.ipynb) + +**第二章:变量与语句** + ++ [点击这里在 Colab 上运行第二章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap02.ipynb) + +**第三章:函数** + ++ [点击这里在 Colab 上运行第三章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap03.ipynb) + +**第四章:函数与接口** + ++ [点击这里在 Colab 上运行第四章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap04.ipynb) + +**第五章:条件语句与递归** + ++ [点击这里在 Colab 上运行第五章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap05.ipynb) + +**第六章:返回值** + ++ [点击这里在 Colab 上运行第六章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap06.ipynb) + +**第七章:迭代与搜索** + ++ [点击这里在 Colab 上运行第七章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap07.ipynb) + +**第八章:字符串与正则表达式** + ++ [点击这里在 Colab 上运行第八章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap08.ipynb) + +**第九章:列表** + ++ [点击这里在 Colab 上运行第九章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap09.ipynb) + +**第十章:字典** + ++ [点击这里在 Colab 上运行第十章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap10.ipynb) + +**第十一章:元组** + ++ [点击这里在 Colab 上运行第十一章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap11.ipynb) + +**第十二章:文本分析与生成** + ++ [点击这里在 Colab 上运行第十二章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap12.ipynb) + +**第十三章:文件与数据库** + ++ [点击这里在 Colab 上运行第十三章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap13.ipynb) + +**第十四章:类与函数** + ++ [点击这里在 Colab 上运行第十四章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap14.ipynb) + +**第十五章:类与方法** + ++ [点击这里在 Colab 上运行第十五章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap15.ipynb) + +**第十六章:类与对象** + ++ [点击这里在 Colab 上运行第十六章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap16.ipynb) + +**第十七章:继承** + ++ [点击这里在 Colab 上运行第十七章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap17.ipynb) + +**第十八章:Python 扩展** + ++ [点击这里在 Colab 上运行第十八章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap18.ipynb) + +**第十九章:总结思考** + ++ [点击这里在 Colab 上运行第十九章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/chapters/chap19.ipynb) + +## 教师资源 + +如果你正在使用这本书进行教学,以下是一些你可能会觉得有用的资源。 + ++ 你可以从[这个 GitHub 仓库](https://github.com/AllenDowney/ThinkPythonSolutions/tree/v3/soln)下载包含解答的笔记本。 + ++ 每章的测验和整本书的总结性测验可以根据要求提供。 + ++ *使用 Jupyter 进行教学与学习* 是一本在线书籍,提供了在课堂上有效使用 Jupyter 的建议。你可以在[这里阅读这本书](https://jupyter4edu.github.io/jupyter-edu-book)。 + ++ 在课堂上使用笔记本的最佳方式之一是现场编程,教师编写代码,学生在自己的笔记本中跟随。要了解现场编程——以及关于教学编程的其他很多宝贵建议——我推荐由 The Carpentries 提供的教师培训,[你可以在这里阅读](https://carpentries.github.io/instructor-training)。 + ++ 对于每一章,我创建了一个包含原文的“空白”笔记本,但大部分代码已被删除。这些笔记本对于进行跟随练习非常有用,学习者可以在其中填写空白。[空白笔记本的链接在这里](https://allendowney.github.io/ThinkPython/blank.html)。 diff --git a/translations/zh/tnkpy3e_02.md b/translations/zh/tnkpy3e_02.md new file mode 100644 index 0000000..48223b9 --- /dev/null +++ b/translations/zh/tnkpy3e_02.md @@ -0,0 +1,119 @@ +# 前言 + +> 原文:[`allendowney.github.io/ThinkPython/chap00.html`](https://allendowney.github.io/ThinkPython/chap00.html) + +## 这本书适合谁? + +如果你想学习编程,你来对地方了。Python 是初学者最好的编程语言之一,它也是目前最具需求的技能之一。 + +你也来得正是时候,因为现在学习编程可能比以往任何时候都要容易。借助像 ChatGPT 这样的虚拟助手,你不必孤单学习。在本书中,我将建议你如何使用这些工具加速学习。 + +本书主要面向从未编程过的人和那些有其他编程语言经验的人。如果你已经有了丰富的 Python 经验,可能会觉得前几章的进度太慢。 + +学习编程的一个挑战是你必须学习*两*种语言:一种是编程语言本身,另一种是我们用来讨论程序的词汇。如果你只学习编程语言,当你需要解释错误信息、阅读文档、与他人交流或者使用虚拟助手时,你很可能会遇到问题。如果你已经做了一些编程,但没有学会这第二种语言,希望你能从本书中受益。 + +## 本书目标 + +在写这本书时,我尽力小心处理词汇。当一个术语首次出现时,我会对其进行定义。在每一章的末尾,还有一个术语表,回顾了本章中出现的术语。 + +我还尽量简明扼要。读书所需的思维努力越少,你就越能集中精力编程。 + +但是,你不能仅仅通过读书来学习编程——你必须实践。因此,本书在每一章的末尾都包括了练习,帮助你巩固所学内容。 + +如果你认真阅读并坚持完成练习,你会取得进展。但我要提醒你——学习编程并不容易,即使对于经验丰富的程序员来说,它也可能是令人沮丧的。随着学习的深入,我会建议一些策略,帮助你编写正确的程序并修复错误的程序。 + +## 导读 + +本书的每一章都是建立在前一章的基础上的,因此你应该按顺序阅读,并在继续之前花时间完成练习。 + +前六章介绍了基本的元素,如算术、条件语句和循环。它们还介绍了编程中最重要的概念——函数,以及使用函数的一种强大方法——递归。 + +第七章和第八章介绍了字符串——它们可以表示字母、单词和句子——以及与之相关的算法。 + +第 9 到第十二章介绍了 Python 的核心数据结构——列表、字典和元组——这些是编写高效程序的强大工具。第十二章介绍了分析文本和随机生成新文本的算法。像这样的算法是大型语言模型(LLM)的核心,因此本章将让你了解像 ChatGPT 这样的工具是如何工作的。 + +第十三章讲解了如何将数据存储到长期存储中——文件和数据库。作为练习,你可以编写一个程序,搜索文件系统并查找重复文件。 + +第 14 到第十七章介绍了面向对象编程(OOP),这是一种组织程序和它们处理的数据的方式。许多 Python 库是以面向对象的风格编写的,因此这些章节将帮助你理解它们的设计——并定义你自己的对象。 + +本书的目标不是覆盖整个 Python 语言。而是专注于语言的一个子集,这个子集能够以最少的概念提供最大的能力。尽管如此,Python 仍然有许多功能,可以帮助你高效地解决常见问题。第十八章介绍了其中的一些功能。 + +最后,第十九章分享了我离别时的想法,并给出了继续编程旅程的建议。 + +## 第三版有什么新内容? + +本版中最大的变化是由两项新技术推动的——Jupyter 笔记本和虚拟助手。 + +本书的每一章都是一个 Jupyter 笔记本,它是一个包含普通文本和代码的文档。对我而言,这使得编写代码、测试代码并保持与文本一致变得更加容易。对你而言,这意味着你可以在一个地方运行代码、修改代码并完成练习。使用笔记本的说明在第一章中。 + +另一个大变化是,我增加了有关如何使用像 ChatGPT 这样的虚拟助手的建议,并利用它们加速学习。当本书的上一版在 2016 年出版时,这些工具的前身远不如现在有用,而且大多数人对此一无所知。如今,它们已成为软件工程的标准工具,我认为它们将成为学习编程——以及学习其他许多内容——的变革性工具。 + +书中的其他变化源于我对第二版的遗憾。 + +第一个遗憾是我没有强调软件测试。这在 2016 年时就已经是一个遗憾的遗漏,但随着虚拟助手的出现,自动化测试变得更加重要。因此,这一版介绍了 Python 最广泛使用的测试工具——`doctest` 和 `unittest`,并包含了几个练习,你可以在其中练习使用这些工具。 + +我的另一个遗憾是第二版的练习不够均衡——有些比其他的更有趣,而有些则过于难。转向 Jupyter 笔记本帮助我开发并测试了一个更具吸引力和更有效的练习顺序。 + +在这次修订中,主题的顺序几乎没有变化,但我重新安排了一些章节,并将两个较短的章节合并为一个。此外,我扩展了字符串的内容,加入了正则表达式。 + +有些章节使用了海龟图形。在之前的版本中,我使用了 Python 的` turtle`模块,但遗憾的是它在 Jupyter 笔记本中无法工作。所以我用一个新的海龟模块替换了它,应该更易于使用。 + +最后,我重写了大量的文本,澄清了需要澄清的地方,并删减了那些我可以更简洁表达的部分。 + +我对这个新版本感到非常自豪——希望你会喜欢! + +## 入门指南 + +对于大多数编程语言,包括 Python,你可以使用许多工具来编写和运行程序。这些工具被称为集成开发环境(IDEs)。一般来说,IDE 有两种类型: + ++ 一些 IDE 与包含代码的文件一起工作,因此它们提供了编辑和运行这些文件的工具。 + ++ 其他 IDE 主要与笔记本一起使用,笔记本是包含文本和代码的文档。 + +对于初学者,我推荐从像 Jupyter 这样的笔记本开发环境开始。 + +本书的笔记本可以从[`allendowney.github.io/ThinkPython`](https://allendowney.github.io/ThinkPython)上的在线仓库获得。 + +有两种使用方式: + ++ 你可以下载笔记本并在自己的计算机上运行。在这种情况下,你需要安装 Python 和 Jupyter,这并不难,但如果你想学习 Python,花费大量时间安装软件可能会让人感到沮丧。 + ++ 另一种选择是通过 Colab 运行笔记本,Colab 是一个在网页浏览器中运行的 Jupyter 环境,因此你无需安装任何东西。Colab 由 Google 运营,且免费使用。 + +如果你刚开始学习,我强烈建议你从 Colab 开始。 + +## 教师资源 + +如果你是用本书进行教学,以下是一些你可能会觉得有用的资源。 + ++ 你可以在[`allendowney.github.io/ThinkPython`](https://allendowney.github.io/ThinkPython)找到带有习题解答的笔记本,并附有以下额外资源的链接。 + ++ 每章的测验和整本书的总结性测验可以根据请求提供。 + ++ *Jupyter 的教学与学习*是一本在线书籍,提供了在课堂上有效使用 Jupyter 的建议。你可以在[`jupyter4edu.github.io/jupyter-edu-book`](https://jupyter4edu.github.io/jupyter-edu-book)阅读这本书。 + ++ 使用笔记本的最佳方式之一是现场编码,教师编写代码,学生在自己的笔记本中跟随。要了解现场编码并获取关于编程教学的其他建议,我推荐 The Carpentries 提供的讲师培训,网址是[`carpentries.github.io/instructor-training`](https://carpentries.github.io/instructor-training) + +## 致谢 + +非常感谢 Jeff Elkner,他将我的 Java 书籍翻译成 Python,从而启动了这个项目,并让我接触到了最终成为我最喜爱的语言。同时感谢 Chris Meyers,他为 *如何像计算机科学家一样思考* 贡献了几个章节。 + +感谢自由软件基金会开发的 GNU 自由文档许可证,使我能够与 Jeff 和 Chris 合作,也感谢创用 CC 为我目前使用的许可证提供支持。 + +感谢 Python 语言的开发者和维护者,以及我使用的库,包括 Turtle 图形模块;感谢我开发本书时使用的工具,包括 Jupyter 和 JupyterBook;还要感谢我使用的服务,包括 ChatGPT、Copilot、Colab 和 GitHub。 + +感谢 Lulu 的编辑,他们参与了 *如何像计算机科学家一样思考* 的编辑工作,感谢 O'Reilly Media 的编辑,他们参与了 *Think Python* 的编辑工作。 + +特别感谢第二版的技术审阅者 Melissa Lewis 和 Luciano Ramalho,以及第三版的 Sam Lau 和 Luciano Ramalho(再次感谢!)。我还特别感谢 Luciano 开发了我在多个章节中使用的 Turtle 图形模块,名为 `jupyturtle`。 + +感谢所有曾与本书早期版本合作的学生,以及所有提交了修正和建议的贡献者。在过去的几年里,超过 100 位眼光敏锐、思考周到的读者提供了建议和修正。他们的贡献和对本项目的热情给予了我极大的帮助。 + +如果你有任何建议或修正,请发送电子邮件至 `feedback@thinkpython.com`。如果你能至少提供出现错误的句子的一部分,这将帮助我更容易地找到问题。页面和章节号也可以,但不如句子直接有用。谢谢! + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可:[MIT 许可证](https://mit-license.org/) + +文本许可:[创用 CC 姓名标示-非商业性使用-相同方式共享 4.0 国际版](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_03.md b/translations/zh/tnkpy3e_03.md new file mode 100644 index 0000000..173bd60 --- /dev/null +++ b/translations/zh/tnkpy3e_03.md @@ -0,0 +1,610 @@ +# 1\. 编程作为一种思维方式 + +> 原文:[`allendowney.github.io/ThinkPython/chap01.html`](https://allendowney.github.io/ThinkPython/chap01.html) + +本书的第一个目标是教你如何使用 Python 编程。但是,学习编程意味着学习一种新的思维方式,因此本书的第二个目标是帮助你像计算机科学家一样思考。这种思维方式结合了数学、工程和自然科学的一些最佳特征。像数学家一样,计算机科学家使用形式化语言来表示思想——特别是表示计算。像工程师一样,他们设计事物,将组件组装成系统,并在替代方案之间进行权衡。像科学家一样,他们观察复杂系统的行为,提出假设,并测试预测。 + +我们将从编程的最基本元素开始,逐步深入。在本章中,我们将了解 Python 如何表示数字、字母和单词。你还将学会执行算术运算。 + +你还将开始学习编程的词汇,包括运算符、表达式、值和类型等术语。这个词汇非常重要——你需要它来理解本书的其他内容,与其他程序员沟通,并使用和理解虚拟助手。 + +## 1.1\. 算术运算符 + +**算术运算符**是表示算术计算的符号。例如,加号`+`表示加法运算。 + +```py +30 + 12 +``` + +```py +42 +``` + +减号`-`是执行减法的运算符。 + +```py +43 - 1 +``` + +```py +42 +``` + +星号`*`表示乘法运算。 + +```py +6 * 7 +``` + +```py +42 +``` + +斜杠`/`表示除法运算: + +```py +84 / 2 +``` + +```py +42.0 +``` + +注意,除法的结果是`42.0`而不是`42`。这是因为 Python 中有两种类型的数字: + ++ **整数**,表示没有分数或小数部分的数字,以及 + ++ **浮点数**,表示整数和带有小数点的数字。 + +如果你对两个整数进行加法、减法或乘法运算,结果是一个整数。但是如果你对两个整数进行除法运算,结果是一个浮动小数。Python 提供了另一个运算符`//`,用于执行**整数除法**。整数除法的结果始终是一个整数。 + +```py +84 // 2 +``` + +```py +42 +``` + +整数除法也被称为“地板除法”,因为它总是向下舍入(朝向“地板”)。 + +```py +85 // 2 +``` + +```py +42 +``` + +最后,运算符`**`执行指数运算;也就是说,它将一个数字提升到一个幂: + +```py +7 ** 2 +``` + +```py +49 +``` + +在某些其他语言中,插入符号`^`用于表示指数运算,但在 Python 中,它是一个位运算符,称为 XOR。如果你不熟悉位运算符,结果可能会让你感到意外: + +```py +7 ^ 2 +``` + +```py +5 +``` + +我不会在本书中介绍位运算符,但你可以在[`wiki.python.org/moin/BitwiseOperators`](http://wiki.python.org/moin/BitwiseOperators)阅读相关内容。 + +## 1.2\. 表达式 + +运算符和数字的集合叫做**表达式**。一个表达式可以包含任意数量的运算符和数字。例如,下面是一个包含两个运算符的表达式。 + +```py +6 + 6 ** 2 +``` + +```py +42 +``` + +请注意,指数运算优先于加法。Python 遵循你在数学课上学到的运算顺序:指数运算优先于乘法和除法,而乘法和除法又优先于加法和减法。 + +在下面的例子中,乘法发生在加法之前。 + +```py +12 + 5 * 6 +``` + +```py +42 +``` + +如果你希望加法先发生,可以使用括号。 + +```py +(12 + 5) * 6 +``` + +```py +102 +``` + +每个表达式都有一个**值**。例如,表达式 `6 * 7` 的值是 `42`。 + +## 1.3\. 算术函数 + +除了算术运算符,Python 还提供了一些**函数**,可以与数字一起使用。例如,`round` 函数接受一个浮动点数字,并将其四舍五入到最接近的整数。 + +```py +round(42.4) +``` + +```py +42 +``` + +```py +round(42.6) +``` + +```py +43 +``` + +`abs` 函数计算一个数字的绝对值。对于正数,绝对值就是数字本身。 + +```py +abs(42) +``` + +```py +42 +``` + +对于负数,绝对值是正数。 + +```py +abs(-42) +``` + +```py +42 +``` + +当我们使用像这样的函数时,我们说我们在**调用**该函数。调用函数的表达式叫做**函数调用**。 + +当你调用函数时,括号是必须的。如果不加括号,会得到错误信息。 + +```py +abs 42 +``` + +```py + Cell In[17], line 1 + abs 42 + ^ +SyntaxError: invalid syntax +``` + +你可以忽略这条消息的第一行,它现在不包含任何我们需要理解的信息。第二行是包含错误的代码,下面有一个插入符号 (`^`),指示发现错误的位置。 + +最后一行表明这是一个**语法错误**,意味着表达式的结构有问题。在这个例子中,问题是函数调用需要括号。 + +让我们看看如果你省略括号*以及*值会发生什么。 + +```py +abs +``` + +```py + +``` + +函数名本身就是一个合法的表达式,具有一个值。当它被显示时,值表示 `abs` 是一个函数,并包含一些稍后我会解释的附加信息。 + +## 1.4\. 字符串 + +除了数字,Python 还可以表示字母的序列,这些序列被称为**字符串**,因为字母像珠子一样串在一起。要写一个字符串,我们可以将字母序列放在直引号内。 + +```py +'Hello' +``` + +```py +'Hello' +``` + +使用双引号也是合法的。 + +```py +"world" +``` + +```py +'world' +``` + +双引号使得写包含撇号的字符串变得容易,因为撇号和直引号是相同的符号。 + +```py +"it's a small " +``` + +```py +"it's a small " +``` + +字符串也可以包含空格、标点符号和数字。 + +```py +'Well, ' +``` + +```py +'Well, ' +``` + +`+` 运算符可以与字符串一起使用;它将两个字符串连接成一个字符串,这个操作称为**连接**。 + +```py +'Well, ' + "it's a small " + 'world.' +``` + +```py +"Well, it's a small world." +``` + +`*` 运算符也可以与字符串一起使用;它可以制作字符串的多个副本并将它们连接起来。 + +```py +'Spam, ' * 4 +``` + +```py +'Spam, Spam, Spam, Spam, ' +``` + +其他的算术运算符不能与字符串一起使用。 + +Python 提供了一个名为 `len` 的函数,用于计算字符串的长度。 + +```py +len('Spam') +``` + +```py +4 +``` + +注意,`len` 计算引号之间的字母数,但不包括引号本身。 + +当你创建一个字符串时,请确保使用直引号。反引号,也称为反引号,会导致语法错误。 + +```py +`Hello` +``` + +```py + Cell In[26], line 1 + `Hello` + ^ +SyntaxError: invalid syntax +``` + +智能引号,也称为卷曲引号,也是非法的。 + +## 1.5\. 值和类型 + +到目前为止,我们已经看到了三种类型的值: + ++ `2` 是一个整数, + ++ `42.0` 是一个浮点数,而 + ++ `'Hello'` 是一个字符串。 + +一种值被称为**类型**。每个值都有一个类型 - 或者我们有时说它“属于”一个类型。 + +Python 提供了一个名为 `type` 的函数,可以告诉你任何值的类型。整数的类型是 `int`。 + +```py +type(2) +``` + +```py +int +``` + +浮点数的类型是 `float`。 + +```py +type(42.0) +``` + +```py +float +``` + +字符串的类型是 `str`。 + +```py +type('Hello, World!') +``` + +```py +str +``` + +类型 `int`、`float` 和 `str` 可以被用作函数。例如,`int` 可以接受一个浮点数并将其转换为整数(总是向下取整)。 + +```py +int(42.9) +``` + +```py +42 +``` + +而 `float` 可以将整数转换为浮点数值。 + +```py +float(42) +``` + +```py +42.0 +``` + +现在,这里有一些可能让人困惑的东西。如果你把一串数字放在引号中会得到什么呢? + +```py +'126' +``` + +```py +'126' +``` + +它看起来像一个数字,但实际上它是一个字符串。 + +```py +type('126') +``` + +```py +str +``` + +如果你尝试像数字一样使用它,可能会得到一个错误。 + +```py +'126' / 3 +``` + +```py +TypeError: unsupported operand type(s) for /: 'str' and 'int' +``` + +这个例子会生成一个 `TypeError`,这意味着表达式中的值(称为**操作数**)具有错误的类型。错误消息表明 `/` 运算符不支持这些值的类型,它们分别为 `str` 和 `int`。 + +如果你有一个包含数字的字符串,你可以使用 `int` 将其转换为整数。 + +```py +int('126') / 3 +``` + +```py +42.0 +``` + +如果你有一个包含数字和小数点的字符串,你可以使用 `float` 将其转换为浮点数。 + +```py +float('12.6') +``` + +```py +12.6 +``` + +当你写一个大整数时,你可能会尝试在数字组之间使用逗号,例如 `1,000,000`。这在 Python 中是一个合法的表达式,但结果不是一个整数。 + +```py +1,000,000 +``` + +```py +(1, 0, 0) +``` + +Python 解释 `1,000,000` 为一个逗号分隔的整数序列。我们稍后会学到更多关于这种序列的知识。 + +你可以使用下划线使大数更易于阅读。 + +```py +1_000_000 +``` + +```py +1000000 +``` + +## 1.6\. 形式语言和自然语言 + +**自然语言** 是人们说的语言,如英语、西班牙语和法语。它们不是由人类设计的;它们是自然演变而来的。 + +**形式语言** 是由人类设计用于特定应用的语言。例如,数学家使用的符号是一种形式语言,非常擅长表示数字和符号之间的关系。类似地,编程语言是被设计用来表达计算的形式语言。 + +尽管形式语言和自然语言有一些共同点,但它们之间存在重要的区别: + ++ 歧义:自然语言充满了歧义,人们通过使用上下文线索和其他信息来处理它。形式语言旨在几乎或完全无歧义,这意味着任何程序具有完全确定的意义,无论上下文如何。 + ++ 冗余性:为了弥补歧义并减少误解,自然语言使用冗余。因此,它们通常很冗长。正式语言不那么冗余,更加简洁。 + ++ 文字的字面性:自然语言充满成语和隐喻。正式语言确切地表达其意思。 + +因为我们都是用自然语言长大的,有时很难适应正式语言。正式语言比自然语言更为密集,因此阅读起来需要更多时间。此外,结构也很重要,所以不总是从上到下、从左到右阅读最佳。最后,细节至关重要。在自然语言中,你可以忽略拼写和标点的小错误,但在正式语言中,这可能造成很大差异。 + +## 1.7\. 调试 + +程序员会犯错误。出于一些奇怪的原因,编程错误被称为**bug**,追踪它们的过程称为**debugging**。 + +编程,尤其是调试,有时会激发强烈的情绪。如果你在解决一个困难的 bug,你可能会感到愤怒、悲伤或尴尬。 + +准备好应对这些反应可能有助于你应对它们。一种方法是把计算机看作是一个具有某些优势(如速度和精度)和特定弱点(如缺乏同理心和无法把握全局图景)的员工。 + +你的工作是成为一名优秀的经理:找到利用优势和减少弱点的方法。找到利用你的情绪参与问题解决的方法,同时不让你的反应影响你有效工作的能力。 + +学习调试可能很令人沮丧,但这是一种有价值的技能,对编程之外的许多活动都很有用。在每章的末尾,都会有一个像这样的部分,提供关于调试的建议。希望它们有所帮助! + +## 1.8\. 术语表 + +**算术运算符:** 符号,如`+`和`*`,表示加法或乘法等算术运算。 + +**整数:** 一种表示没有分数或小数部分的数字的类型。 + +**浮点数:** 一种表示整数和带小数部分的数字的类型。 + +**整数除法:** 一个运算符,`//`,用于将两个数字相除并向下取整至整数。 + +**表达式:** 变量、值和运算符的组合。 + +**值:** 整数、浮点数或字符串 - 或者我们稍后将看到的其他类型的值。 + +**函数:** 执行某些有用操作的一系列语句的命名序列。函数可能需要参数,也可能不需要,并且可能产生结果,也可能不产生。 + +**函数调用:** 一个表达式或表达式的一部分,运行一个函数。它由函数名后跟括号中的参数列表组成。 + +**语法错误:** 程序中的错误,使其无法解析 - 因此也无法运行。 + +**字符串:** 一种表示字符序列的类型。 + +**concatenation:** 将两个字符串连接在一起。 + +**type:** 一类值。我们到目前为止见过的类型有整数(类型 `int`)、浮点数(类型 `float`)和字符串(类型 `str`)。 + +**operand:** 操作符作用的值之一。 + +**natural language:** 人们说的自然演变而来的任何语言。 + +**formal language:** 人们为特定目的设计的任何语言,例如表示数学思想或计算机程序。所有编程语言都是形式语言。 + +**bug:** 程序中的错误。 + +**debugging:** 查找和修正错误的过程。 + +## 1.9\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +### 1.9.1\. 向虚拟助手提问 + +在你学习本书的过程中,有几种方式可以利用虚拟助手或聊天机器人帮助你学习。 + ++ 如果你想了解章节中的某个主题,或者有任何不清楚的地方,可以请求解释。 + ++ 如果你在做任何练习时遇到困难,可以请求帮助。 + +在每一章中,我都会建议你可以和虚拟助手一起做的练习,但我鼓励你自己尝试,看看什么方法最适合你。 + +这里有一些你可以向虚拟助手提问的主题: + ++ 之前我提到了位运算符,但我没有解释为什么 `7 ^ 2` 的值是 5。试试问“Python 中的位运算符是什么?”或者“`7 XOR 2` 的值是多少?” + ++ 我还提到了运算顺序。想了解更多细节,可以问“Python 中的运算顺序是什么?” + ++ `round` 函数,我们用它来将浮点数四舍五入到最接近的整数,可以接受第二个参数。试试问“round 函数的参数是什么?”或者“如何将 pi 四舍五入到三位小数?” + ++ 还有一个我没提到的算术运算符;试试问“Python 中的取余运算符是什么?” + +大多数虚拟助手都了解 Python,因此它们能比较可靠地回答类似这样的问题。但请记住,这些工具也会犯错。如果你从聊天机器人那里得到代码,一定要进行测试! + +### 1.9.2\. 练习 + +你可能会想,如果一个数字以 `0.5` 结尾,`round` 会做什么?答案是它有时会向上舍入,有时会向下舍入。试试这些例子,看看你能不能弄清楚它遵循什么规则。 + +```py +round(42.5) +``` + +```py +42 +``` + +```py +round(43.5) +``` + +```py +44 +``` + +如果你感兴趣,可以问虚拟助手:“如果一个数字以 0.5 结尾,Python 会向上还是向下舍入?” + +### 1.9.3\. 练习 + +当你学习新特性时,应该尝试自己动手并故意犯错。这样,你会学到错误信息的含义,当你再次看到它们时就能理解它们。这比在后期偶然犯错要好。 + +1. 你可以使用负号来表示负数,例如 `-2`。如果你在一个数字前面加上加号会发生什么?那 `2++2` 呢? + +1. 如果你有两个值中间没有运算符,比如`4 2`,会发生什么? + +1. 如果你调用一个函数像`round(42.5)`,如果省略一个或两个括号会发生什么? + +### 1.9.4\. 练习 + +记住每个表达式都有一个值,每个值都有一个类型,我们可以使用`type`函数来查找任何值的类型。 + +以下表达式的值的类型是什么?请对每个做出最佳猜测,然后使用`type`来确认。 + ++ `765` + ++ `2.718` + ++ `'2 pi'` + ++ `abs(-7)` + ++ `abs(-7.0)` + ++ `abs` + ++ `int` + ++ `type` + +### 1.9.5\. 练习 + +以下问题让你有机会练习编写算术表达式。 + +1. 42 分钟 42 秒是多少秒? + +1. 10 公里是多少英里?提示:1 英里等于 1.61 公里。 + +1. 如果你在 42 分钟 42 秒内跑完 10 公里,你的平均配速是多少(单位:每英秒)? + +1. 你的平均配速是多少(单位:分钟和秒每英里)? + +1. 你的平均速度是多少(单位:英里每小时)? + +如果你已经了解了变量,你可以在这个练习中使用它们。如果你不懂,也可以做这个练习,然后我们会在下一章介绍它们。 + +[Think Python: 第三版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可:[MIT 许可](https://mit-license.org/) + +文本许可:[知识共享署名-非商业性使用-相同方式共享 4.0 国际](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_04.md b/translations/zh/tnkpy3e_04.md new file mode 100644 index 0000000..6843d6c --- /dev/null +++ b/translations/zh/tnkpy3e_04.md @@ -0,0 +1,538 @@ +# 2\. 变量与语句 + +> 原文:[`allendowney.github.io/ThinkPython/chap02.html`](https://allendowney.github.io/ThinkPython/chap02.html) + +在上一章中,我们使用运算符编写了执行算术计算的表达式。 + +在本章中,你将学习关于变量和语句、`import`语句以及`print`函数的知识。我还将介绍更多我们用来讨论程序的词汇,包括“参数”和“模块”。 + +## 2.1\. 变量 + +**变量**是指向某个值的名称。要创建一个变量,我们可以像这样写一个**赋值语句**。 + +```py +n = 17 +``` + +一个赋值语句有三个部分:左边是变量名,等号操作符`=`,右边是表达式。在这个示例中,表达式是一个整数。在以下示例中,表达式是一个浮动小数。 + +```py +pi = 3.141592653589793 +``` + +在以下示例中,表达式是一个字符串。 + +```py +message = 'And now for something completely different' +``` + +当你执行赋值语句时,没有输出。Python 会创建变量并赋予它一个值,但赋值语句没有可见的效果。然而,在创建变量后,你可以将其作为表达式使用。因此我们可以这样显示`message`的值: + +```py +message +``` + +```py +'And now for something completely different' +``` + +你还可以将变量用作包含算术运算符的表达式的一部分。 + +```py +n + 25 +``` + +```py +42 +``` + +```py +2 * pi +``` + +```py +6.283185307179586 +``` + +你还可以在调用函数时使用变量。 + +```py +round(pi) +``` + +```py +3 +``` + +```py +len(message) +``` + +```py +42 +``` + +## 2.2\. 状态图 + +在纸面上表示变量的常见方式是写下变量名,并画一个箭头指向它的值。 + +![_images/54319c437cca81caa5fa9ee52a1edfb01810e693b7ebd67c56de713f1d132655.png](img/137875fd94efd00be105057fca45cab8.png) + +这种图形被称为**状态图**,因为它展示了每个变量的状态(可以把它看作是变量的“心理状态”)。我们将在全书中使用状态图来表示 Python 如何存储变量及其值的模型。 + +## 2.3\. 变量名 + +变量名可以任意长。它们可以包含字母和数字,但不能以数字开头。使用大写字母是合法的,但通常约定变量名使用小写字母。 + +变量名中唯一可以出现的标点符号是下划线字符`_`。它通常用于多个单词的名字中,例如`your_name`或`airspeed_of_unladen_swallow`。 + +如果给变量一个非法的名称,就会得到语法错误。`million!`是非法的,因为它包含了标点符号。 + +```py +million! = 1000000 +``` + +```py + Cell In[12], line 1 + million! = 1000000 + ^ +SyntaxError: invalid syntax +``` + +`76trombones`是非法的,因为它以数字开头。 + +```py +76trombones = 'big parade' +``` + +```py + Cell In[13], line 1 + 76trombones = 'big parade' + ^ +SyntaxError: invalid decimal literal +``` + +`class`也是非法的,但可能不太明显为什么。 + +```py +class = 'Self-Defence Against Fresh Fruit' +``` + +```py + Cell In[14], line 1 + class = 'Self-Defence Against Fresh Fruit' + ^ +SyntaxError: invalid syntax +``` + +事实证明,`class`是一个**关键字**,是用来指定程序结构的特殊词汇。关键字不能用作变量名。 + +这是 Python 关键字的完整列表: + +```py +False await else import pass +None break except in raise +True class finally is return +and continue for lambda try +as def from nonlocal while +assert del global not with +async elif if or yield +``` + +你不需要记住这个列表。在大多数开发环境中,关键字会以不同的颜色显示;如果你尝试将其作为变量名使用,你会知道的。 + +## 2.4\. 导入语句 + +为了使用一些 Python 功能,你必须**导入**它们。例如,下面的语句导入了 `math` 模块。 + +```py +import math +``` + +**模块**是一个包含变量和函数的集合。数学模块提供了一个叫做`pi`的变量,包含了数学常数\(\pi\)的值。我们可以像这样显示它的值。 + +```py +math.pi +``` + +```py +3.141592653589793 +``` + +要在模块中使用变量,你必须在模块名称和变量名称之间使用**点操作符**(`.`)。 + +数学模块还包含函数。例如,`sqrt` 计算平方根。 + +```py +math.sqrt(25) +``` + +```py +5.0 +``` + +而 `pow` 将一个数字提升为第二个数字的幂。 + +```py +math.pow(5, 2) +``` + +```py +25.0 +``` + +到目前为止,我们已经看到两种将数字提升为幂的方法:我们可以使用 `math.pow` 函数或指数运算符 `**`。两者都可以,但运算符的使用频率比函数更高。 + +## 2.5\. 表达式和语句 + +到目前为止,我们已经看到几种类型的表达式。一个表达式可以是一个单独的值,比如整数、浮点数或字符串。它也可以是一个包含值和运算符的集合。它还可以包括变量名和函数调用。这是一个包含这些元素的表达式。 + +```py +19 + n + round(math.pi) * 2 +``` + +```py +42 +``` + +我们也见过几种类型的语句。**语句**是一个有作用但没有值的代码单元。例如,一个赋值语句创建一个变量并赋予它一个值,但语句本身没有值。 + +```py +n = 17 +``` + +同样,导入语句也有一个作用——它导入一个模块,以便我们可以使用它包含的变量和函数——但它没有可见的效果。 + +```py +import math +``` + +计算表达式的值称为**求值**。执行语句称为**执行**。 + +## 2.6\. print 函数 + +当你求值一个表达式时,结果会被显示出来。 + +```py +n + 1 +``` + +```py +18 +``` + +但是,如果你计算多个表达式,只有最后一个表达式的值会被显示。 + +```py +n + 2 +n + 3 +``` + +```py +20 +``` + +要显示多个值,你可以使用 `print` 函数。 + +```py +print(n+2) +print(n+3) +``` + +```py +19 +20 +``` + +它同样适用于浮点数和字符串。 + +```py +print('The value of pi is approximately') +print(math.pi) +``` + +```py +The value of pi is approximately +3.141592653589793 +``` + +你还可以使用由逗号分隔的表达式序列。 + +```py +print('The value of pi is approximately', math.pi) +``` + +```py +The value of pi is approximately 3.141592653589793 +``` + +注意,`print` 函数会在值之间添加空格。 + +## 2.7\. 参数 + +当你调用一个函数时,括号中的表达式被称为**参数**。通常我会解释为什么,但是在这种情况下,术语的技术含义几乎与词汇的常见含义无关,所以我就不尝试了。 + +到目前为止,我们看到的一些函数只接受一个参数,像 `int`。 + +```py +int('101') +``` + +```py +101 +``` + +有些需要两个参数,像 `math.pow`。 + +```py +math.pow(5, 2) +``` + +```py +25.0 +``` + +有些可以接受额外的可选参数。例如,`int` 可以接受一个第二个参数,指定数字的基数。 + +```py +int('101', 2) +``` + +```py +5 +``` + +二进制中的数字序列`101`表示十进制中的数字 5。 + +`round`还可以接受一个可选的第二个参数,表示四舍五入的位数。 + +```py +round(math.pi, 3) +``` + +```py +3.142 +``` + +一些函数可以接受任意数量的参数,比如`print`。 + +```py +print('Any', 'number', 'of', 'arguments') +``` + +```py +Any number of arguments +``` + +如果你调用一个函数并提供了太多的参数,那也是一个`TypeError`。 + +```py +float('123.0', 2) +``` + +```py +TypeError: float expected at most 1 argument, got 2 +``` + +如果你提供了太少的参数,那也是一个`TypeError`。 + +```py +math.pow(2) +``` + +```py +TypeError: pow expected 2 arguments, got 1 +``` + +如果你提供了一个类型函数无法处理的参数,那也是一个`TypeError`。 + +```py +math.sqrt('123') +``` + +```py +TypeError: must be real number, not str +``` + +在开始时进行这种检查可能会让人烦恼,但它有助于你发现和修正错误。 + +## 2.8\. 注释 + +随着程序变得越来越大和复杂,它们变得更难阅读。正式的编程语言是密集的,通常很难看一段代码就明白它在做什么以及为什么这么做。 + +因此,最好在程序中添加注释,用自然语言解释程序在做什么。这些注释被称为**注释**,以`#`符号开头。 + +```py +# number of seconds in 42:42 +seconds = 42 * 60 + 42 +``` + +在这种情况下,注释会单独出现在一行上。你也可以将注释放在一行的末尾: + +```py +miles = 10 / 1.61 # 10 kilometers in miles +``` + +从`#`到行末的所有内容都会被忽略——它对程序的执行没有影响。 + +注释在记录代码中不明显的特性时最有用。可以合理假设读者能够弄明白*代码做了什么*;更有用的是解释*为什么*代码这么做。 + +这个注释与代码重复,毫无用处: + +```py +v = 8 # assign 8 to v +``` + +这个注释包含了代码中没有的信息: + +```py +v = 8 # velocity in miles per hour +``` + +良好的变量名可以减少注释的需要,但过长的名字可能会让复杂的表达式难以阅读,因此需要做出取舍。 + +## 2.9\. 调试 + +程序中可能发生三种类型的错误:语法错误、运行时错误和语义错误。区分它们很有用,这样可以更快地定位问题。 + ++ **语法错误**:“语法”是指程序的结构以及关于该结构的规则。如果程序中的任何地方存在语法错误,Python 不会运行程序,而是立即显示一条错误信息。 + ++ **运行时错误**:如果程序中没有语法错误,它就可以开始运行。但如果发生错误,Python 会显示一条错误信息并停止运行。这种错误被称为运行时错误,也叫做**异常**,因为它表示发生了某些异常情况。 + ++ **语义错误**:第三种类型的错误是“语义”错误,即与含义相关的错误。如果程序中存在语义错误,它会运行,但不会生成错误信息,而且它不会按你预期的方式工作。识别语义错误可能很棘手,因为它需要你通过查看程序的输出,反向推理出程序在做什么。 + +如我们所见,非法的变量名是语法错误。 + +```py +million! = 1000000 +``` + +```py + Cell In[40], line 1 + million! = 1000000 + ^ +SyntaxError: invalid syntax +``` + +如果你使用了不支持的操作符类型,这就是一个运行时错误。 + +```py +'126' / 3 +``` + +```py +TypeError: unsupported operand type(s) for /: 'str' and 'int' +``` + +最后,这里有一个语义错误的例子。假设我们想要计算`1`和`3`的平均值,但我们忽略了操作顺序,写成了这样: + +```py +1 + 3 / 2 +``` + +```py +2.5 +``` + +当此表达式被求值时,它不会产生错误信息,因此没有语法错误或运行时错误。但结果不是`1`和`3`的平均值,因此程序不正确。这是一个语义错误,因为程序运行了,但没有达到预期的效果。 + +## 2.10\. 术语表 + +**变量:** 一个代表值的名称。 + +**赋值语句:** 给变量赋值的语句。 + +**状态图:** 一组变量及其引用值的图形表示。 + +**关键字:** 用来指定程序结构的特殊词语。 + +**导入语句:** 读取模块文件,使我们可以使用其中的变量和函数的语句。 + +**模块:** 一个包含 Python 代码的文件,包括函数定义,有时还包括其他语句。 + +**点操作符:** 用于通过指定模块名称后跟点和函数名来访问另一个模块中的函数的操作符`。`。 + +**求值:** 执行表达式中的操作以计算值。 + +**语句:** 一行或多行代码,表示一个命令或操作。 + +**执行:** 运行一个语句并按照它的指示操作。 + +**参数:** 在调用函数时提供给函数的值。 + +**注释:** 程序中包含的文本,提供关于程序的信息,但对程序执行没有影响。 + +**运行时错误:** 导致程序显示错误信息并退出的错误。 + +**异常:** 在程序运行时检测到的错误。 + +**语义错误:** 一个错误,导致程序执行不正确,但不会显示错误信息。 + +## 2.11\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +### 2.11.1\. 向虚拟助手提问 + +再次鼓励你使用虚拟助手来了解本章中的任何主题。 + +如果你对我列出的任何关键字感到好奇,你可以问:“为什么 class 是一个关键字?”或“为什么变量名不能是关键字?” + +你可能注意到,`int`、`float`和`str`不是 Python 的关键字。它们是代表类型的变量,也可以作为函数使用。因此,使用这些名称作为变量或函数是*合法的*,但强烈不建议这样做。可以问助手:“为什么使用 int、float 和 str 作为变量名不好?” + +也可以问:“Python 中的内置函数有哪些?”如果你对其中的任何函数感兴趣,询问更多信息。 + +在本章中,我们导入了`math`模块,并使用了其中的一些变量和函数。可以问助手:“math 模块中有哪些变量和函数?”以及“除了 math,还有哪些模块是 Python 的核心模块?” + +### 2.11.2\. 练习 + +重申我在上一章中的建议,每当你学习一个新特性时,你应该故意犯一些错误,看看会发生什么。 + ++ 我们已经看到`n = 17`是合法的。那么`17 = n`呢? + ++ `x = y = 1` 怎么样? + ++ 在一些编程语言中,每条语句都以分号(`;`)结尾。如果在 Python 语句末尾加上分号,会发生什么? + ++ 如果在语句末尾加一个句点,会发生什么? + ++ 如果你拼写模块名错误并尝试导入`maath`,会发生什么? + +### 2.11.3\. 练习 + +练习使用 Python 解释器作为计算器: + +**第一部分。** 半径为\(r\)的球体的体积是\(\frac{4}{3} \pi r³\)。半径为 5 的球体体积是多少?首先创建一个名为`radius`的变量,然后将结果赋给一个名为`volume`的变量,并显示结果。添加注释,表示`radius`的单位是厘米,`volume`的单位是立方厘米。 + +**第二部分。** 一条三角学定理说,对于任何值\(x\),\((\cos x)² + (\sin x)² = 1\)。让我们看看对于特定值\(x = 42\),它是否成立。 + +创建一个名为`x`的变量并赋值。然后使用`math.cos`和`math.sin`计算\(x\)的正弦和余弦,以及它们平方的和。 + +结果应该接近 1。它可能不是精确的 1,因为浮点运算并不完全准确——它只是近似正确的。 + +**第三部分。** 除了`pi`,在`math`模块中定义的另一个变量是`e`,它代表自然对数的底数,数学符号表示为\(e\)。如果你不熟悉这个值,可以问虚拟助手“`math.e`是什么?”现在让我们通过三种方法计算\(e²\): + ++ 使用`math.e`和指数运算符(`**`)。 + ++ 使用`math.pow`将`math.e`的值提高到`2`的幂。 + ++ 使用`math.exp`,它接受一个参数\(x\),并计算\(e^x\)。 + +你可能注意到,最后一个结果与其他两个略有不同。看看你能不能找出哪个是正确的。 + +[《Think Python: 第三版》](https://allendowney.github.io/ThinkPython/index.html) + +版权 2024 [Allen B. Downey](https://allendowney.com) + +代码许可证:[MIT 许可证](https://mit-license.org/) + +文本许可证:[知识共享署名-非商业性使用-相同方式共享 4.0 国际版](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_05.md b/translations/zh/tnkpy3e_05.md new file mode 100644 index 0000000..87301a7 --- /dev/null +++ b/translations/zh/tnkpy3e_05.md @@ -0,0 +1,535 @@ +# 3. 函数 + +> 原文:[`allendowney.github.io/ThinkPython/chap03.html`](https://allendowney.github.io/ThinkPython/chap03.html) + +在上一章中,我们使用了 Python 提供的几个函数,比如 `int` 和 `float`,以及 `math` 模块提供的一些函数,如 `sqrt` 和 `pow`。在这一章中,你将学习如何创建自己的函数并运行它们。我们还将展示一个函数如何调用另一个函数。作为示例,我们将展示《蒙提·派森》歌曲的歌词。这些搞笑的例子展示了一个重要特性——编写自己函数的能力是编程的基础。 + +本章还介绍了一个新的语句——`for` 循环,它用于重复计算。 + +## 3.1. 定义新函数 + +**函数定义**指定了一个新函数的名称以及在调用该函数时运行的语句序列。下面是一个例子: + +```py +def print_lyrics(): + print("I'm a lumberjack, and I'm okay.") + print("I sleep all night and I work all day.") +``` + +`def` 是一个关键字,表示这是一个函数定义。函数的名字是 `print_lyrics`。任何合法的变量名也是合法的函数名。 + +函数名后面的空括号表示该函数不接受任何参数。 + +函数定义的第一行叫做 **头部**,其余部分称为 **函数体**。头部必须以冒号结束,函数体必须缩进。按惯例,缩进通常使用四个空格。这个函数的体部分包含两个打印语句;通常,函数体可以包含任意数量的语句。 + +定义一个函数会创建一个 **函数对象**,我们可以像这样显示它。 + +```py +print_lyrics +``` + +```py + +``` + +输出表明 `print_lyrics` 是一个不接受任何参数的函数。`__main__` 是包含 `print_lyrics` 的模块名。 + +现在我们已经定义了一个函数,我们可以像调用内建函数一样调用它。 + +```py +print_lyrics() +``` + +```py +I'm a lumberjack, and I'm okay. +I sleep all night and I work all day. +``` + +当函数运行时,它会执行函数体中的语句,这些语句会显示《伐木工歌》的前两行。 + +## 3.2. 参数 + +我们看到的一些函数需要参数;例如,当你调用 `abs` 时,你传递一个数字作为参数。一些函数需要多个参数;例如,`math.pow` 需要两个参数,一个是底数,另一个是指数。 + +这是一个接收参数的函数定义。 + +```py +def print_twice(string): + print(string) + print(string) +``` + +括号中的变量名是一个 **参数**。当调用函数时,参数会被赋予实参的值。例如,我们可以这样调用 `print_twice`。 + +```py +print_twice('Dennis Moore, ') +``` + +```py +Dennis Moore, +Dennis Moore, +``` + +运行这个函数的效果与将参数赋值给参数变量,然后执行函数体相同,如下所示。 + +```py +string = 'Dennis Moore, ' +print(string) +print(string) +``` + +```py +Dennis Moore, +Dennis Moore, +``` + +你也可以使用一个变量作为参数。 + +```py +line = 'Dennis Moore, ' +print_twice(line) +``` + +```py +Dennis Moore, +Dennis Moore, +``` + +在这个例子中,`line` 的值被赋给了参数 `string`。 + +## 3.3. 调用函数 + +一旦定义了一个函数,你就可以在另一个函数中使用它。为了演示,我们将编写打印《Spam 歌》歌词的函数([`www.songfacts.com/lyrics/monty-python/the-spam-song`](https://www.songfacts.com/lyrics/monty-python/the-spam-song))。 + +> Spam,Spam,Spam,Spam, +> +> Spam,Spam,Spam,Spam, +> +> Spam,Spam, +> +> (可爱的 Spam,神奇的 Spam!) +> +> Spam,Spam, + +我们从以下函数开始,它接受两个参数。 + +```py +def repeat(word, n): + print(word * n) +``` + +我们可以使用这个函数来打印歌曲的第一行,像这样。 + +```py +spam = 'Spam, ' +repeat(spam, 4) +``` + +```py +Spam, Spam, Spam, Spam, +``` + +为了显示前两行,我们可以定义一个新的函数,使用`repeat`。 + +```py +def first_two_lines(): + repeat(spam, 4) + repeat(spam, 4) +``` + +然后像这样调用它。 + +```py +first_two_lines() +``` + +```py +Spam, Spam, Spam, Spam, +Spam, Spam, Spam, Spam, +``` + +为了显示最后三行,我们可以定义另一个函数,这个函数同样使用`repeat`。 + +```py +def last_three_lines(): + repeat(spam, 2) + print('(Lovely Spam, Wonderful Spam!)') + repeat(spam, 2) +``` + +```py +last_three_lines() +``` + +```py +Spam, Spam, +(Lovely Spam, Wonderful Spam!) +Spam, Spam, +``` + +最后,我们可以通过一个函数将所有内容组合起来,打印出整首诗。 + +```py +def print_verse(): + first_two_lines() + last_three_lines() +``` + +```py +print_verse() +``` + +```py +Spam, Spam, Spam, Spam, +Spam, Spam, Spam, Spam, +Spam, Spam, +(Lovely Spam, Wonderful Spam!) +Spam, Spam, +``` + +当我们运行`print_verse`时,它调用了`first_two_lines`,而`first_two_lines`又调用了`repeat`,`repeat`则调用了`print`。这涉及了很多函数。 + +当然,我们本可以用更少的函数做同样的事情,但这个示例的重点是展示函数如何协同工作。 + +## 3.4\. 重复 + +如果我们想显示多于一段的歌词,可以使用`for`语句。下面是一个简单的示例。 + +```py +for i in range(2): + print(i) +``` + +```py +0 +1 +``` + +第一行是以冒号结尾的头部。第二行是主体,需要缩进。 + +头部以关键字`for`开始,后面跟着一个名为`i`的新变量和另一个关键字`in`。它使用`range`函数创建一个包含两个值的序列,这两个值分别是`0`和`1`。在 Python 中,当我们开始计数时,通常是从`0`开始的。 + +当`for`语句运行时,它将`range`中的第一个值赋给`i`,然后在主体中运行`print`函数,显示`0`。 + +当程序执行到主体末尾时,它会回到头部,这就是为什么这个语句被称为**循环**。第二次进入循环时,它将`range`中的下一个值赋给`i`并显示出来。然后,由于这是`range`中的最后一个值,循环结束。 + +这是我们如何使用`for`循环打印歌曲的两段歌词。 + +```py +for i in range(2): + print("Verse", i) + print_verse() + print() +``` + +```py +Verse 0 +Spam, Spam, Spam, Spam, +Spam, Spam, Spam, Spam, +Spam, Spam, +(Lovely Spam, Wonderful Spam!) +Spam, Spam, + +Verse 1 +Spam, Spam, Spam, Spam, +Spam, Spam, Spam, Spam, +Spam, Spam, +(Lovely Spam, Wonderful Spam!) +Spam, Spam, +``` + +你可以在一个函数内部放置一个`for`循环。例如,`print_n_verses`接受一个名为`n`的参数,该参数必须是整数,并显示给定数量的诗句。 + +```py +def print_n_verses(n): + for i in range(n): + print_verse() + print() +``` + +在这个例子中,我们没有在循环主体中使用`i`,但头部仍然需要有一个变量名。 + +## 3.5\. 变量和参数是局部的 + +当你在函数内部创建一个变量时,它是**局部的**,意味着它只在函数内部存在。例如,下面的函数接受两个参数,将它们连接起来并打印结果两次。 + +```py +def cat_twice(part1, part2): + cat = part1 + part2 + print_twice(cat) +``` + +这是一个使用它的示例: + +```py +line1 = 'Always look on the ' +line2 = 'bright side of life.' +cat_twice(line1, line2) +``` + +```py +Always look on the bright side of life. +Always look on the bright side of life. +``` + +当`cat_twice`运行时,它会创建一个名为`cat`的局部变量,而该变量在函数结束时被销毁。如果我们尝试显示它,就会得到一个`NameError`: + +```py +print(cat) +``` + +```py +NameError: name 'cat' is not defined +``` + +在函数外部,`cat`是未定义的。 + +参数也是局部的。例如,在`cat_twice`外部,没有`part1`或`part2`这样的东西。 + +## 3.6\. 堆栈图 + +为了跟踪哪些变量可以在哪些地方使用,有时画一个**堆栈图**会很有用。像状态图一样,堆栈图展示了每个变量的值,但它们还展示了每个变量所属的函数。 + +每个函数都由一个**框架**表示。框架是一个外面写着函数名称、里面包含函数参数和局部变量的框。 + +这是上一个例子的堆栈图。 + +![_images/02b6ddc296c3c51396cc7c1a916aa9f4ea1bc5ed61b9fe10d6ec63e9b928fc68.png](img/76dde745ad063790ee3d6fa39fb2bf64.png) + +这些框架按照堆栈的顺序排列,表示哪个函数调用了哪个函数,依此类推。从底部开始,`print`由`print_twice`调用,`print_twice`由`cat_twice`调用,`cat_twice`由`__main__`调用——这是最上层框架的一个特殊名称。当你在任何函数外部创建一个变量时,它属于`__main__`。 + +在`print`的框架中,问号表示我们不知道参数的名称。如果你感到好奇,可以问虚拟助手:“Python 的 print 函数的参数是什么?” + +## 3.7\. 追踪栈 + +当函数中发生运行时错误时,Python 会显示正在运行的函数的名称、调用它的函数的名称,依此类推,直到堆栈的顶部。为了看到一个例子,我将定义一个包含错误的`print_twice`版本——它试图打印`cat`,这是另一个函数中的局部变量。 + +```py +def print_twice(string): + print(cat) # NameError + print(cat) +``` + +现在让我们来看一下运行`cat_twice`时会发生什么。 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs, including a traceback. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +```py +cat_twice(line1, line2) +``` + +```py +--------------------------------------------------------------------------- +NameError Traceback (most recent call last) +Cell In[27], line 1 +----> 1 cat_twice(line1, line2) + line1 = 'Always look on the ' + line2 = 'bright side of life.' + +Cell In[20], line 3, in cat_twice(part1='Always look on the ', part2='bright side of life.') + 1 def cat_twice(part1, part2): + 2 cat = part1 + part2 +----> 3 print_twice(cat) + cat = 'Always look on the bright side of life.' + +Cell In[25], line 2, in print_twice(string='Always look on the bright side of life.') + 1 def print_twice(string): +----> 2 print(cat) # NameError + 3 print(cat) + +NameError: name 'cat' is not defined +``` + +错误信息包含一个**追踪栈**,显示了错误发生时正在运行的函数、调用该函数的函数等。在这个例子中,它显示了`cat_twice`调用了`print_twice`,并且错误发生在`print_twice`中。 + +追踪栈中函数的顺序与堆栈图中框架的顺序相同。正在运行的函数位于底部。 + +## 3.8\. 为什么要使用函数? + +可能还不清楚为什么将程序划分为多个函数值得花费精力。这里有几个原因: + ++ 创建一个新函数让你有机会为一组语句命名,这使得程序更易于阅读和调试。 + ++ 函数可以通过消除重复的代码使程序变得更小。以后,如果需要修改,你只需在一个地方做出更改。 + ++ 将一个长程序拆分成多个函数可以让你逐个调试各个部分,然后将它们组合成一个完整的工作程序。 + ++ 设计良好的函数通常对许多程序都有用。一旦你写并调试了一个函数,你可以重用它。 + +## 3.9\. 调试 + +调试可能令人沮丧,但它也充满挑战、有趣,有时甚至是令人愉快的。而且它是你可以学习的最重要的技能之一。 + +从某种意义上说,调试就像侦探工作。你会得到线索,然后推测出导致你看到的结果的事件。 + +调试也像实验科学。一旦你对发生了什么有了一些想法,你就修改程序并再次尝试。如果你的假设是正确的,你就能预测修改的结果,并且离一个可用的程序更近一步。如果假设错了,你就得提出新的假设。 + +对某些人来说,编程和调试是同一回事;也就是说,编程是逐步调试程序,直到它按你想要的方式工作。这个想法是你应该从一个能正常工作的程序开始,然后逐步进行小的修改,并在修改时调试它们。 + +如果你发现自己花了很多时间调试,这通常是一个信号,说明你在开始测试之前写了太多的代码。如果你采取更小的步骤,你可能会发现自己能更快地前进。 + +## 3.10\. 词汇表 + +**function definition:** 创建函数的语句。 + +**header:** 函数定义的第一行。 + +**body:** 函数定义内部的语句序列。 + +**function object:** 通过函数定义创建的值。函数的名称是一个引用函数对象的变量。 + +**parameter:** 在函数内部用于引用作为参数传递的值的名称。 + +**loop:** 一个运行一个或多个语句的语句,通常是重复的。 + +**local variable:** 在函数内部定义的变量,只能在函数内部访问。 + +**stack diagram:** 函数堆栈的图形表示,显示了它们的变量以及它们引用的值。 + +**frame:** 堆栈图中的一个框,表示一个函数调用。它包含该函数的局部变量和参数。 + +**traceback:** 当发生异常时打印的正在执行的函数列表。 + +## 3.11\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +### 3.11.1\. 向虚拟助手提问 + +函数或`for`循环中的语句按照约定缩进四个空格。但并非所有人都同意这一约定。如果你对这一伟大的争论的历史感到好奇,可以让虚拟助手“告诉我关于 Python 中的空格和制表符”。 + +虚拟助手在编写小函数方面非常擅长。 + +1. 请让你喜欢的虚拟助手“编写一个名为 repeat 的函数,它接收一个字符串和一个整数,并将该字符串打印指定的次数。” + +1. 如果结果使用了`for`循环,你可以问:“能不能不用`for`循环?” + +1. 从本章中任选一个其他函数,并请虚拟助手编写它。挑战在于准确描述函数,以便得到你想要的结果。使用你在本书中学到的词汇。 + +虚拟助手在调试函数方面也非常擅长。 + +1. 询问 VA,这个`print_twice`版本有什么问题。 + + ```py + def print_twice(string): + print(cat) + print(cat) + ``` + +如果您在以下任何练习中遇到困难,请考虑向 VA 寻求帮助。 + +### 3.11.2\. 练习 + +编写一个名为`print_right`的函数,它以名为`text`的字符串作为参数,并打印字符串,使得字符串的最后一个字母位于显示的第 40 列。 + +提示:使用`len`函数、字符串连接运算符(`+`)和字符串重复运算符(`*`)。 + +这里有一个示例展示它应该如何工作。 + +```py +print_right("Monty") +print_right("Python's") +print_right("Flying Circus") +``` + +```py + Monty + Python's + Flying Circus +``` + +### 3.11.3\. 练习 + +编写一个名为`triangle`的函数,它接受一个字符串和一个整数,并绘制一个具有给定高度的金字塔,由字符串的副本组成。这里有一个使用字符串`'L'`的 5 级金字塔的示例。 + +```py +triangle('L', 5) +``` + +```py +L +LL +LLL +LLLL +LLLLL +``` + +### 3.11.4\. 练习 + +编写一个名为`rectangle`的函数,它接受一个字符串和两个整数,并绘制一个具有给定宽度和高度的矩形,由字符串的副本组成。这里有一个宽度为`5`,高度为`4`的矩形的示例,由字符串`'H'`组成。 + +```py +rectangle('H', 5, 4) +``` + +```py +HHHHH +HHHHH +HHHHH +HHHHH +``` + +### 3.11.5\. 练习 + +歌曲“99 瓶啤酒”以这首诗歌开始: + +> 墙上有 99 瓶啤酒 +> +> 99 瓶啤酒 +> +> 拿一个下来,传递它 +> +> 墙上有 98 瓶啤酒 + +然后第二节是一样的,只是从 98 瓶开始,以 97 结束。歌曲会继续——很长时间——直到没有啤酒为止。 + +编写一个名为`bottle_verse`的函数,它以一个数字作为参数,并显示以给定数量的瓶子开头的诗句。 + +提示:考虑从能够打印诗歌的第一、第二或最后一行的函数开始,然后使用它来编写`bottle_verse`。 + +使用这个函数调用来显示第一节。 + +```py +bottle_verse(99) +``` + +```py +99 bottles of beer on the wall +99 bottles of beer +Take one down, pass it around +98 bottles of beer on the wall +``` + +如果你想打印整首歌,可以使用这个`for`循环,它从`99`数到`1`。你不必完全理解这个例子——我们稍后会更详细地了解`for`循环和`range`函数。 + +```py +for n in range(99, 0, -1): + bottle_verse(n) + print() +``` + +[Think Python: 3rd Edition](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 年 [Allen B. Downey](https://allendowney.com) + +代码许可:[MIT 许可证](https://mit-license.org/) + +文本许可证:[知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_06.md b/translations/zh/tnkpy3e_06.md new file mode 100644 index 0000000..ef8829e --- /dev/null +++ b/translations/zh/tnkpy3e_06.md @@ -0,0 +1,449 @@ +# 4\. 函数与接口 + +> 原文:[`allendowney.github.io/ThinkPython/chap04.html`](https://allendowney.github.io/ThinkPython/chap04.html) + +本章介绍了一个名为`jupyturtle`的模块,它允许你通过给一只虚拟的海龟下指令来创建简单的图形。我们将使用这个模块编写绘制正方形、多边形和圆形的函数,并演示**接口设计**,这是一种设计能协同工作的函数的方式。 + +## 4.1\. jupyturtle 模块 + +要使用`jupyturtle`模块,我们可以这样导入它。 + +```py +import jupyturtle +``` + +现在我们可以使用模块中定义的函数,如`make_turtle`和`forward`。 + +```py +jupyturtle.make_turtle() +jupyturtle.forward(100) +``` + +`make_turtle`创建一个**画布**,这是屏幕上可以绘图的区域,并且创建一只海龟,海龟通过一个圆形的壳和一个三角形的头来表示。圆形表示海龟的位置,三角形表示它面朝的方向。 + +`forward`指令让海龟沿着它面朝的方向移动指定的距离,并在此过程中绘制一条线段。这个距离使用的是任意单位,实际的大小取决于你的计算机屏幕。 + +我们将多次使用`jupyturtle`模块中定义的函数,所以如果我们每次都不必写模块名,那就更方便了。如果我们像这样导入模块,这样做是可能的。 + +```py +from jupyturtle import make_turtle, forward +``` + +这个版本的导入语句从`jupyturtle`模块导入了`make_turtle`和`forward`,这样我们就可以像这样调用它们。 + +```py +make_turtle() +forward(100) +``` + +`jupyturtle`还提供了另外两个函数,我们将使用它们,分别是`left`和`right`。我们将这样导入它们。 + +```py +from jupyturtle import left, right +``` + +`left`指令使海龟向左转动。它接受一个参数,表示转动的角度,单位是度。例如,我们可以这样使海龟左转 90 度。 + +```py +make_turtle() +forward(50) +left(90) +forward(50) +``` + +这个程序使海龟先向东移动然后向北移动,留下了两条线段。在继续之前,试试看能否修改之前的程序,绘制一个正方形。 + +## 4.2\. 绘制一个正方形 + +这是绘制正方形的一种方式。 + +```py +make_turtle() + +forward(50) +left(90) + +forward(50) +left(90) + +forward(50) +left(90) + +forward(50) +left(90) +``` + +因为这个程序会重复执行相同的一对语句四次,我们可以通过`for`循环来更简洁地实现同样的效果。 + +```py +make_turtle() +for i in range(4): + forward(50) + left(90) +``` + +## 4.3\. 封装与泛化 + +让我们将上一节的绘制正方形的代码放到一个名为`square`的函数里。 + +```py +def square(): + for i in range(4): + forward(50) + left(90) +``` + +现在我们可以这样调用这个函数。 + +```py +make_turtle() +square() +``` + +将一段代码封装到一个函数中叫做**封装**。封装的好处之一是它为代码附上了一个名称,这可以作为一种文档说明。另一个好处是,如果你要重复使用这段代码,调用函数比复制粘贴函数体要简洁得多! + +在当前版本中,正方形的大小始终是`50`。如果我们想绘制不同大小的正方形,可以将边长作为参数传入。 + +```py +def square(length): + for i in range(4): + forward(length) + left(90) +``` + +现在我们可以绘制不同大小的正方形了。 + +```py +make_turtle() +square(30) +square(60) +``` + +向函数添加一个参数叫做**泛化**,因为它使得函数变得更加通用:在之前的版本中,正方形的大小总是一样的;而在这个版本中,它可以是任意大小。 + +如果我们添加另一个参数,我们可以使它更通用。以下函数绘制具有给定边数的规则多边形。 + +```py +def polygon(n, length): + angle = 360 / n + for i in range(n): + forward(length) + left(angle) +``` + +在一个具有`n`条边的规则多边形中,相邻边之间的角度是`360 / n`度。 + +以下示例绘制一个具有 7 条边、边长为 30 的多边形。 + +```py +make_turtle() +polygon(7, 30) +``` + +当一个函数有很多数值型参数时,很容易忘记它们是什么,或者它们应该按什么顺序排列。一个好主意是,在参数列表中包含参数的名称。 + +```py +make_turtle() +polygon(n=7, length=30) +``` + +这些有时被称为“命名参数”,因为它们包括了参数名称。但在 Python 中,它们更常被称为**关键字参数**(不要与 Python 中的保留字如`for`和`def`混淆)。 + +这种赋值运算符`=`的使用提醒我们参数和参数列表的工作方式——当你调用一个函数时,实参会被赋值给形参。 + +## 4.4\. 近似圆形 + +现在假设我们要画一个圆。我们可以通过画一个边数非常多的多边形来近似画圆,这样每一条边足够小,几乎看不见。这里有一个函数,使用`polygon`绘制一个具有`30`条边的多边形,近似一个圆。 + +```py +import math + +def circle(radius): + circumference = 2 * math.pi * radius + n = 30 + length = circumference / n + polygon(n, length) +``` + +`circle`接受圆的半径作为参数。它计算`circumference`,即具有给定半径的圆的周长。`n`是边数,所以`circumference / n`是每条边的长度。 + +这个函数可能需要很长时间才能运行。我们可以通过使用一个名为`delay`的关键字参数来加速它,该参数设置海龟每一步后等待的时间(以秒为单位)。默认值是`0.2`秒——如果我们将其设置为`0.02`秒,运行速度大约快 10 倍。 + +```py +make_turtle(delay=0.02) +circle(30) +``` + +这个解决方案的一个局限性是`n`是一个常量,这意味着对于非常大的圆,边太长了,而对于小圆,我们浪费时间绘制非常短的边。一个选择是通过将`n`作为参数来泛化这个函数。但现在我们暂时保持简单。 + +## 4.5\. 重构 + +现在让我们写一个更通用的`circle`版本,叫做`arc`,它接受第二个参数`angle`,并绘制一个跨度为给定角度的圆弧。例如,如果`angle`是`360`度,它绘制一个完整的圆。如果`angle`是`180`度,它绘制一个半圆。 + +为了编写`circle`,我们能够重用`polygon`,因为多边形的边数多时是圆的一个良好近似。但我们不能用`polygon`来编写`arc`。 + +相反,我们将创建`polygon`的更通用版本,叫做`polyline`。 + +```py +def polyline(n, length, angle): + for i in range(n): + forward(length) + left(angle) +``` + +`polyline`接受三个参数:要绘制的线段数`n`,线段长度`length`,以及它们之间的角度`angle`。 + +现在,我们可以重写`polygon`来使用`polyline`。 + +```py +def polygon(n, length): + angle = 360.0 / n + polyline(n, length, angle) +``` + +我们可以使用`polyline`来绘制`arc`。 + +```py +def arc(radius, angle): + arc_length = 2 * math.pi * radius * angle / 360 + n = 30 + length = arc_length / n + step_angle = angle / n + polyline(n, length, step_angle) +``` + +`arc`类似于`circle`,只是它计算`arc_length`,即圆周的一部分。 + +最后,我们可以重写`circle`来使用`arc`。 + +```py +def circle(radius): + arc(radius, 360) +``` + +为了检查这些函数是否按预期工作,我们将用它们画出像蜗牛一样的图形。使用`delay=0`时,海龟运行得尽可能快。 + +```py +make_turtle(delay=0) +polygon(n=20, length=9) +arc(radius=70, angle=70) +circle(radius=10) +``` + +在这个例子中,我们从有效的代码开始,并通过不同的函数重新组织它。像这样的改变,通过改进代码而不改变其行为,称为**重构**。 + +如果我们提前规划,可能会先编写`polyline`并避免重构,但通常在项目开始时,你还不知道足够多的内容来设计所有的函数。一旦开始编写代码,你会更好地理解问题。有时候,重构是你已经学到一些东西的标志。 + +## 4.6\. 堆栈图 + +当我们调用`circle`时,它会调用`arc`,而`arc`又会调用`polyline`。我们可以使用堆栈图来展示这一系列的函数调用及每个函数的参数。 + +![_images/92e303702d06597847739633fef20d2b08ccd373273752d5cbf8c1c93eaeb26d.png](img/92d83d3a339d69adaf61582f205196f6.png) + +请注意,`polyline`中的`angle`值与`arc`中的`angle`值不同。参数是局部的,这意味着你可以在不同的函数中使用相同的参数名;它在每个函数中都是一个不同的变量,并且可能指向不同的值。 + +## 4.7\. 开发计划 + +**开发计划**是编写程序的过程。我们在这一章中使用的过程是“封装与泛化”。这个过程的步骤如下: + +1. 首先编写一个没有函数定义的小程序。 + +1. 一旦你让程序正常工作,找出其中的一个连贯部分,将其封装成一个函数并为其命名。 + +1. 通过添加适当的参数来泛化函数。 + +1. 重复步骤 1 到 3,直到你有一组有效的函数。 + +1. 寻找通过重构改进程序的机会。例如,如果你在多个地方有相似的代码,考虑将它提取到一个适当的通用函数中。 + +这个过程有一些缺点——稍后我们会看到一些替代方法——但是如果你事先不知道如何将程序分解成函数,它是有用的。这个方法让你在编写过程中逐步设计。 + +函数的设计有两个部分: + ++ **接口**是指函数的使用方式,包括它的名称、它接受的参数以及它应该做什么。 + ++ **实现**是指函数如何完成其预定的任务。 + +例如,这是我们编写的第一个版本的`circle`,它使用了`polygon`。 + +```py +def circle(radius): + circumference = 2 * math.pi * radius + n = 30 + length = circumference / n + polygon(n, length) +``` + +这是使用`arc`的重构版本。 + +```py +def circle(radius): + arc(radius, 360) +``` + +这两个函数有相同的接口——它们接受相同的参数并做相同的事情——但它们的实现不同。 + +## 4.8\. 文档字符串 + +**文档字符串**是函数开头的字符串,用于解释接口(“doc”是“documentation”的缩写)。下面是一个例子: + +```py +def polyline(n, length, angle): + """Draws line segments with the given length and angle between them. + + n: integer number of line segments + length: length of the line segments + angle: angle between segments (in degrees) + """ + for i in range(n): + forward(length) + left(angle) +``` + +按惯例,文档字符串是三引号括起来的字符串,也称为**多行字符串**,因为三引号允许字符串跨越多行。 + +文档字符串应该: + ++ 简洁地说明函数的作用,而不深入细节说明它是如何工作的, + ++ 解释每个参数对函数行为的影响,并且 + ++ 如果参数类型不明显,请指明每个参数应是什么类型。 + +编写这类文档是接口设计的重要部分。设计良好的接口应该简洁易懂;如果你很难解释你的函数,可能是接口设计有待改进。 + +## 4.9\. 调试 + +接口就像是函数和调用者之间的契约。调用者同意提供某些参数,函数同意执行某些操作。 + +例如,`polyline` 函数需要三个参数:`n` 必须是整数;`length` 应该是正数;`angle` 必须是一个数值,且理解为角度单位是度。 + +这些要求被称为**前置条件**,因为它们应在函数开始执行之前为真。相反,函数结束时的条件是**后置条件**。后置条件包括函数的预期效果(比如绘制线段)和任何副作用(比如移动海龟或进行其他更改)。 + +前置条件由调用者负责。如果调用者违反了前置条件,导致函数不能正常工作,那么错误在调用者,而不是函数本身。 + +如果前置条件满足而后置条件不满足,则说明问题出在函数中。如果你的前置条件和后置条件明确,它们可以帮助调试。 + +## 4.10\. 术语表 + +**接口设计:** 设计函数接口的过程,其中包括函数应接受的参数。 + +**画布:** 用于显示图形元素的窗口,包括线条、圆形、矩形和其他形状。 + +**封装:** 将一系列语句转化为函数定义的过程。 + +**泛化:** 将某些不必要的具体内容(如一个数字)替换为适当的一般内容(如一个变量或参数)的过程。 + +**关键字参数:** 包括参数名称的参数。 + +**重构:** 修改一个已工作的程序,以改善函数接口和代码的其他质量的过程。 + +**开发计划:** 编写程序的过程。 + +**文档字符串:** 出现在函数定义顶部的字符串,用于记录函数的接口。 + +**多行字符串:** 用三引号括起来的字符串,可以跨越程序中的多行。 + +**前置条件:** 函数开始前调用者应满足的要求。 + +**后置条件:** 函数结束前应该满足的要求。 + +## 4.11\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +对于以下练习,可能有一些额外的海龟函数你可能想要使用。 + ++ `penup`将海龟的虚拟笔抬起,这样它在移动时不会留下轨迹。 + ++ `pendown`将笔放下。 + +以下函数使用`penup`和`pendown`来移动海龟而不留下轨迹。 + +```py +from jupyturtle import penup, pendown + +def jump(length): + """Move forward length units without leaving a trail. + + Postcondition: Leaves the pen down. + """ + penup() + forward(length) + pendown() +``` + +### 4.11.1\. 练习 + +编写一个名为`rectangle`的函数,绘制一个给定边长的矩形。例如,下面是一个宽度为`80`单位,高度为`40`单位的矩形。 + +### 4.11.2\. 练习 + +编写一个名为`rhombus`的函数,绘制一个给定边长和内角的菱形。例如,下面是一个边长为`50`,内角为`60`度的菱形。 + +### 4.11.3\. 练习 + +现在编写一个更通用的函数,名为`parallelogram`,绘制一个具有平行边的四边形。然后重写`rectangle`和`rhombus`,使其使用`parallelogram`。 + +### 4.11.4\. 练习 + +编写一组适当通用的函数,能够绘制像这样的形状。 + +![](img/73a457cfb8b88465b8d35a8d99cfe5fe.png) + +提示:编写一个名为`triangle`的函数,绘制一个三角形段,然后编写一个名为`draw_pie`的函数,使用`triangle`。 + +### 4.11.5\. 练习 + +编写一组适当通用的函数,能够绘制像这样的花朵。 + +![](img/04840afbf96285b3c181f739f961b9d1.png) + +提示:使用`arc`编写一个名为`petal`的函数,绘制一片花瓣。 + +### 4.11.6\. 请虚拟助手帮忙 + +Python 中有几个像`jupyturtle`这样的模块,我们在本章中使用的模块是为本书定制的。所以如果你请虚拟助手帮忙,它可能不知道使用哪个模块。但如果你给它一些示例,它应该能够弄明白。例如,试试这个提示,看看它能否写出一个绘制螺旋的函数: + +```py +The following program uses a turtle graphics module to draw a circle: + +from jupyturtle import make_turtle, forward, left +import math + +def polygon(n, length): + angle = 360 / n + for i in range(n): + forward(length) + left(angle) + +def circle(radius): + circumference = 2 * math.pi * radius + n = 30 + length = circumference / n + polygon(n, length) + +make_turtle(delay=0) +circle(30) + +Write a function that draws a spiral. +``` + +请记住,结果可能使用了我们还没有见过的功能,并且可能包含错误。复制虚拟助手的代码,看看能否使其工作。如果没有得到你想要的结果,试着修改提示。 + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可:[MIT License](https://mit-license.org/) + +文字许可:[Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_07.md b/translations/zh/tnkpy3e_07.md new file mode 100644 index 0000000..f4b2846 --- /dev/null +++ b/translations/zh/tnkpy3e_07.md @@ -0,0 +1,818 @@ +# 5. 条件语句与递归 + +> 原文:[`allendowney.github.io/ThinkPython/chap05.html`](https://allendowney.github.io/ThinkPython/chap05.html) + +本章的主要主题是`if`语句,根据程序的状态执行不同的代码。通过`if`语句,我们将能够探索计算机科学中的一个强大概念——**递归**。 + +但我们将从三个新特性开始:取模运算符、布尔表达式和逻辑运算符。 + +## 5.1. 整数除法与取模 + +回想一下,整数除法运算符`//`将两个数字相除并向下舍入为整数。例如,假设一部电影的放映时间是 105 分钟,你可能想知道这是多少小时。常规除法返回一个浮动数值: + +```py +minutes = 105 +minutes / 60 +``` + +```py +1.75 +``` + +但是我们通常不写带小数点的小时数。整数除法返回整数小时数,并向下舍入: + +```py +minutes = 105 +hours = minutes // 60 +hours +``` + +```py +1 +``` + +要得到余数,你可以用分钟形式减去一小时: + +```py +remainder = minutes - hours * 60 +remainder +``` + +```py +45 +``` + +或者你可以使用**取模运算符**,`%`,它将两个数字相除并返回余数。 + +```py +remainder = minutes % 60 +remainder +``` + +```py +45 +``` + +取模运算符比看起来更有用。例如,它可以检查一个数字是否能被另一个数字整除——如果`x % y`为零,那么`x`能被`y`整除。 + +同时,它还可以提取数字的最右边一位或几位。例如,`x % 10`返回`x`的最右一位数字(以十进制表示)。类似地,`x % 100`返回最后两位数字。 + +```py +x = 123 +x % 10 +``` + +```py +3 +``` + +```py +x % 100 +``` + +```py +23 +``` + +最后,取模运算符可以进行“钟表算术”。例如,如果一个事件从上午 11 点开始并持续三小时,我们可以使用取模运算符来计算它结束的时间。 + +```py +start = 11 +duration = 3 +end = (start + duration) % 12 +end +``` + +```py +2 +``` + +该事件将于下午 2 点结束。 + +## 5.2. 布尔表达式 + +**布尔表达式**是一个值为真或假的表达式。例如,下面的表达式使用了等于运算符`==`,它比较两个值,如果它们相等则返回`True`,否则返回`False`: + +```py +5 == 5 +``` + +```py +True +``` + +```py +5 == 7 +``` + +```py +False +``` + +一个常见的错误是使用单个等号(`=`)而不是双等号(`==`)。记住,`=`是将值赋给变量,而`==`是比较两个值。 + +```py +x = 5 +y = 7 +``` + +```py +x == y +``` + +```py +False +``` + +`True`和`False`是属于`bool`类型的特殊值;它们不是字符串: + +```py +type(True) +``` + +```py +bool +``` + +```py +type(False) +``` + +```py +bool +``` + +`==`运算符是**关系运算符**之一;其他的有: + +```py +x != y # x is not equal to y +``` + +```py +True +``` + +```py +x > y # x is greater than y +``` + +```py +False +``` + +```py +x < y # x is less than to y +``` + +```py +True +``` + +```py +x >= y # x is greater than or equal to y +``` + +```py +False +``` + +```py +x <= y # x is less than or equal to y +``` + +```py +True +``` + +## 5.3. 逻辑运算符 + +要将布尔值组合成表达式,我们可以使用**逻辑运算符**。最常见的有`and`、`or`和`not`。这些运算符的意义与它们在英语中的含义相似。例如,下面的表达式的值为`True`,当且仅当`x`大于`0` *并且*小于`10`。 + +```py +x > 0 and x < 10 +``` + +```py +True +``` + +如果*任一或两个*条件为真,下面的表达式的值为`True`,即如果数字能被 2 *或* 3 整除: + +```py +x % 2 == 0 or x % 3 == 0 +``` + +```py +False +``` + +最后,`not`运算符否定一个布尔表达式,因此如果`x > y`为`False`,下面的表达式将是`True`。 + +```py +not x > y +``` + +```py +True +``` + +严格来说,逻辑运算符的操作数应该是布尔表达式,但 Python 并不严格。任何非零数字都会被解释为`True`: + +```py +42 and True +``` + +```py +True +``` + +这种灵活性可能很有用,但它有一些细节可能让人困惑。你可能会想避免使用它。 + +## 5.4\. if 语句 + +为了编写有用的程序,我们几乎总是需要能够检查条件,并根据条件改变程序的行为。**条件语句**赋予我们这个能力。最简单的形式是`if`语句: + +```py +if x > 0: + print('x is positive') +``` + +```py +x is positive +``` + +`if`是 Python 的关键字。`if`语句的结构与函数定义相同:一个头部,后跟一个缩进的语句或语句序列,称为**块**。 + +`if`后面的布尔表达式称为**条件**。如果条件为真,缩进块中的语句会执行。如果条件为假,则不执行。 + +在块中可以包含任意数量的语句,但必须至少包含一个。有时,创建一个什么也不做的块是有用的——通常是作为你还未编写代码的占位符。在这种情况下,你可以使用`pass`语句,它什么也不做。 + +```py +if x < 0: + pass # TODO: need to handle negative values! +``` + +注释中的`TODO`字样是一个约定,提醒你稍后需要做某事。 + +## 5.5\. `else`子句 + +一个`if`语句可以有第二部分,称为`else`子句。语法如下: + +```py +if x % 2 == 0: + print('x is even') +else: + print('x is odd') +``` + +```py +x is odd +``` + +如果条件为真,则执行第一个缩进的语句;否则,执行第二个缩进的语句。 + +在这个例子中,如果`x`是偶数,那么`x`除以`2`的余数是`0`,所以条件为真,程序显示`x is even`。如果`x`是奇数,则余数是`1`,条件为假,程序显示`x is odd`。 + +由于条件必须为真或假,最终只有一个分支会被执行。分支被称为**分支**。 + +## 5.6\. 链式条件语句 + +有时可能存在多于两个的可能性,需要更多的分支。表达这种计算的一种方式是**链式条件语句**,它包含一个`elif`子句。 + +```py +if x < y: + print('x is less than y') +elif x > y: + print('x is greater than y') +else: + print('x and y are equal') +``` + +```py +x is less than y +``` + +`elif`是“else if”的缩写。`elif`子句的数量没有限制。如果有`else`子句,它必须位于最后,但不必存在。 + +每个条件都会按顺序检查。如果第一个条件为假,则检查下一个,以此类推。如果其中一个条件为真,则执行相应的分支,`if`语句结束。即使有多个条件为真,也只有第一个为真的分支会执行。 + +## 5.7\. 嵌套条件语句 + +一个条件语句也可以嵌套在另一个条件语句中。我们可以像这样重新编写前一节中的例子: + +```py +if x == y: + print('x and y are equal') +else: + if x < y: + print('x is less than y') + else: + print('x is greater than y') +``` + +```py +x is less than y +``` + +外部的`if`语句包含了两个分支。第一个分支包含一个简单的语句。第二个分支包含另一个`if`语句,它有自己的两个分支。那两个分支都是简单语句,尽管它们也可以是条件语句。 + +尽管语句的缩进使得结构变得清晰,**嵌套条件语句**可能仍然难以阅读。我建议你尽量避免使用它们。 + +逻辑运算符通常提供了一种简化嵌套条件语句的方法。这里是一个包含嵌套条件的例子。 + +```py +if 0 < x: + if x < 10: + print('x is a positive single-digit number.') +``` + +```py +x is a positive single-digit number. +``` + +只有当我们通过了两个条件判断,`print`语句才会执行,因此我们可以通过`and`运算符获得相同的效果。 + +```py +if 0 < x and x < 10: + print('x is a positive single-digit number.') +``` + +```py +x is a positive single-digit number. +``` + +对于这种情况,Python 提供了一种更简洁的选项: + +```py +if 0 < x < 10: + print('x is a positive single-digit number.') +``` + +```py +x is a positive single-digit number. +``` + +## 5.8\. 递归 + +一个函数调用自身是合法的。虽然它为什么是个好事可能不那么显而易见,但事实证明,这是程序能够做的最神奇的事情之一。这里有一个例子。 + +```py +def countdown(n): + if n <= 0: + print('Blastoff!') + else: + print(n) + countdown(n-1) +``` + +如果`n`为 0 或负数,`countdown`输出单词“Blastoff!”否则,它输出`n`,然后调用自身,传递`n-1`作为参数。 + +这是当我们以参数`3`调用此函数时发生的情况。 + +```py +countdown(3) +``` + +```py +3 +2 +1 +Blastoff! +``` + +`countdown`的执行从`n=3`开始,由于`n`大于`0`,它显示`3`,然后调用自身。… + +> `countdown`的执行从`n=2`开始,由于`n`大于`0`,它显示`2`,然后调用自身。… +> +> > `countdown`的执行从`n=1`开始,由于`n`大于`0`,它显示`1`,然后调用自身。… +> > +> > > `countdown`的执行从`n=0`开始,由于`n`不大于`0`,它显示“Blastoff!”并返回。 +> > > +> > 得到`n=1`的`countdown`返回。 +> > +> 得到`n=2`的`countdown`返回。 + +得到`n=3`的`countdown`返回。 + +一个调用自身的函数是**递归的**。作为另一个例子,我们可以写一个函数,打印字符串`n`次。 + +```py +def print_n_times(string, n): + if n > 0: + print(string) + print_n_times(string, n-1) +``` + +如果`n`为正数,`print_n_times`会显示`string`的值,然后调用自身,传递`string`和`n-1`作为参数。 + +如果`n`为`0`或负数,条件为假,`print_n_times`什么也不做。 + +下面是它的工作原理。 + +```py +print_n_times('Spam ', 4) +``` + +```py +Spam +Spam +Spam +Spam +``` + +对于像这样的简单例子,可能使用`for`循环会更容易。但稍后我们会看到一些使用`for`循环很难编写而递归容易编写的例子,所以早点开始学习递归是有益的。 + +## 5.9\. 递归函数的栈图 + +这是一个栈图,展示了当我们用`n=3`调用`countdown`时创建的框架。 + +![_images/e6406e06456c8a30f02728ca9ab2416d72ded3e8d84449e77a93f601da23cedc.png](img/5dabd26c3a8ec6a56a8c98d2081490cb.png) + +四个`countdown`框架的参数`n`值各不相同。栈底部,即`n=0`的地方,称为**基准情况**。它不再做递归调用,因此没有更多的框架。 + +## 5.10\. 无限递归 + +如果递归永远无法到达基准情况,它将不断进行递归调用,程序也永远不会结束。这被称为**无限递归**,通常来说,这种情况是不推荐的。下面是一个包含无限递归的最小函数。 + +```py +def recurse(): + recurse() +``` + +每当`recurse`被调用时,它会调用自己,这样就会创建另一个栈帧。在 Python 中,栈上同时存在的栈帧数量是有限制的。如果程序超出了这个限制,就会导致运行时错误。 + +```py +recurse() +``` + +```py +--------------------------------------------------------------------------- +RecursionError Traceback (most recent call last) +Cell In[40], line 1 +----> 1 recurse() + +Cell In[38], line 2, in recurse() + 1 def recurse(): +----> 2 recurse() + +Cell In[38], line 2, in recurse() + 1 def recurse(): +----> 2 recurse() + + [... skipping similar frames: recurse at line 2 (2957 times)] + +Cell In[38], line 2, in recurse() + 1 def recurse(): +----> 2 recurse() + +RecursionError: maximum recursion depth exceeded +``` + +错误追踪信息显示,错误发生时栈上几乎有 3000 个栈帧。 + +如果不小心遇到无限递归,检查你的函数,确认是否有一个不进行递归调用的基准情况。如果有基准情况,检查是否能够保证到达它。 + +## 5.11\. 键盘输入 + +到目前为止,我们编写的程序没有接收任何来自用户的输入。它们每次都会做相同的事情。 + +Python 提供了一个内置函数叫做`input`,它会暂停程序并等待用户输入。当用户按下*Return*或*Enter*键时,程序恢复执行,`input`会返回用户输入的内容作为字符串。 + +```py +text = input() +``` + +在获取用户输入之前,你可能想要显示一个提示,告诉用户应该输入什么。`input`可以接受一个提示作为参数: + +```py +name = input('What...is your name?\n') +name +``` + +```py +What...is your name? +It is Arthur, King of the Britons +``` + +```py +'It is Arthur, King of the Britons' +``` + +提示末尾的序列`\n`表示**换行符**,它是一个特殊字符,导致换行——这样用户的输入就会显示在提示的下方。 + +如果你期望用户输入一个整数,可以使用`int`函数将返回值转换为`int`。 + +```py +prompt = 'What...is the airspeed velocity of an unladen swallow?\n' +speed = input(prompt) +speed +``` + +```py +What...is the airspeed velocity of an unladen swallow? +What do you mean: an African or European swallow? +``` + +```py +'What do you mean: an African or European swallow?' +``` + +但如果用户输入了非整数的内容,你将得到一个运行时错误。 + +```py +int(speed) +``` + +```py +ValueError: invalid literal for int() with base 10: 'What do you mean: an African or European swallow?' +``` + +我们将在后面学习如何处理这种类型的错误。 + +## 5.12\. 调试 + +当出现语法错误或运行时错误时,错误消息包含了大量信息,但可能会让人感到不知所措。通常最有用的部分是: + ++ 错误的类型是什么,以及 + ++ 错误发生的位置。 + +语法错误通常很容易找到,但也有一些陷阱。与空格和制表符相关的错误可能会很棘手,因为它们是不可见的,而我们习惯于忽略它们。 + +```py +x = 5 + y = 6 +``` + +```py + Cell In[49], line 2 + y = 6 + ^ +IndentationError: unexpected indent +``` + +在这个例子中,问题在于第二行缩进了一个空格。但是错误信息指向了`y`,这很具有误导性。错误消息指示问题被发现的位置,但实际的错误可能出现在代码的更早部分。 + +运行时错误也有类似情况。例如,假设你尝试将一个比率转换为分贝,如下所示: + +```py +import math +numerator = 9 +denominator = 10 +ratio = numerator // denominator +decibels = 10 * math.log10(ratio) +``` + +```py +--------------------------------------------------------------------------- +ValueError Traceback (most recent call last) +Cell In[51], line 5 + 3 denominator = 10 + 4 ratio = numerator // denominator +----> 5 decibels = 10 * math.log10(ratio) + +ValueError: math domain error +``` + +错误信息显示的是第 5 行,但那一行没有问题。问题出在第 4 行,那里使用了整数除法而不是浮点数除法——结果是`ratio`的值为`0`。当我们调用`math.log10`时,会得到一个`ValueError`,错误信息为`math domain error`,因为`0`不在`math.log10`的有效参数“域”内,因为`0`的对数是未定义的。 + +一般来说,你应该花时间仔细阅读错误信息,但不要假设它们说的每句话都是正确的。 + +## 5.13\. 词汇表 + +**递归:** 调用当前正在执行的函数的过程。 + +**取模运算符:** 一个运算符`%`,用于整数,并返回一个数字除以另一个数字后的余数。 + +**布尔表达式:** 其值为`True`或`False`的表达式。 + +**关系运算符:** 用于比较操作数的运算符:`==`、`!=`、`>`、`<`、`>=`和`<=`。 + +**逻辑运算符:** 用于组合布尔表达式的运算符,包括`and`、`or`和`not`。 + +**条件语句:** 根据某些条件控制执行流程的语句。 + +**条件:** 条件语句中的布尔表达式,决定执行哪个分支。 + +**代码块:** 一个或多个缩进的语句,表示它们是另一个语句的一部分。 + +**分支:** 条件语句中的一个替代执行语句序列。 + +**链式条件:** 具有一系列替代分支的条件语句。 + +**嵌套条件:** 出现在另一个条件语句分支中的条件语句。 + +**递归:** 调用自身的函数就是递归的。 + +**基本情况:** 递归函数中的一个条件分支,不进行递归调用。 + +**无限递归:** 没有基本情况或永远达不到基本情况的递归。最终,无限递归会导致运行时错误。 + +**换行符:** 在字符串的两个部分之间创建换行的字符。 + +## 5.14\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +### 5.14.1\. 向虚拟助手提问 + ++ 向虚拟助手询问:“取模运算符有什么用途?” + ++ Python 提供了运算符来计算逻辑操作`and`、`or`和`not`,但它没有计算排他性`or`操作的运算符,通常写作`xor`。向助手询问:“什么是逻辑`xor`操作,我如何在 Python 中计算它?” + +在本章中,我们看到了两种写三分支`if`语句的方法,使用链式条件或嵌套条件。你可以使用虚拟助手将它们相互转换。例如,问虚拟助手:“将这条语句转换为链式条件。” + +```py +if x == y: + print('x and y are equal') +else: + if x < y: + print('x is less than y') + else: + print('x is greater than y') +``` + +```py +x is less than y +``` + +向虚拟助手询问:“用一个条件重写这条语句。” + +```py +if 0 < x: + if x < 10: + print('x is a positive single-digit number.') +``` + +```py +x is a positive single-digit number. +``` + +看看虚拟助手是否能简化这个不必要的复杂性。 + +```py +if not x <= 0 and not x >= 10: + print('x is a positive single-digit number.') +``` + +```py +x is a positive single-digit number. +``` + +这是一个尝试递归的函数,它以 2 为步长倒数。 + +```py +def countdown_by_two(n): + if n == 0: + print('Blastoff!') + else: + print(n) + countdown_by_two(n-2) +``` + +看起来它能正常工作。 + +```py +countdown_by_two(6) +``` + +```py +6 +4 +2 +Blastoff! +``` + +但它有一个错误。询问虚拟助手问题出在哪里,以及如何修复它。将它提供的解决方案粘贴回来并进行测试。 + +### 5.14.2\. 练习 + +`time`模块提供了一个名为`time`的函数,它返回自“Unix 纪元”(1970 年 1 月 1 日 00:00:00 UTC 协调世界时)以来的秒数。 + +```py +from time import time + +now = time() +now +``` + +```py +1716394001.8466134 +``` + +使用整数除法和取模运算符来计算自 1970 年 1 月 1 日以来的天数,并且计算当前的时、分、秒。 + +你可以在[`docs.python.org/3/library/time.html`](https://docs.python.org/3/library/time.html)上阅读更多关于`time`模块的信息。 + +### 5.14.3\. 练习 + +如果你给定了三根棍子,你可能无法将它们排列成三角形。例如,如果其中一根棍子长 12 英寸,而其他两根棍子长 1 英寸,你就无法让短棍在中间相遇。对于任何三条边,都有一个测试来判断是否可以形成三角形: + +> 如果三条边中的任何一条大于其他两条边的和,那么就不能形成三角形。否则,可以形成三角形。(如果两条边的和等于第三条边,它们就形成了所谓的“退化”三角形。) + +编写一个名为`is_triangle`的函数,接受三个整数作为参数,并根据是否能够从给定长度的棍子中形成三角形,打印“是”或“否”。提示:使用链式条件。 + +### 5.14.4\. 练习 + +以下程序的输出是什么?绘制一个堆栈图,展示程序打印结果时的状态。 + +```py +def recurse(n, s): + if n == 0: + print(s) + else: + recurse(n-1, n+s) + +recurse(3, 0) +``` + +```py +6 +``` + +### 5.14.5\. 练习 + +以下练习使用了第四章中描述的`jupyturtle`模块。 + +阅读以下函数,看看你能否弄清楚它的作用。然后运行它,看看你是否理解正确。调整`length`、`angle`和`factor`的值,观察它们对结果的影响。如果你不确定自己理解其工作原理,可以尝试问一个虚拟助手。 + +```py +from jupyturtle import forward, left, right, back + +def draw(length): + angle = 50 + factor = 0.6 + + if length > 5: + forward(length) + left(angle) + draw(factor * length) + right(2 * angle) + draw(factor * length) + left(angle) + back(length) +``` + +### 5.14.6\. 练习 + +问虚拟助手:“什么是科赫曲线?” + +要画一个长度为`x`的科赫曲线,你只需要 + +1. 画一个长度为`x/3`的科赫曲线。 + +1. 向左转 60 度。 + +1. 画一个长度为`x/3`的科赫曲线。 + +1. 向右转 120 度。 + +1. 画一个长度为`x/3`的科赫曲线。 + +1. 向左转 60 度。 + +1. 画一个长度为`x/3`的科赫曲线。 + +例外情况是如果`x`小于`5`——在这种情况下,你可以直接画一条长度为`x`的直线。 + +编写一个名为`koch`的函数,接受`x`作为参数,并绘制给定长度的科赫曲线。 + +结果应如下所示: + +```py +make_turtle(delay=0) +koch(120) +``` + +### 5.14.7\. 练习 + +虚拟助理知道`jupyturtle`模块中的功能,但这些功能有许多版本,名称不同,因此助理可能不知道你在谈论哪个版本。 + +解决这个问题的方法是,在提问之前,你可以提供额外的信息。例如,你可以以“这是一个使用`jupyturtle`模块的程序”开头,然后粘贴本章节中的一个示例。之后,助理应该能够生成使用此模块的代码。 + +例如,向助理询问绘制谢尔宾斯基三角形的程序。你得到的代码应该是一个很好的起点,但你可能需要进行一些调试。如果第一次尝试不起作用,你可以告诉助理发生了什么,并请求帮助,或者自行调试。 + +这是结果的大致样子,尽管你得到的版本可能会有所不同。 + +```py +make_turtle(delay=0, height=200) + +draw_sierpinski(100, 3) +``` + +[《Think Python:第三版》](https://allendowney.github.io/ThinkPython/index.html) + +版权 2024 [Allen B. Downey](https://allendowney.com) + +代码许可:[MIT 许可证](https://mit-license.org/) + +文本许可:[知识共享署名-非商业性使用-相同方式共享 4.0 国际](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_08.md b/translations/zh/tnkpy3e_08.md new file mode 100644 index 0000000..db6a08f --- /dev/null +++ b/translations/zh/tnkpy3e_08.md @@ -0,0 +1,766 @@ +# 6. 返回值 + +> 原文:[`allendowney.github.io/ThinkPython/chap06.html`](https://allendowney.github.io/ThinkPython/chap06.html) + +在前面的章节中,我们使用了内置函数——如`abs`和`round`——以及数学模块中的函数——如`sqrt`和`pow`。当你调用这些函数中的一个时,它返回一个值,你可以将其赋值给一个变量或作为表达式的一部分使用。 + +迄今为止我们编写的函数是不同的。有些使用`print`函数显示值,有些使用 turtle 函数绘制图形。但它们没有返回我们赋值给变量或在表达式中使用的值。 + +在本章中,我们将看到如何编写返回值的函数。 + +## 6.1. 有些函数有返回值 + +当你像调用`math.sqrt`这样的函数时,结果被称为**返回值**。如果函数调用出现在单元格的末尾,Jupyter 会立即显示返回值。 + +```py +import math + +math.sqrt(42 / math.pi) +``` + +```py +3.656366395715726 +``` + +如果你将返回值赋值给一个变量,它不会被显示。 + +```py +radius = math.sqrt(42 / math.pi) +``` + +但你可以稍后显示它。 + +```py +radius +``` + +```py +3.656366395715726 +``` + +或者你可以将返回值作为表达式的一部分使用。 + +```py +radius + math.sqrt(42 / math.pi) +``` + +```py +7.312732791431452 +``` + +这是一个返回值的函数示例。 + +```py +def circle_area(radius): + area = math.pi * radius**2 + return area +``` + +`circle_area`将`radius`作为参数,计算该半径的圆的面积。 + +最后一行是一个`return`语句,它返回`area`的值。 + +如果我们像这样调用函数,Jupyter 会显示返回值。 + +```py +circle_area(radius) +``` + +```py +42.00000000000001 +``` + +我们可以将返回值赋值给一个变量。 + +```py +a = circle_area(radius) +``` + +或者将其作为表达式的一部分使用。 + +```py +circle_area(radius) + 2 * circle_area(radius / 2) +``` + +```py +63.000000000000014 +``` + +后面我们可以显示赋值给结果的变量的值。 + +```py +a +``` + +```py +42.00000000000001 +``` + +但我们无法访问`area`。 + +```py +area +``` + +```py +NameError: name 'area' is not defined +``` + +`area`是函数中的局部变量,因此我们无法从函数外部访问它。 + +## 6.2. 有些函数返回 None + +如果一个函数没有`return`语句,它会返回`None`,这是一个特殊的值,类似于`True`和`False`。例如,这里是第三章中的`repeat`函数。 + +```py +def repeat(word, n): + print(word * n) +``` + +如果我们像这样调用它,它会显示蒙提·派森歌曲《芬兰》的第一行。 + +```py +repeat('Finland, ', 3) +``` + +```py +Finland, Finland, Finland, +``` + +这个函数使用`print`函数来显示一个字符串,但它没有使用`return`语句返回值。如果我们将结果赋值给一个变量,它仍然会显示这个字符串。 + +```py +result = repeat('Finland, ', 3) +``` + +```py +Finland, Finland, Finland, +``` + +如果我们显示变量的值,我们什么也得不到。 + +```py +result +``` + +`result`实际上有一个值,但 Jupyter 不会显示它。不过我们可以像这样显示它。 + +```py +print(result) +``` + +```py +None +``` + +`repeat`的返回值是`None`。 + +现在这里有一个类似`repeat`的函数,不同之处在于它有一个返回值。 + +```py +def repeat_string(word, n): + return word * n +``` + +请注意,我们可以在`return`语句中使用一个表达式,而不仅仅是一个变量。 + +使用这个版本,我们可以将结果赋值给一个变量。当函数运行时,它不会显示任何内容。 + +```py +line = repeat_string('Spam, ', 4) +``` + +但之后我们可以显示赋值给`line`的值。 + +```py +line +``` + +```py +'Spam, Spam, Spam, Spam, ' +``` + +这样的函数被称为**纯函数**,因为它不会显示任何内容或产生任何其他效果——除了返回一个值。 + +## 6.3. 返回值与条件语句 + +如果 Python 没有提供`abs`,我们可以像这样编写它。 + +```py +def absolute_value(x): + if x < 0: + return -x + else: + return x +``` + +如果`x`为负,第一条`return`语句返回`-x`,函数立即结束。否则,第二条`return`语句返回`x`,函数结束。因此,这个函数是正确的。 + +然而,如果你将`return`语句放在条件语句中,你必须确保程序的每一条路径都能到达一个`return`语句。例如,这是一个错误版本的`absolute_value`。 + +```py +def absolute_value_wrong(x): + if x < 0: + return -x + if x > 0: + return x +``` + +如果我们以`0`作为参数调用这个函数,会发生什么呢? + +```py +absolute_value_wrong(0) +``` + +什么都没有得到!问题在于:当`x`为`0`时,两个条件都不成立,函数结束而没有执行`return`语句,这意味着返回值是`None`,因此 Jupyter 不会显示任何内容。 + +另一个例子,这是一个带有额外`return`语句的`absolute_value`版本。 + +```py +def absolute_value_extra_return(x): + if x < 0: + return -x + else: + return x + + return 'This is dead code' +``` + +如果`x`为负,第一条`return`语句执行,函数结束。否则,第二条`return`语句执行,函数结束。无论哪种情况,我们都不会到达第三条`return`语句——因此它永远不会执行。 + +不能运行的代码叫做**死代码**。通常情况下,死代码不会造成任何危害,但它通常表明存在误解,并且可能会让试图理解程序的人感到困惑。 + +## 6.4\. 增量开发 + +当你编写更大的函数时,可能会发现你花费更多时间在调试上。为了应对越来越复杂的程序,你可能会想尝试**增量开发**,这是一种每次只添加和测试少量代码的方式。 + +举个例子,假设你想找出由坐标\((x_1, y_1)\)和\((x_2, y_2)\)表示的两点之间的距离。根据毕达哥拉斯定理,距离是: + +\[\mathrm{distance} = \sqrt{(x_2 - x_1)² + (y_2 - y_1)²}\] + +第一步是考虑一个`distance`函数在 Python 中应该是什么样子的——也就是说,输入(参数)是什么,输出(返回值)是什么? + +对于这个函数,输入是点的坐标。返回值是距离。你可以立即写出函数的大纲: + +```py +def distance(x1, y1, x2, y2): + return 0.0 +``` + +这个版本尚未计算距离——它总是返回零。但它是一个完整的函数,具有返回值,这意味着你可以在使其更复杂之前进行测试。 + +为了测试这个新函数,我们将用样本参数调用它: + +```py +distance(1, 2, 4, 6) +``` + +```py +0.0 +``` + +我选择这些值是为了让水平距离为`3`,垂直距离为`4`。这样,结果就是`5`,这是一个`3-4-5`直角三角形的斜边长度。测试一个函数时,知道正确的答案是非常有用的。 + +此时,我们已经确认函数可以运行并返回一个值,我们可以开始向函数体中添加代码。一个好的下一步是找出`x2 - x1`和`y2 - y1`的差值。这是一个将这些值存储在临时变量中的版本,并显示它们。 + +```py +def distance(x1, y1, x2, y2): + dx = x2 - x1 + dy = y2 - y1 + print('dx is', dx) + print('dy is', dy) + return 0.0 +``` + +如果函数正常工作,它应该显示`dx is 3`和`dy is 4`。如果是这样,我们就知道函数已经得到了正确的参数并且正确地进行了第一次计算。如果不是,检查的代码行就很少。 + +```py +distance(1, 2, 4, 6) +``` + +```py +dx is 3 +dy is 4 +``` + +```py +0.0 +``` + +到目前为止很好。接下来我们计算`dx`和`dy`的平方和: + +```py +def distance(x1, y1, x2, y2): + dx = x2 - x1 + dy = y2 - y1 + dsquared = dx**2 + dy**2 + print('dsquared is: ', dsquared) + return 0.0 +``` + +再次运行函数并检查输出,应该是`25`。 + +```py +distance(1, 2, 4, 6) +``` + +```py +dsquared is: 25 +``` + +```py +0.0 +``` + +最后,我们可以使用`math.sqrt`来计算距离: + +```py +def distance(x1, y1, x2, y2): + dx = x2 - x1 + dy = y2 - y1 + dsquared = dx**2 + dy**2 + result = math.sqrt(dsquared) + print("result is", result) +``` + +然后进行测试。 + +```py +distance(1, 2, 4, 6) +``` + +```py +result is 5.0 +``` + +结果是正确的,但这个版本的函数显示了结果,而不是返回它,因此返回值是`None`。 + +我们可以通过用`return`语句替换`print`函数来修复这个问题。 + +```py +def distance(x1, y1, x2, y2): + dx = x2 - x1 + dy = y2 - y1 + dsquared = dx**2 + dy**2 + result = math.sqrt(dsquared) + return result +``` + +这个版本的`distance`是一个纯函数。如果我们这样调用它,只有结果会被显示。 + +```py +distance(1, 2, 4, 6) +``` + +```py +5.0 +``` + +如果我们将结果赋值给一个变量,什么也不会显示。 + +```py +d = distance(1, 2, 4, 6) +``` + +我们编写的`print`语句对于调试很有用,但一旦函数正常工作,就可以将它们移除。这样的代码称为**临时代码**,它在构建程序时很有帮助,但不是最终产品的一部分。 + +这个例子演示了渐进式开发。这个过程的关键方面包括: + +1. 从一个可运行的程序开始,进行小的修改,并在每次修改后进行测试。 + +1. 使用变量来保存中间值,以便你可以显示和检查它们。 + +1. 一旦程序工作正常,就可以移除临时代码。 + +在任何时候,如果出现错误,你应该有一个清晰的方向去找出问题。渐进式开发可以节省大量的调试时间。 + +## 6.5\. 布尔函数 + +函数可以返回布尔值`True`和`False`,这通常方便将复杂的测试封装在函数中。例如,`is_divisible`检查`x`是否能被`y`整除且没有余数。 + +```py +def is_divisible(x, y): + if x % y == 0: + return True + else: + return False +``` + +这是我们如何使用它的方式。 + +```py +is_divisible(6, 4) +``` + +```py +False +``` + +```py +is_divisible(6, 3) +``` + +```py +True +``` + +在函数内部,`==`运算符的结果是一个布尔值,因此我们可以通过直接返回它来更简洁地编写这个函数。 + +```py +def is_divisible(x, y): + return x % y == 0 +``` + +布尔函数通常用于条件语句中。 + +```py +if is_divisible(6, 2): + print('divisible') +``` + +```py +divisible +``` + +可能会想写成这样: + +```py +if is_divisible(6, 2) == True: + print('divisible') +``` + +```py +divisible +``` + +但是比较是没有必要的。 + +## 6.6\. 带返回值的递归 + +现在我们可以编写具有返回值的函数,我们也可以编写具有返回值的递归函数,有了这个能力,我们已经跨越了一个重要的门槛——我们现在拥有的 Python 子集是**图灵完备**的,这意味着我们可以执行任何可以通过算法描述的计算。 + +为了演示带返回值的递归,我们将评估几个递归定义的数学函数。递归定义类似于循环定义,定义中会引用正在定义的事物。真正的循环定义并不十分有用: + +> vorpal:用于描述某物是 vorpal 的形容词。 + +如果你在字典里看到了这个定义,可能会觉得很烦恼。另一方面,如果你查阅阶乘函数的定义,用符号\(!\)表示,可能会得到如下内容: + +\[\begin{split}\begin{aligned} 0! &= 1 \\ n! &= n~(n-1)! \end{aligned}\end{split}\] + +这个定义表示,\(0\)的阶乘是\(1\),而任何其他值\(n\)的阶乘是\(n\)与\(n-1\)的阶乘相乘。 + +如果你能写出某个东西的递归定义,你就能写一个 Python 程序来计算它。按照增量开发的过程,我们首先从一个接受`n`作为参数并总是返回`0`的函数开始。 + +```py +def factorial(n): + return 0 +``` + +现在让我们添加定义的第一部分——如果参数恰好是`0`,我们只需要返回`1`: + +```py +def factorial(n): + if n == 0: + return 1 + else: + return 0 +``` + +现在让我们填写第二部分——如果`n`不为`0`,我们必须进行递归调用,找到`n-1`的阶乘,然后将结果与`n`相乘: + +```py +def factorial(n): + if n == 0: + return 1 + else: + recurse = factorial(n-1) + return n * recurse +``` + +这个程序的执行流程类似于第五章中的`countdown`流程。如果我们用值`3`调用`factorial`: + +由于`3`不等于`0`,我们采取第二个分支并计算`n-1`的阶乘。… + +> 由于`2`不等于`0`,我们采取第二个分支并计算`n-1`的阶乘。… +> +> > 由于`1`不等于`0`,我们采取第二个分支并计算`n-1`的阶乘。… +> > +> > > 由于`0`等于`0`,我们采取第一个分支并返回`1`,不再进行递归调用。 +> > > +> > 返回值`1`与`n`(即`1`)相乘,结果被返回。 +> > +> 返回值`1`与`n`(即`2`)相乘,结果被返回。 + +返回值`2`与`n`(即`3`)相乘,结果`6`成为整个过程启动时函数调用的返回值。 + +下图展示了这个函数调用序列的栈图。 + +![_images/726efd741b2b10a7e5f8789b876a18d14f2818ebbdfc92ab4c41430c6422fcfc.png](img/b37eb9da251407831430919397bc7f9a.png) + +返回值被显示为从栈中返回。在每一帧中,返回值是`n`与`recurse`的乘积。 + +在最后一帧中,本地变量`recurse`不存在,因为创建它的分支没有执行。 + +## 6.7\. 信念的飞跃 + +跟踪执行流程是阅读程序的一种方式,但它很快就会变得让人不堪重负。另一种方法是我所称的“信念的飞跃”。当你遇到一个函数调用时,与你跟踪执行流程不同,你可以*假设*该函数正确工作并返回正确的结果。 + +实际上,当你使用内置函数时,你已经在实践这种“信念的飞跃”。当你调用`abs`或`math.sqrt`时,你并没有检查这些函数的内部实现——你只是认为它们是有效的。 + +当你调用自己的函数时,也是如此。例如,之前我们写了一个名为`is_divisible`的函数,用来判断一个数是否能被另一个数整除。一旦我们确信这个函数是正确的,就可以在不再查看函数体的情况下使用它。 + +递归程序也是如此。当你到达递归调用时,应该假设递归调用是正确的,而不是跟随执行流程。然后你应该问自己,“假设我可以计算\(n-1\)的阶乘,我能计算\(n\)的阶乘吗?”阶乘的递归定义意味着你可以通过乘以\(n\)来计算。 + +当然,假设一个函数在你还没写完的时候就能正确工作,这有点奇怪,但这就是为什么它被称为信任的跳跃! + +## 6.8\. 斐波那契 + +在`factorial`之后,最常见的递归函数示例是`fibonacci`,它有如下定义: + +\[\begin{split}\begin{aligned} \mathrm{fibonacci}(0) &= 0 \\ \mathrm{fibonacci}(1) &= 1 \\ \mathrm{fibonacci}(n) &= \mathrm{fibonacci}(n-1) + \mathrm{fibonacci}(n-2) \end{aligned}\end{split}\] + +将其翻译为 Python 代码,像这样: + +```py +def fibonacci(n): + if n == 0: + return 0 + elif n == 1: + return 1 + else: + return fibonacci(n-1) + fibonacci(n-2) +``` + +如果你尝试跟踪这里的执行流程,即使是对于较小的\(n\)值,你也会感到头晕目眩。但根据信任的跳跃法则,如果你假设两个递归调用是正确的,你就可以确信最后的`return`语句是正确的。 + +顺便提一下,这种计算斐波那契数的方法效率非常低。在第十章中,我会解释为什么,并提出一种改进方法。 + +## 6.9\. 检查类型 + +如果我们调用`factorial`并将`1.5`作为参数传递,会发生什么呢? + +```py +factorial(1.5) +``` + +```py +RecursionError: maximum recursion depth exceeded in comparison +``` + +看起来像是无限递归。这怎么可能呢?函数在`n == 1`或`n == 0`时有基本情况。但如果`n`不是整数,我们可能会*错过*基本情况并进行无限递归。 + +在这个例子中,`n`的初始值为`1.5`。在第一次递归调用中,`n`的值是`0.5`。接下来是`-0.5`,然后它变得更小(更负),但永远不会是`0`。 + +为了避免无限递归,我们可以使用内置函数`isinstance`来检查参数的类型。下面是我们检查一个值是否为整数的方法。 + +```py +isinstance(3, int) +``` + +```py +True +``` + +```py +isinstance(1.5, int) +``` + +```py +False +``` + +现在,这是一个带有错误检查的`factorial`版本。 + +```py +def factorial(n): + if not isinstance(n, int): + print('factorial is only defined for integers.') + return None + elif n < 0: + print('factorial is not defined for negative numbers.') + return None + elif n == 0: + return 1 + else: + return n * factorial(n-1) +``` + +首先,它检查`n`是否为整数。如果不是,它会显示一个错误消息并返回`None`。 + +```py +factorial('crunchy frog') +``` + +```py +factorial is only defined for integers. +``` + +然后,它检查`n`是否为负数。如果是,它会显示一个错误消息并返回`None`。 + +```py +factorial(-2) +``` + +```py +factorial is not defined for negative numbers. +``` + +如果我们通过了两个检查,我们就知道`n`是一个非负整数,因此可以确信递归会终止。检查函数的参数以确保它们具有正确的类型和值,称为**输入验证**。 + +## 6.10\. 调试 + +将一个大型程序分解成更小的函数会创建自然的调试检查点。如果某个函数无法正常工作,可以考虑三种可能性: + ++ 函数获取的参数有问题——也就是说,前置条件被违反了。 + ++ 函数有问题——也就是说,后置条件被违反了。 + ++ 调用者在返回值的使用上出现了问题。 + +为了排除第一种可能性,可以在函数的开始处添加`print`语句,显示参数的值(可能还包括它们的类型)。或者你可以写代码显式检查前置条件。 + +如果参数看起来没问题,可以在每个`return`语句之前添加`print`语句,显示返回值。如果可能,使用有助于检查结果的参数调用函数。 + +如果函数似乎在工作,检查函数调用,确保返回值被正确使用——或者至少被使用! + +在函数的开始和结束处添加`print`语句可以帮助使执行流程更加可见。例如,以下是带有`print`语句的`factorial`函数版本: + +```py +def factorial(n): + space = ' ' * (4 * n) + print(space, 'factorial', n) + if n == 0: + print(space, 'returning 1') + return 1 + else: + recurse = factorial(n-1) + result = n * recurse + print(space, 'returning', result) + return result +``` + +`space` 是一个由空格字符组成的字符串,用来控制输出的缩进。以下是`factorial(3)`的结果: + +```py +factorial(3) +``` + +```py + factorial 3 + factorial 2 + factorial 1 + factorial 0 + returning 1 + returning 1 + returning 2 + returning 6 +``` + +```py +6 +``` + +如果你对执行流程感到困惑,这种输出可能会很有帮助。开发有效的脚手架代码需要一些时间,但少量的脚手架代码可以节省大量调试时间。 + +## 6.11\. 术语表 + +**返回值:** 函数的结果。如果函数调用作为表达式使用,则返回值是该表达式的值。 + +**纯函数:** 一个不会显示任何内容或产生任何其他副作用的函数,除了返回返回值之外。 + +**死代码:** 程序中无法运行的部分,通常是因为它出现在`return`语句之后。 + +**增量开发:** 一种程序开发计划,旨在通过一次只添加和测试少量代码来避免调试。 + +**脚手架代码:** 在程序开发过程中使用的代码,但不是最终版本的一部分。 + +**图灵完备:** 如果一个语言或语言的子集能够执行任何可以通过算法描述的计算,那么它是图灵完备的。 + +**输入验证:** 检查函数的参数,确保它们具有正确的类型和值。 + +## 6.12\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +### 6.12.1\. 向虚拟助手询问 + +在本章中,我们看到一个不正确的函数,它可能在没有返回值的情况下结束。 + +```py +def absolute_value_wrong(x): + if x < 0: + return -x + if x > 0: + return x +``` + +这是同一个函数的一个版本,它在末尾有死代码。 + +```py +def absolute_value_extra_return(x): + if x < 0: + return -x + else: + return x + + return 'This is dead code.' +``` + +我们看到了以下示例,这虽然是正确的,但不够地道。 + +```py +def is_divisible(x, y): + if x % y == 0: + return True + else: + return False +``` + +向虚拟助手询问这些函数的错误,并看看它是否能发现错误或改进风格。 + +然后问:“编写一个函数,接受两点的坐标并计算它们之间的距离。”看看结果是否与我们在本章中编写的`distance`函数类似。 + +### 6.12.2\. 练习 + +使用增量开发编写一个名为`hypot`的函数,给定直角三角形的其他两个边的长度作为参数,返回斜边的长度。 + +注意:在数学模块中有一个叫做`hypot`的函数,执行相同的操作,但你不应该在这个练习中使用它! + +即使你第一次就能正确编写该函数,还是从一个始终返回`0`的函数开始,并练习逐步修改,边修改边测试。完成后,该函数应仅返回一个值——不应输出任何内容。 + +### 6.12.3\. 练习 + +编写一个布尔函数`is_between(x, y, z)`,如果\(x < y < z\)或者\(z < y < x\),则返回`True`,否则返回`False`。 + +### 6.12.4\. 练习 + +阿克曼函数\(A(m, n)\)定义如下: + +\[\begin{split}\begin{aligned} A(m, n) = \begin{cases} n+1 & \mbox{如果} m = 0 \\ A(m-1, 1) & \mbox{如果} m > 0 \mbox{ 且 } n = 0 \\ A(m-1, A(m, n-1)) & \mbox{如果} m > 0 \mbox{ 且 } n > 0. \end{cases} \end{aligned}\end{split}\] + +编写一个名为`ackermann`的函数来计算阿克曼函数。当你调用`ackermann(5, 5)`时,会发生什么? + +### 6.12.5\. 练习 + +\(a\)和\(b\)的最大公约数(GCD)是能够整除它们两者且没有余数的最大数。 + +找到两个数的 GCD 的一种方法是基于以下观察:如果\(r\)是将\(a\)除以\(b\)后的余数,那么\(gcd(a, b) = gcd(b, r)\)。作为基础情况,我们可以使用\(gcd(a, 0) = a\)。 + +编写一个名为`gcd`的函数,接受参数`a`和`b`,并返回它们的最大公约数。 + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权 2024 [Allen B. Downey](https://allendowney.com) + +代码许可证:[MIT 许可证](https://mit-license.org/) + +文本许可证:[创意共享署名-非商业性使用-相同方式共享 4.0 国际版](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_09.md b/translations/zh/tnkpy3e_09.md new file mode 100644 index 0000000..f3960c5 --- /dev/null +++ b/translations/zh/tnkpy3e_09.md @@ -0,0 +1,713 @@ +# 7\. 迭代与搜索 + +> 原文:[`allendowney.github.io/ThinkPython/chap07.html`](https://allendowney.github.io/ThinkPython/chap07.html) + +1939 年,厄尼斯特·文森特·赖特(Ernest Vincent Wright)出版了一本名为*《盖兹比》*的小说,这本小说有 50,000 个单词,却不包含字母“e”。因为“e”是英语中最常见的字母,甚至写几个不使用它的单词也是困难的。为了感受它的难度,在本章中我们将计算英语单词中至少包含一个“e”的比例。 + +为此,我们将使用`for`语句循环遍历字符串中的字母和文件中的单词,并在循环中更新变量以统计包含字母“e”的单词数量。我们将使用`in`运算符来检查字母是否出现在单词中,你将学习一种叫做“线性搜索”的编程模式。 + +作为练习,你将使用这些工具来解决一个叫做“拼字蜂”(Spelling Bee)的字谜。 + +## 7.1\. 循环与字符串 + +在第三章中,我们看到一个使用`range`函数的`for`循环来显示一系列数字。 + +```py +for i in range(3): + print(i, end=' ') +``` + +```py +0 1 2 +``` + +这个版本使用了关键字参数`end`,因此`print`函数在每个数字后面添加一个空格,而不是换行符。 + +我们还可以使用`for`循环来显示字符串中的字母。 + +```py +for letter in 'Gadsby': + print(letter, end=' ') +``` + +```py +G a d s b y +``` + +请注意,我将变量名从`i`更改为`letter`,这样可以提供更多关于它所代表的值的信息。在`for`循环中定义的变量称为**循环变量**。 + +现在我们可以遍历单词中的字母,检查它是否包含字母“e”。 + +```py +for letter in "Gadsby": + if letter == 'E' or letter == 'e': + print('This word has an "e"') +``` + +在继续之前,我们将把这个循环封装成一个函数。 + +```py +def has_e(): + for letter in "Gadsby": + if letter == 'E' or letter == 'e': + print('This word has an "e"') +``` + +我们让它成为一个纯函数,如果单词包含“e”则返回`True`,否则返回`False`。 + +```py +def has_e(): + for letter in "Gadsby": + if letter == 'E' or letter == 'e': + return True + return False +``` + +我们可以将其推广为以单词作为参数。 + +```py +def has_e(word): + for letter in word: + if letter == 'E' or letter == 'e': + return True + return False +``` + +现在我们可以这样测试: + +```py +has_e('Gadsby') +``` + +```py +False +``` + +```py +has_e('Emma') +``` + +```py +True +``` + +## 7.2\. 阅读单词列表 + +为了查看有多少单词包含字母“e”,我们需要一个单词列表。我们将使用一个包含约 114,000 个官方填字游戏单词的列表;即那些被认为在填字游戏和其他字谜游戏中有效的单词。 + +单词列表保存在一个名为`words.txt`的文件中,该文件已在本章的笔记本中下载。为了读取它,我们将使用内置函数`open`,它接受文件名作为参数,并返回一个**文件对象**,我们可以用它来读取文件。 + +```py +file_object = open('words.txt') +``` + +文件对象提供了一个叫做`readline`的函数,它从文件中读取字符,直到遇到换行符,并将结果作为字符串返回: + +```py +file_object.readline() +``` + +```py +'aa\n' +``` + +请注意,调用`readline`的语法与我们到目前为止看到的函数不同。这是因为它是一个**方法**,即与对象相关联的函数。在这种情况下,`readline`与文件对象相关联,因此我们使用对象的名称、点操作符和方法名称来调用它。 + +列表中的第一个单词是“aa”,它是一种岩浆。序列`\n`代表换行符,分隔这个单词与下一个单词。 + +文件对象会记录它在文件中的位置,因此如果你再次调用`readline`,就会得到下一个单词: + +```py +line = file_object.readline() +line +``` + +```py +'aah\n' +``` + +为了去掉单词结尾的换行符,我们可以使用`strip`,它是一个与字符串关联的方法,因此我们可以这样调用它。 + +```py +word = line.strip() +word +``` + +```py +'aah' +``` + +`strip`方法会去除字符串开头和结尾的空白字符——包括空格、制表符和换行符。 + +你还可以在`for`循环中使用文件对象。这个程序读取`words.txt`并打印每个单词,每行一个: + +```py +for line in open('words.txt'): + word = line.strip() + print(word) +``` + +现在我们可以读取单词列表,下一步是统计它们的数量。为此,我们需要能够更新变量。 + +## 7.3\. 更新变量 + +正如你可能已经发现的,给同一个变量做多个赋值是合法的。新的赋值语句会使一个已存在的变量指向新的值(并停止指向旧值)。 + +例如,下面是一个创建变量的初始赋值。 + +```py +x = 5 +x +``` + +```py +5 +``` + +这里是一个改变变量值的赋值语句。 + +```py +x = 7 +x +``` + +```py +7 +``` + +下图展示了这些赋值在状态图中的样子。 + +![_images/5f2c6acf2632453c84d4a58782db35679dedcd804dd1683fca0560ccb15d92ea.png](img/20c498abf4d329b6904ae1a2d4c15efa.png) + +虚线箭头表示`x`不再指向`5`。实线箭头表示它现在指向`7`。 + +一种常见的赋值类型是**更新**,其中变量的新值依赖于旧值。 + +```py +x = x + 1 +x +``` + +```py +8 +``` + +这条语句的意思是:“获取`x`的当前值,增加 1,然后将结果重新赋值给`x`。” + +如果你试图更新一个不存在的变量,会得到一个错误,因为 Python 在赋值给变量之前会先计算右边的表达式。 + +```py +z = z + 1 +``` + +```py +NameError: name 'z' is not defined +``` + +在你更新变量之前,你必须**初始化**它,通常通过简单的赋值来实现: + +```py +z = 0 +z = z + 1 +z +``` + +```py +1 +``` + +增加变量值的操作称为**增量**;减少变量值的操作称为**减量**。由于这些操作非常常见,Python 提供了**增强赋值运算符**,使得更新变量变得更加简洁。例如,`+=` 运算符会将变量增加给定的数值。 + +```py +z += 2 +z +``` + +```py +3 +``` + +其他算术运算符也有增强赋值运算符,包括`-=`和`*=`。 + +## 7.4\. 循环与计数 + +以下程序计算单词列表中单词的数量。 + +```py +total = 0 + +for line in open('words.txt'): + word = line.strip() + total += 1 +``` + +它首先将`total`初始化为`0`。每次循环时,它会将`total`增加`1`。因此,当循环结束时,`total`表示单词的总数。 + +```py +total +``` + +```py +113783 +``` + +这种用来计算某件事情发生次数的变量叫做**计数器**。 + +我们可以在程序中添加第二个计数器,用来追踪包含字母“e”的单词数量。 + +```py +total = 0 +count = 0 + +for line in open('words.txt'): + word = line.strip() + total = total + 1 + if has_e(word): + count += 1 +``` + +让我们来看一看有多少个单词包含字母“e”。 + +```py +count +``` + +```py +76162 +``` + +作为`total`的百分比,大约三分之二的单词使用了字母“e”。 + +```py +count / total * 100 +``` + +```py +66.93618554617122 +``` + +因此,你可以理解为什么在不使用这些词的情况下构造一本书是多么困难。 + +## 7.5\. `in` 运算符 + +我们在本章编写的 `has_e` 版本比实际需要的要复杂。Python 提供了一个运算符 `in`,它检查一个字符是否出现在字符串中。 + +```py +word = 'Gadsby' +'e' in word +``` + +```py +False +``` + +所以我们可以将 `has_e` 改写为这样。 + +```py +def has_e(word): + if 'E' in word or 'e' in word: + return True + else: + return False +``` + +由于 `if` 语句的条件有布尔值,我们可以省略 `if` 语句,直接返回布尔值。 + +```py +def has_e(word): + return 'E' in word or 'e' in word +``` + +我们可以通过使用 `lower` 方法进一步简化这个函数,`lower` 会将字符串中的字母转换为小写。这里是一个例子。 + +```py +word.lower() +``` + +```py +'gadsby' +``` + +`lower` 会创建一个新的字符串——它不会修改现有的字符串——所以 `word` 的值不会改变。 + +```py +word +``` + +```py +'Gadsby' +``` + +这是我们在 `has_e` 中如何使用 `lower` 的方法。 + +```py +def has_e(word): + return 'e' in word.lower() +``` + +```py +has_e('Gadsby') +``` + +```py +False +``` + +```py +has_e('Emma') +``` + +```py +True +``` + +## 7.6\. 搜索 + +基于这个简化版的 `has_e`,我们可以编写一个更通用的函数 `uses_any`,它接受第二个参数,该参数是一个字母的字符串。如果单词中使用了这些字母中的任何一个,它返回 `True`,否则返回 `False`。 + +```py +def uses_any(word, letters): + for letter in word.lower(): + if letter in letters.lower(): + return True + return False +``` + +这里是一个结果为 `True` 的例子。 + +```py +uses_any('banana', 'aeiou') +``` + +```py +True +``` + +另一个例子中结果是 `False`。 + +```py +uses_any('apple', 'xyz') +``` + +```py +False +``` + +`uses_any` 会将 `word` 和 `letters` 转换为小写,因此它可以处理任何大小写组合。 + +```py +uses_any('Banana', 'AEIOU') +``` + +```py +True +``` + +`uses_any` 的结构类似于 `has_e`。它会循环遍历 `word` 中的字母并逐个检查。如果找到一个出现在 `letters` 中的字母,它会立即返回 `True`。如果循环结束都没有找到任何字母,它会返回 `False`。 + +这种模式称为**线性搜索**。在本章末的练习中,你将编写更多使用这种模式的函数。 + +## 7.7\. Doctest + +在第四章中,我们使用了文档字符串来记录函数——即解释它的作用。也可以使用文档字符串来*测试*一个函数。这里是包含测试的 `uses_any` 函数版本。 + +```py +def uses_any(word, letters): + """Checks if a word uses any of a list of letters. + + >>> uses_any('banana', 'aeiou') + True + >>> uses_any('apple', 'xyz') + False + """ + for letter in word.lower(): + if letter in letters.lower(): + return True + return False +``` + +每个测试都以 `>>>` 开头,这是一些 Python 环境中用来表示用户可以输入代码的提示符。在 doctest 中,提示符后跟一个表达式,通常是一个函数调用。接下来的一行表示如果函数正确工作,表达式应具有的值。 + +在第一个例子中,`'banana'` 使用了 `'a'`,所以结果应该是 `True`。在第二个例子中,`'apple'` 没有使用 `'xyz'` 中的任何字符,所以结果应该是 `False`。 + +为了运行这些测试,我们必须导入 `doctest` 模块,并运行一个名为 `run_docstring_examples` 的函数。为了让这个函数更容易使用,我编写了以下函数,它接受一个函数对象作为参数。 + +```py +from doctest import run_docstring_examples + +def run_doctests(func): + run_docstring_examples(func, globals(), name=func.__name__) +``` + +我们还没有学习 `globals` 和 `__name__`,可以忽略它们。现在我们可以像这样测试 `uses_any`。 + +```py +run_doctests(uses_any) +``` + +`run_doctests` 会找到文档字符串中的表达式并对其进行评估。如果结果是预期的值,则测试**通过**。否则,它**失败**。 + +如果所有测试都通过,`run_doctests`将不显示任何输出——在这种情况下,无消息即好消息。若要查看测试失败时发生的情况,下面是`uses_any`的一个错误版本。 + +```py +def uses_any_incorrect(word, letters): + """Checks if a word uses any of a list of letters. + + >>> uses_any_incorrect('banana', 'aeiou') + True + >>> uses_any_incorrect('apple', 'xyz') + False + """ + for letter in word.lower(): + if letter in letters.lower(): + return True + else: + return False # INCORRECT! +``` + +这是我们测试时发生的情况。 + +```py +run_doctests(uses_any_incorrect) +``` + +```py +********************************************************************** +File "__main__", line 4, in uses_any_incorrect +Failed example: + uses_any_incorrect('banana', 'aeiou') +Expected: + True +Got: + False +``` + +输出包括失败的示例、函数预期生成的值和函数实际生成的值。 + +如果你不确定为什么这个测试失败,你将有机会作为练习进行调试。 + +## 7.8\. 词汇表 + +**循环变量:** 在`for`循环头部定义的变量。 + +**文件对象:** 一个表示已打开文件的对象,负责追踪文件的哪些部分已被读取或写入。 + +**方法:** 与对象关联的函数,并通过点操作符调用。 + +**更新:** 一种赋值语句,用于给已存在的变量赋新值,而不是创建新变量。 + +**初始化:** 创建一个新变量并为其赋值。 + +**增量:** 增加变量的值。 + +**递减:** 减少变量的值。 + +**计数器:** 用于计数的变量,通常初始化为零,然后递增。 + +**线性搜索:** 一种计算模式,它通过一系列元素进行搜索,并在找到目标时停止。 + +**通过:** 如果测试运行并且结果符合预期,则该测试通过。 + +**失败:** 如果测试运行后结果与预期不符,则该测试失败。 + +## 7.9\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +### 7.9.1\. 向虚拟助手询问 + +在`uses_any`中,你可能注意到第一个`return`语句在循环内部,而第二个在外部。 + +```py +def uses_any(word, letters): + for letter in word.lower(): + if letter in letters.lower(): + return True + return False +``` + +当人们第一次编写像这样的函数时,通常会犯一个错误,就是将两个`return`语句放在循环内部,像这样。 + +```py +def uses_any_incorrect(word, letters): + for letter in word.lower(): + if letter in letters.lower(): + return True + else: + return False # INCORRECT! +``` + +向虚拟助手询问这个版本有什么问题。 + +### 7.9.2\. 练习 + +编写一个名为`uses_none`的函数,它接受一个单词和一个禁用字母的字符串,如果该单词不包含任何禁用字母,则返回`True`。 + +这是一个包含两个文档测试的函数大纲。填写函数代码以通过这些测试,并添加至少一个文档测试。 + +```py +def uses_none(word, forbidden): + """Checks whether a word avoid forbidden letters. + + >>> uses_none('banana', 'xyz') + True + >>> uses_none('apple', 'efg') + False + """ + return None +``` + +### 7.9.3\. 练习 + +编写一个名为`uses_only`的函数,它接受一个单词和一个字母字符串,如果该单词仅包含字符串中的字母,则返回`True`。 + +这是一个包含两个文档测试的函数大纲。填写函数代码以通过这些测试,并添加至少一个文档测试。 + +```py +def uses_only(word, available): + """Checks whether a word uses only the available letters. + + >>> uses_only('banana', 'ban') + True + >>> uses_only('apple', 'apl') + False + """ + return None +``` + +### 7.9.4\. 练习 + +编写一个名为`uses_all`的函数,它接受一个单词和一个字母字符串,如果该单词包含该字符串中的所有字母至少一次,则返回`True`。 + +这是一个包含两个文档测试的函数大纲。填写函数代码以通过这些测试,并添加至少一个文档测试。 + +```py +def uses_all(word, required): + """Checks whether a word uses all required letters. + + >>> uses_all('banana', 'ban') + True + >>> uses_all('apple', 'api') + False + """ + return None +``` + +### 7.9.5\. 练习 + +*纽约时报*每天发布一个名为“拼字蜂”的谜题,挑战读者使用七个字母拼尽可能多的单词,其中一个字母是必需的。单词必须至少有四个字母。 + +例如,在我写这篇文章的那一天,字母是`ACDLORT`,其中`R`是必需的字母。所以“color”是一个合法的单词,但“told”不是,因为它没有使用`R`,而“rat”也不是,因为它只有三个字母。字母可以重复,因此“ratatat”是合法的。 + +编写一个名为`check_word`的函数,用于检查给定的单词是否符合要求。它应该接受三个参数:要检查的单词、一个包含七个可用字母的字符串,以及一个包含单个必需字母的字符串。你可以使用你在之前练习中写的函数。 + +下面是包含文档测试的函数大纲。填写函数并检查所有测试是否通过。 + +```py +def check_word(word, available, required): + """Check whether a word is acceptable. + + >>> check_word('color', 'ACDLORT', 'R') + True + >>> check_word('ratatat', 'ACDLORT', 'R') + True + >>> check_word('rat', 'ACDLORT', 'R') + False + >>> check_word('told', 'ACDLORT', 'R') + False + >>> check_word('bee', 'ACDLORT', 'R') + False + """ + return False +``` + +根据“拼字蜂”的规则, + ++ 四个字母的单词值 1 分。 + ++ 较长的单词每个字母得 1 分。 + ++ 每个谜题至少包含一个“全字母句”(pangram),即包含所有字母的句子。这些可以获得 7 个额外分数! + +编写一个名为`score_word`的函数,接受一个单词和一串可用字母,并返回该单词的得分。你可以假设这个单词是合法的。 + +再次,这是包含文档测试的函数大纲。 + +```py +def word_score(word, available): + """Compute the score for an acceptable word. + + >>> word_score('card', 'ACDLORT') + 1 + >>> word_score('color', 'ACDLORT') + 5 + >>> word_score('cartload', 'ACDLORT') + 15 + """ + return 0 +``` + +### 7.9.6\. 练习 + +你可能注意到你在之前练习中写的函数有很多相似之处。实际上,它们如此相似,以至于你可以经常用一个函数来写另一个。 + +例如,如果一个单词没有使用任何一组禁止字母,这意味着它根本没有使用任何字母。所以我们可以这样写一个`uses_none`的版本。 + +```py +def uses_none(word, forbidden): + """Checks whether a word avoids forbidden letters. + + >>> uses_none('banana', 'xyz') + True + >>> uses_none('apple', 'efg') + False + >>> uses_none('', 'abc') + True + """ + return not uses_any(word, forbidden) +``` + +`uses_only`和`uses_all`之间也有相似之处,你可以加以利用。如果你已经有了`uses_only`的工作版本,看看你能否写出一个调用`uses_only`的`uses_all`版本。 + +### 7.9.7\. 练习 + +如果你在前一个问题上卡住了,试着向虚拟助手提问:“给定一个函数,`uses_only`,它接受两个字符串并检查第一个字符串是否只使用第二个字符串中的字母,用它来写`uses_all`,它接受两个字符串并检查第一个字符串是否使用了第二个字符串中的所有字母,允许重复字母。” + +使用`run_doctests`检查答案。 + +### 7.9.8\. 练习 + +现在让我们看看是否能基于`uses_any`写出`uses_all`。 + +向虚拟助手提问:“给定一个函数,`uses_any`,它接受两个字符串并检查第一个字符串是否使用了第二个字符串中的任何字母,你能否用它来写`uses_all`,它接受两个字符串并检查第一个字符串是否使用了第二个字符串中的所有字母,允许重复字母。” + +如果它说可以,确保测试结果! + +```py +# Here's what I got from ChatGPT 4o December 26, 2024 +# It's correct, but it makes multiple calls to uses_any + +def uses_all(s1, s2): + """Checks if all characters in s2 are in s1, allowing repeats.""" + for char in s2: + if not uses_any(s1, char): + return False + return True +``` + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可证:[MIT 许可证](https://mit-license.org/) + +文本许可证:[创作共用署名-非商业性使用-相同方式共享 4.0 国际版](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_10.md b/translations/zh/tnkpy3e_10.md new file mode 100644 index 0000000..e025b77 --- /dev/null +++ b/translations/zh/tnkpy3e_10.md @@ -0,0 +1,774 @@ +# 8\. 字符串和正则表达式 + +> 原文:[`allendowney.github.io/ThinkPython/chap08.html`](https://allendowney.github.io/ThinkPython/chap08.html) + +字符串不同于整数、浮点数和布尔值。字符串是一个**序列**,意味着它包含多个按特定顺序排列的值。在本章中,我们将学习如何访问组成字符串的值,并使用处理字符串的函数。 + +我们还将使用正则表达式,它是查找字符串中模式并执行如搜索和替换等操作的强大工具。 + +作为练习,你将有机会将这些工具应用到一个叫做 Wordle 的单词游戏中。 + +## 8.1\. 字符串是一个序列 + +字符串是字符的序列。**字符**可以是字母(几乎所有字母表中的字母)、数字、标点符号或空格。 + +你可以使用方括号操作符从字符串中选择一个字符。这个示例语句从`fruit`中选择第 1 个字符,并将其赋值给`letter`: + +```py +fruit = 'banana' +letter = fruit[1] +``` + +方括号中的表达式是一个**索引**,之所以这么叫,是因为它*指示*要选择序列中的哪个字符。但结果可能不是你预期的。 + +```py +letter +``` + +```py +'a' +``` + +索引为`1`的字母实际上是字符串中的第二个字母。索引是从字符串开始位置的偏移量,所以第一个字母的偏移量是`0`。 + +```py +fruit[0] +``` + +```py +'b' +``` + +你可以把`'b'`当作`'banana'`的第 0 个字母——读作“零个”。 + +方括号中的索引可以是一个变量。 + +```py +i = 1 +fruit[i] +``` + +```py +'a' +``` + +或者是包含变量和运算符的表达式。 + +```py +fruit[i+1] +``` + +```py +'n' +``` + +但是,索引的值必须是整数——否则你会遇到`TypeError`。 + +```py +fruit[1.5] +``` + +```py +TypeError: string indices must be integers +``` + +正如我们在第一章中看到的,我们可以使用内置函数`len`来获取字符串的长度。 + +```py +n = len(fruit) +n +``` + +```py +6 +``` + +为了获取字符串中的最后一个字母,你可能会想写成这样: + +```py +fruit[n] +``` + +```py +IndexError: string index out of range +``` + +但这会导致`IndexError`,因为在`'banana'`中没有索引为 6 的字母。因为我们从`0`开始计数,所以六个字母的编号是`0`到`5`。要获取最后一个字符,你需要从`n`中减去`1`: + +```py +fruit[n-1] +``` + +```py +'a' +``` + +但有更简单的方法。要获取字符串中的最后一个字母,你可以使用负索引,它从字符串的末尾向后计数。 + +```py +fruit[-1] +``` + +```py +'a' +``` + +索引`-1`选择最后一个字母,`-2`选择倒数第二个字母,以此类推。 + +## 8.2\. 字符串切片 + +字符串的一部分称为**切片**。选择切片类似于选择单个字符。 + +```py +fruit = 'banana' +fruit[0:3] +``` + +```py +'ban' +``` + +运算符`[n:m]`返回字符串从第`n`个字符到第`m`个字符的部分,包括第一个字符但不包括第二个字符。这种行为是反直觉的,但可以通过想象索引指向*字符之间*的空间来帮助理解,如图所示: + +![_images/8ee40ccdcbc16b8660dd062b1c73a25d49a42724ad6026a2fa3ebf191b4a9862.png](img/a09d0d29930012ae176692b0cb80c9df.png) + +例如,切片`[3:6]`选择了字母`ana`,这意味着`6`在作为切片的一部分时是合法的,但作为索引时是不合法的。 + +如果省略第一个索引,切片将从字符串的开头开始。 + +```py +fruit[:3] +``` + +```py +'ban' +``` + +如果省略第二个索引,切片将一直延伸到字符串的末尾: + +```py +fruit[3:] +``` + +```py +'ana' +``` + +如果第一个索引大于或等于第二个索引,结果将是一个**空字符串**,由两个引号表示: + +```py +fruit[3:3] +``` + +```py +'' +``` + +空字符串不包含任何字符,长度为 0。 + +继续这个例子,你认为`fruit[:]`意味着什么?试试看吧。 + +## 8.3\. 字符串是不可变的 + +很容易在赋值语句的左侧使用`[]`运算符,试图更改字符串中的字符,如下所示: + +```py +greeting = 'Hello, world!' +greeting[0] = 'J' +``` + +```py +TypeError: 'str' object does not support item assignment +``` + +结果是一个`TypeError`。在错误信息中,“对象”是字符串,“项”是我们试图赋值的字符。目前,**对象**与值是相同的,但我们稍后会进一步细化这个定义。 + +发生这个错误的原因是字符串是**不可变的**,这意味着你不能更改一个已有的字符串。你能做的最好的事是创建一个新的字符串,它是原始字符串的变体。 + +```py +new_greeting = 'J' + greeting[1:] +new_greeting +``` + +```py +'Jello, world!' +``` + +这个例子将一个新的首字母连接到`greeting`的切片上。它不会对原始字符串产生影响。 + +```py +greeting +``` + +```py +'Hello, world!' +``` + +## 8.4\. 字符串比较 + +关系运算符适用于字符串。要检查两个字符串是否相等,我们可以使用`==`运算符。 + +```py +word = 'banana' + +if word == 'banana': + print('All right, banana.') +``` + +```py +All right, banana. +``` + +其他关系运算符对于将单词按字母顺序排列很有用: + +```py +def compare_word(word): + if word < 'banana': + print(word, 'comes before banana.') + elif word > 'banana': + print(word, 'comes after banana.') + else: + print('All right, banana.') +``` + +```py +compare_word('apple') +``` + +```py +apple comes before banana. +``` + +Python 处理大写字母和小写字母的方式不同于人类。所有大写字母都排在所有小写字母之前,所以: + +```py +compare_word('Pineapple') +``` + +```py +Pineapple comes before banana. +``` + +为了解决这个问题,我们可以在进行比较之前将字符串转换为标准格式,例如全小写。请记住,如果你需要防御一个手持菠萝的人时,这一点非常重要。 + +## 8.5\. 字符串方法 + +字符串提供了执行多种有用操作的方法。方法类似于函数——它接受参数并返回一个值——但语法有所不同。例如,`upper`方法接受一个字符串并返回一个新的字符串,其中所有字母都为大写。 + +它使用的方法语法是`word.upper()`,而不是函数语法`upper(word)`。 + +```py +word = 'banana' +new_word = word.upper() +new_word +``` + +```py +'BANANA' +``` + +这种使用点运算符的方式指定了方法的名称`upper`,以及要应用该方法的字符串名称`word`。空的括号表示此方法不接受任何参数。 + +方法调用称为**调用**;在这种情况下,我们可以说我们正在对`word`调用`upper`方法。 + +## 8.6\. 写入文件 + +字符串运算符和方法对于读取和写入文本文件非常有用。举个例子,我们将处理*德古拉*的文本,这本由布拉姆·斯托克创作的小说可以从古腾堡计划获取([`www.gutenberg.org/ebooks/345`](https://www.gutenberg.org/ebooks/345))。 + +我已经将这本书下载为名为`pg345.txt`的纯文本文件,我们可以像这样打开它进行阅读: + +```py +reader = open('pg345.txt') +``` + +除了书籍的文本外,这个文件在开头包含了一段关于书籍的信息,在结尾包含了一段关于许可证的信息。在处理文本之前,我们可以通过找到以 `'***'` 开头的特殊行来去除这些额外的内容。 + +以下函数接收一行并检查它是否是特殊行之一。它使用 `startswith` 方法,检查字符串是否以给定的字符序列开头。 + +```py +def is_special_line(line): + return line.startswith('*** ') +``` + +我们可以使用这个函数来遍历文件中的行,并仅打印特殊行。 + +```py +for line in reader: + if is_special_line(line): + print(line.strip()) +``` + +```py +*** START OF THE PROJECT GUTENBERG EBOOK DRACULA *** +*** END OF THE PROJECT GUTENBERG EBOOK DRACULA *** +``` + +现在,让我们创建一个新的文件,命名为 `pg345_cleaned.txt`,其中只包含书籍的文本。为了再次循环遍历书籍,我们必须重新打开它以进行读取。并且,为了写入新文件,我们可以以写入模式打开它。 + +```py +reader = open('pg345.txt') +writer = open('pg345_cleaned.txt', 'w') +``` + +`open` 接受一个可选参数,指定“模式”——在这个例子中,`'w'` 表示我们以写入模式打开文件。如果文件不存在,它将被创建;如果文件已经存在,内容将被替换。 + +作为第一步,我们将遍历文件,直到找到第一行特殊行。 + +```py +for line in reader: + if is_special_line(line): + break +``` + +`break` 语句“跳出”循环——也就是说,它会立即结束循环,而不等到文件末尾。 + +当循环退出时,`line` 包含了使条件为真的特殊行。 + +```py +line +``` + +```py +'*** START OF THE PROJECT GUTENBERG EBOOK DRACULA ***\n' +``` + +因为 `reader` 会跟踪文件中的当前位置,我们可以使用第二个循环从我们离开的地方继续。 + +以下循环逐行读取文件的其余部分。当它找到表示文本结束的特殊行时,它会跳出循环。否则,它会将该行写入输出文件。 + +```py +for line in reader: + if is_special_line(line): + break + writer.write(line) +``` + +当这个循环退出时,`line` 包含第二个特殊行。 + +```py +line +``` + +```py +'*** END OF THE PROJECT GUTENBERG EBOOK DRACULA ***\n' +``` + +此时,`reader` 和 `writer` 仍然打开,这意味着我们可以继续从 `reader` 读取行或向 `writer` 写入行。为了表示我们完成了,我们可以通过调用 `close` 方法关闭这两个文件。 + +```py +reader.close() +writer.close() +``` + +为了检查这个过程是否成功,我们可以读取刚创建的新文件中的前几行。 + +```py +for line in open('pg345_cleaned.txt'): + line = line.strip() + if len(line) > 0: + print(line) + if line.endswith('Stoker'): + break +``` + +```py +DRACULA +_by_ +Bram Stoker +``` + +`endswith` 方法检查字符串是否以给定的字符序列结尾。 + +## 8.7\. 查找与替换 + +在 1901 年的冰岛语版《德古拉》中,其中一个角色的名字从 “Jonathan” 改成了 “Thomas”。为了在英文版中进行这一更改,我们可以遍历整本书,使用 `replace` 方法将一个名字替换成另一个,并将结果写入新文件。 + +我们将从计数文件清理版的行数开始。 + +```py +total = 0 +for line in open('pg345_cleaned.txt'): + total += 1 + +total +``` + +```py +15499 +``` + +为了查看一行是否包含“Jonathan”,我们可以使用 `in` 运算符,检查该字符序列是否出现在行中。 + +```py +total = 0 +for line in open('pg345_cleaned.txt'): + if 'Jonathan' in line: + total += 1 + +total +``` + +```py +199 +``` + +有 199 行包含这个名字,但这还不是它出现的总次数,因为它可能在一行中出现多次。为了得到总数,我们可以使用 `count` 方法,它返回字符串中某个序列出现的次数。 + +```py +total = 0 +for line in open('pg345_cleaned.txt'): + total += line.count('Jonathan') + +total +``` + +```py +200 +``` + +现在我们可以像这样将 `'Jonathan'` 替换为 `'Thomas'`: + +```py +writer = open('pg345_replaced.txt', 'w') + +for line in open('pg345_cleaned.txt'): + line = line.replace('Jonathan', 'Thomas') + writer.write(line) +``` + +结果是一个新文件,名为 `pg345_replaced.txt`,其中包含了*德古拉*的一个版本,在这个版本中,Jonathan Harker 被称为 Thomas。 + +## 8.8\. 正则表达式 + +如果我们确切知道要寻找的字符序列,可以使用 `in` 运算符来查找,并使用 `replace` 方法替换它。但还有一种工具,叫做**正则表达式**,也可以执行这些操作——而且功能更多。 + +为了演示,我将从一个简单的例子开始,然后逐步增加难度。假设,我们再次想要找到所有包含特定单词的行。为了换个方式,我们来看一下书中提到主人公德古拉伯爵的地方。这里有一行提到他。 + +```py +text = "I am Dracula; and I bid you welcome, Mr. Harker, to my house." +``` + +这是我们将用于搜索的**模式**。 + +```py +pattern = 'Dracula' +``` + +一个名为 `re` 的模块提供了与正则表达式相关的函数。我们可以像这样导入它,并使用 `search` 函数来检查模式是否出现在文本中。 + +```py +import re + +result = re.search(pattern, text) +result +``` + +```py + +``` + +如果模式出现在文本中,`search` 将返回一个包含搜索结果的 `Match` 对象。除了其他信息外,它还包含一个名为 `string` 的变量,其中包含被搜索的文本。 + +```py +result.string +``` + +```py +'I am Dracula; and I bid you welcome, Mr. Harker, to my house.' +``` + +它还提供了一个名为 `group` 的方法,可以返回匹配模式的文本部分。 + +```py +result.group() +``` + +```py +'Dracula' +``` + +它还提供了一个名为 `span` 的方法,可以返回模式在文本中开始和结束的位置。 + +```py +result.span() +``` + +```py +(5, 12) +``` + +如果模式在文本中没有出现,`search` 的返回值是 `None`。 + +```py +result = re.search('Count', text) +print(result) +``` + +```py +None +``` + +因此,我们可以通过检查结果是否为 `None` 来判断搜索是否成功。 + +```py +result == None +``` + +```py +True +``` + +将这些都结合起来,这里有一个函数,它循环遍历书中的每一行,直到找到匹配给定模式的行,并返回 `Match` 对象。 + +```py +def find_first(pattern): + for line in open('pg345_cleaned.txt'): + result = re.search(pattern, line) + if result != None: + return result +``` + +我们可以用它来找到某个字符的首次出现。 + +```py +result = find_first('Harker') +result.string +``` + +```py +'CHAPTER I. Jonathan Harker’s Journal\n' +``` + +对于这个例子,我们并不需要使用正则表达式——我们可以用 `in` 运算符更轻松地完成相同的事情。但正则表达式可以做一些 `in` 运算符无法做到的事情。 + +例如,如果模式中包括竖线字符,`'|'`,它可以匹配左边或右边的序列。假设我们想要在书中找到 Mina Murray 的首次提及,但我们不确定她是用名字还是姓氏来称呼的。我们可以使用以下模式,它可以匹配这两个名字。 + +```py +pattern = 'Mina|Murray' +result = find_first(pattern) +result.string +``` + +```py +'CHAPTER V. Letters—Lucy and Mina\n' +``` + +我们可以使用这样的模式来查看某个字符通过名字提到的次数。这里有一个函数,它会遍历书中的每一行,并统计与给定模式匹配的行数。 + +```py +def count_matches(pattern): + count = 0 + for line in open('pg345_cleaned.txt'): + result = re.search(pattern, line) + if result != None: + count += 1 + return count +``` + +现在让我们看看 Mina 被提到多少次。 + +```py +count_matches('Mina|Murray') +``` + +```py +229 +``` + +特殊字符`'^'`匹配字符串的开始,因此我们可以找到以给定模式开头的行。 + +```py +result = find_first('^Dracula') +result.string +``` + +```py +'Dracula, jumping to his feet, said:--\n' +``` + +特殊字符`'$'`匹配字符串的结尾,因此我们可以找到以给定模式结尾的行(忽略行尾的换行符)。 + +```py +result = find_first('Harker$') +result.string +``` + +```py +"by five o'clock, we must start off; for it won't do to leave Mrs. Harker\n" +``` + +## 8.9\. 字符串替换 + +布拉姆·斯托克出生在爱尔兰,*而《德古拉》*于 1897 年出版时,他正居住在英格兰。因此,我们可以预期他会使用英国拼写方式,例如“centre”和“colour”。为了验证这一点,我们可以使用以下模式,这个模式可以匹配“centre”或者美国拼写“center”。 + +```py +pattern = 'cent(er|re)' +``` + +在这个模式中,括号包围的是竖线(`|`)所应用的模式部分。所以这个模式匹配的是一个以`'cent'`开头并且以`'er'`或`'re'`结尾的序列。 + +```py +result = find_first(pattern) +result.string +``` + +```py +'horseshoe of the Carpathians, as if it were the centre of some sort of\n' +``` + +正如预期的那样,他使用了英国拼写。 + +我们还可以检查他是否使用了“colour”的英国拼写。以下模式使用了特殊字符`'?'`,表示前一个字符是可选的。 + +```py +pattern = 'colou?r' +``` + +这个模式可以匹配带有`'u'`的“colour”或没有`'u'`的“color”。 + +```py +result = find_first(pattern) +line = result.string +line +``` + +```py +'undergarment with long double apron, front, and back, of coloured stuff\n' +``` + +同样,正如预期的那样,他使用了英国拼写。 + +现在假设我们想制作一本使用美国拼写的书籍版本。我们可以使用`re`模块中的`sub`函数,它执行**字符串替换**。 + +```py +re.sub(pattern, 'color', line) +``` + +```py +'undergarment with long double apron, front, and back, of colored stuff\n' +``` + +第一个参数是我们想要查找并替换的模式,第二个是我们想用来替换的内容,第三个是我们要搜索的字符串。在结果中,你可以看到“colour”已被替换为“color”。 + +## 8.10\. 调试 + +当你在读取和写入文件时,调试可能会很棘手。如果你在使用 Jupyter 笔记本,可以使用**shell 命令**来帮助。例如,要显示文件的前几行,可以使用命令`!head`,像这样: + +```py +!head pg345_cleaned.txt +``` + +初始的感叹号`!`表示这是一个 shell 命令,而不是 Python 的一部分。要显示最后几行,可以使用`!tail`。 + +```py +!tail pg345_cleaned.txt +``` + +当你处理大文件时,调试可能会变得困难,因为输出可能过多,无法手动检查。一个好的调试策略是从文件的一部分开始,先让程序正常工作,然后再用完整的文件运行它。 + +为了制作一个包含大文件部分内容的小文件,我们可以再次使用`!head`并配合重定向符号`>`,表示结果应该写入文件而不是显示在屏幕上。 + +```py +!head pg345_cleaned.txt > pg345_cleaned_10_lines.txt +``` + +默认情况下,`!head`读取前 10 行,但它也接受一个可选参数,用于指示要读取的行数。 + +```py +!head -100 pg345_cleaned.txt > pg345_cleaned_100_lines.txt +``` + +这个 shell 命令读取`pg345_cleaned.txt`的前 100 行,并将它们写入名为`pg345_cleaned_100_lines.txt`的文件。 + +注意:shell 命令`!head`和`!tail`并非在所有操作系统上都可用。如果它们在你这里无法使用,我们可以用 Python 编写类似的函数。请参考本章最后的第一个练习获取建议。 + +## 8.11\. 术语表 + +**sequence(序列):** 一种有序的值集合,每个值由一个整数索引标识。 + +**character(字符):** 字符串中的一个元素,包括字母、数字和符号。 + +**index(索引):** 用于选择序列中项的整数值,例如字符串中的字符。在 Python 中,索引从`0`开始。 + +**slice(切片):** 由一系列索引范围指定的字符串的一部分。 + +**empty string(空字符串):** 一个不包含任何字符且长度为`0`的字符串。 + +**object(对象):** 一个变量可以引用的事物。对象有类型和数值。 + +**immutable(不可变):** 如果一个对象的元素不能被改变,则该对象是不可变的。 + +**invocation(调用):** 一个表达式——或表达式的一部分——用于调用方法。 + +**regular expression(正则表达式):** 定义搜索模式的字符序列。 + +**pattern(模式):** 规定一个字符串必须满足的要求,以便构成匹配。 + +**string substitution(字符串替换):** 用另一个字符串替换字符串的部分内容。 + +**shell command(shell 命令):** 用于与操作系统交互的 shell 语言中的语句。 + +## 8.12\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +### 8.12.1\. 向虚拟助手提问 + +在本章中,我们仅触及了正则表达式能做的事情的表面。为了了解正则表达式的可能性,可以问虚拟助手:“Python 正则表达式中最常用的特殊字符有哪些?” + +你还可以请求匹配特定类型字符串的模式。例如,尝试询问: + ++ 编写一个匹配带有连字符的 10 位电话号码的 Python 正则表达式。 + ++ 编写一个匹配带有数字和街道名称的街道地址的 Python 正则表达式,地址后跟`ST`或`AVE`。 + ++ 编写一个匹配完整姓名的 Python 正则表达式,该姓名可能包含常见的称谓,如`Mr`或`Mrs`,后跟一个或多个以大写字母开头的名字,名字之间可能有连字符。 + +如果你想看一些更复杂的内容,可以尝试询问正则表达式,它匹配任何合法的 URL。 + +正则表达式通常在引号前面加上字母`r`,表示这是一个“原始字符串”。欲了解更多信息,可以问虚拟助手:“Python 中的原始字符串是什么?” + +### 8.12.2\. 练习 + +看看你能否编写一个函数,实现与 shell 命令`!head`相同的功能。它应该接受三个参数:要读取的文件名、要读取的行数以及要将这些行写入的文件名。如果第三个参数是`None`,它应该显示这些行,而不是将它们写入文件。 + +考虑请虚拟助手帮忙,但如果这样做,请告诉它不要使用`with`语句或`try`语句。 + +### 8.12.3\. 练习 + +“Wordle”是一个在线字谜游戏,目标是在六次或更少的尝试中猜出一个五个字母的单词。每次尝试必须被识别为一个单词,不包括专有名词。每次尝试后,你会得到有关你猜测的字母哪些出现在目标单词中,哪些在正确位置的信息。 + +例如,假设目标单词是`MOWER`,你猜测了`TRIED`。你会得知`E`在单词中并且位置正确,`R`在单词中但位置不正确,`T`、`I`和`D`不在单词中。 + +作为一个不同的例子,假设你已经猜测了单词`SPADE`和`CLERK`,并且你得知`E`在这个单词中,但不在这两个位置上,且其他字母都没有出现在单词中。在单词列表中的单词中,有多少个可能是目标单词?编写一个名为`check_word`的函数,接收一个五个字母的单词并检查它是否可能是目标单词,基于这些猜测。 + +你可以使用上一章中的任何函数,如`uses_any`。 + +### 8.12.4\. 练习 + +继续上一个练习,假设你猜测了单词`TOTEM`,并得知`E`*仍然*不在正确的位置,但`M`在正确的位置。剩下多少个单词? + +### 8.12.5\. 练习 + +*基督山伯爵*是亚历山大·仲马的小说,被认为是经典。然而,在这本书的英文版序言中,作家翁贝托·埃科承认他发现这本书是“有史以来写得最糟糕的小说之一”。 + +特别地,他说它在“重复使用相同的形容词”方面是“厚颜无耻的”,并特别提到“它的角色要么颤抖,要么变得苍白”的次数。 + +为了验证他的意见是否有效,让我们统计包含单词`pale`的所有行,无论是`pale`、`pales`、`paled`、`paleness`,还是相关的单词`pallor`。使用一个正则表达式匹配这些单词。作为额外的挑战,确保它不会匹配其他单词,比如`impale`——你可能需要向虚拟助手寻求帮助。 + +[Think Python: 第三版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可:[MIT 许可证](https://mit-license.org/) + +文本许可:[创作共用 署名-非商业性使用-相同方式共享 4.0 国际版](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_11.md b/translations/zh/tnkpy3e_11.md new file mode 100644 index 0000000..b052ed5 --- /dev/null +++ b/translations/zh/tnkpy3e_11.md @@ -0,0 +1,786 @@ +# 9\. 列表 + +> 原文:[`allendowney.github.io/ThinkPython/chap09.html`](https://allendowney.github.io/ThinkPython/chap09.html) + +本章介绍了 Python 最有用的内建类型之一——列表。你还将学习更多关于对象的知识,以及当多个变量指向同一对象时会发生什么。 + +在本章末的练习中,我们将创建一个单词列表,并用它来查找特殊单词,如回文和变位词。 + +## 9.1\. 列表是一个序列 + +像字符串一样,**列表**是值的序列。在字符串中,值是字符;在列表中,它们可以是任何类型。列表中的值称为**元素**。 + +有多种方法可以创建一个新列表;最简单的方法是将元素括在方括号(`[`和`]`)中。例如,这是一个包含两个整数的列表。 + +```py +numbers = [42, 123] +``` + +这里是一个包含三个字符串的列表。 + +```py +cheeses = ['Cheddar', 'Edam', 'Gouda'] +``` + +列表的元素不必是相同的类型。以下列表包含了一个字符串,一个浮点数,一个整数,甚至另一个列表。 + +```py +t = ['spam', 2.0, 5, [10, 20]] +``` + +一个包含在另一个列表中的列表被称为**嵌套**列表。 + +一个不包含任何元素的列表被称为空列表;你可以用空括号`[]`来创建一个空列表。 + +```py +empty = [] +``` + +`len`函数返回列表的长度。 + +```py +len(cheeses) +``` + +```py +3 +``` + +空列表的长度是`0`。 + +以下图显示了`cheeses`、`numbers`和`empty`的状态图。 + +![_images/957063a97a11b84c82fcc4b0a736a796b5d1a3da6dc4f71db1409a9e25d1a570.png](img/7a76c4015360d2c859efe99a405dc889.png) + +列表用带有“list”字样的盒子表示,列表的编号元素位于其中。 + +## 9.2\. 列表是可变的 + +要读取列表的一个元素,我们可以使用括号运算符。第一个元素的索引是`0`。 + +```py +cheeses[0] +``` + +```py +'Cheddar' +``` + +与字符串不同,列表是可变的。当括号运算符出现在赋值语句的左侧时,它标识了将被赋值的列表元素。 + +```py +numbers[1] = 17 +numbers +``` + +```py +[42, 17] +``` + +`numbers`的第二个元素,原本是`123`,现在是`17`。 + +列表索引的工作方式与字符串索引相同: + ++ 任何整数表达式都可以用作索引。 + ++ 如果你尝试读取或写入一个不存在的元素,将会出现`IndexError`。 + ++ 如果索引是负值,它从列表的末尾开始倒数。 + +`in`运算符作用于列表——它检查给定的元素是否出现在列表中的任何位置。 + +```py +'Edam' in cheeses +``` + +```py +True +``` + +```py +'Wensleydale' in cheeses +``` + +```py +False +``` + +虽然一个列表可以包含另一个列表,但嵌套的列表仍然被视为一个元素——因此,在以下列表中,只有四个元素。 + +```py +t = ['spam', 2.0, 5, [10, 20]] +len(t) +``` + +```py +4 +``` + +而`10`不被视为`t`的一个元素,因为它是嵌套列表中的一个元素,而不是`t`的元素。 + +```py +10 in t +``` + +```py +False +``` + +## 9.3\. 列表切片 + +切片运算符作用于列表的方式与它在字符串上作用的方式相同。以下示例选择了四个字母列表中的第二和第三个元素。 + +```py +letters = ['a', 'b', 'c', 'd'] +letters[1:3] +``` + +```py +['b', 'c'] +``` + +如果省略第一个索引,切片将从列表的开头开始。 + +```py +letters[:2] +``` + +```py +['a', 'b'] +``` + +如果省略第二个索引,切片将延伸到列表的末尾。 + +```py +letters[2:] +``` + +```py +['c', 'd'] +``` + +如果省略两个索引,切片将是整个列表的副本。 + +```py +letters[:] +``` + +```py +['a', 'b', 'c', 'd'] +``` + +复制列表的另一种方式是使用 `list` 函数。 + +```py +list(letters) +``` + +```py +['a', 'b', 'c', 'd'] +``` + +由于 `list` 是一个内置函数的名称,你应该避免将它用作变量名。 + +## 9.4\. 列表操作 + +`+` 运算符用于连接列表。 + +```py +t1 = [1, 2] +t2 = [3, 4] +t1 + t2 +``` + +```py +[1, 2, 3, 4] +``` + +`*` 运算符会将列表重复给定的次数。 + +```py +['spam'] * 4 +``` + +```py +['spam', 'spam', 'spam', 'spam'] +``` + +没有其他数学运算符可以与列表一起使用,但内置函数 `sum` 会将元素相加。 + +```py +sum(t1) +``` + +```py +3 +``` + +`min` 和 `max` 用于找到最小和最大元素。 + +```py +min(t1) +``` + +```py +1 +``` + +```py +max(t2) +``` + +```py +4 +``` + +## 9.5\. 列表方法 + +Python 提供了对列表操作的方法。例如,`append` 会将一个新元素添加到列表的末尾: + +```py +letters.append('e') +letters +``` + +```py +['a', 'b', 'c', 'd', 'e'] +``` + +`extend` 接受一个列表作为参数,并将其中的所有元素附加到当前列表中: + +```py +letters.extend(['f', 'g']) +letters +``` + +```py +['a', 'b', 'c', 'd', 'e', 'f', 'g'] +``` + +有两种方法可以从列表中删除元素。如果你知道要删除元素的索引,可以使用 `pop`。 + +```py +t = ['a', 'b', 'c'] +t.pop(1) +``` + +```py +'b' +``` + +返回值是被删除的元素。我们也可以确认列表已经被修改。 + +```py +t +``` + +```py +['a', 'c'] +``` + +如果你知道要删除的元素(但不知道索引),可以使用 `remove`: + +```py +t = ['a', 'b', 'c'] +t.remove('b') +``` + +`remove` 的返回值是 `None`,但我们可以确认列表已经被修改。 + +```py +t +``` + +```py +['a', 'c'] +``` + +如果你请求的元素不在列表中,那就会抛出 ValueError 错误。 + +```py +t.remove('d') +``` + +```py +ValueError: list.remove(x): x not in list +``` + +## 9.6\. 列表和字符串 + +字符串是字符的序列,而列表是值的序列,但字符列表和字符串并不相同。要将字符串转换为字符列表,可以使用 `list` 函数。 + +```py +s = 'spam' +t = list(s) +t +``` + +```py +['s', 'p', 'a', 'm'] +``` + +`list` 函数将字符串拆分为单独的字母。如果你想将字符串拆分为单词,可以使用 `split` 方法: + +```py +s = 'pining for the fjords' +t = s.split() +t +``` + +```py +['pining', 'for', 'the', 'fjords'] +``` + +一个可选参数叫做**分隔符**,用于指定哪些字符作为单词边界。以下示例使用了连字符作为分隔符。 + +```py +s = 'ex-parrot' +t = s.split('-') +t +``` + +```py +['ex', 'parrot'] +``` + +如果你有一个字符串列表,可以使用 `join` 将它们连接成一个单一的字符串。`join` 是一个字符串方法,因此你需要在分隔符上调用它,并将列表作为参数传递。 + +```py +delimiter = ' ' +t = ['pining', 'for', 'the', 'fjords'] +s = delimiter.join(t) +s +``` + +```py +'pining for the fjords' +``` + +在这个例子中,分隔符是空格字符,所以 `join` 会在单词之间加上空格。要将字符串连接在一起而不添加空格,可以使用空字符串 `''` 作为分隔符。 + +## 9.7\. 遍历列表 + +你可以使用 `for` 语句遍历列表中的元素。 + +```py +for cheese in cheeses: + print(cheese) +``` + +```py +Cheddar +Edam +Gouda +``` + +例如,使用 `split` 将字符串分割成单词列表后,我们可以使用 `for` 遍历它们。 + +```py +s = 'pining for the fjords' + +for word in s.split(): + print(word) +``` + +```py +pining +for +the +fjords +``` + +对一个空列表进行 `for` 循环时,缩进的语句永远不会执行。 + +```py +for x in []: + print('This never happens.') +``` + +## 9.8\. 排序列表 + +Python 提供了一个内置函数 `sorted`,用于对列表的元素进行排序。 + +```py +scramble = ['c', 'a', 'b'] +sorted(scramble) +``` + +```py +['a', 'b', 'c'] +``` + +原始列表保持不变。 + +```py +scramble +``` + +```py +['c', 'a', 'b'] +``` + +`sorted` 可以与任何类型的序列一起使用,不仅限于列表。所以我们可以像这样对字符串中的字母进行排序。 + +```py +sorted('letters') +``` + +```py +['e', 'e', 'l', 'r', 's', 't', 't'] +``` + +结果是一个列表。要将列表转换为字符串,我们可以使用 `join`。 + +```py +''.join(sorted('letters')) +``` + +```py +'eelrstt' +``` + +使用空字符串作为分隔符时,列表中的元素将被连接在一起,中间没有任何分隔符。 + +## 9.9\. 对象和值 + +如果我们运行这些赋值语句: + +```py +a = 'banana' +b = 'banana' +``` + +我们知道 `a` 和 `b` 都指向一个字符串,但我们不知道它们是否指向*同一个*字符串。有两种可能的状态,如下图所示。 + +![_images/2e5ee2dabd4af114f59b1130cfa488eb144238c577461e791df9db8984a2de95.png](img/d47075b35980a553f7e01bc56e1e6c1f.png) + +在左侧的图表中,`a` 和 `b` 引用两个具有相同值的不同对象。在右侧的图表中,它们引用同一个对象。要检查两个变量是否引用同一个对象,可以使用 `is` 运算符。 + +```py +a = 'banana' +b = 'banana' +a is b +``` + +```py +True +``` + +在这个例子中,Python 只创建了一个字符串对象,`a` 和 `b` 都引用它。但是当你创建两个列表时,你得到两个对象。 + +```py +a = [1, 2, 3] +b = [1, 2, 3] +a is b +``` + +```py +False +``` + +所以状态图看起来是这样的。 + +![_images/c8cfa39ca5b7bb5dc02b5731f4e053972e409045852e63845c82d4f14657fb77.png](img/32a99d0796e7c6eb18f5b3038069d04e.png) + +在这种情况下,我们会说这两个列表是**等价**的,因为它们有相同的元素,但不是**相同**的,因为它们不是同一个对象。如果两个对象是相同的,则它们也是等价的,但如果它们是等价的,则它们不一定是相同的。 + +## 9.10\. 别名 + +如果 `a` 引用一个对象,然后你赋值 `b = a`,那么两个变量都引用同一个对象。 + +```py +a = [1, 2, 3] +b = a +b is a +``` + +```py +True +``` + +所以状态图看起来是这样的。 + +![_images/bdf2f2d164766afae1abfc341924bf22fd6235f7db44edc4d5ab322d1a5323a1.png](img/90be22db2df1a3c8bf4e0ef5ba93a2a5.png) + +将变量与对象的关联称为**引用**。在这个例子中,有两个对同一对象的引用。 + +拥有多个引用的对象有多个名称,因此我们说对象是**别名**的。如果别名对象是可变的,则使用一个名称进行更改会影响到另一个名称。在这个例子中,如果我们更改 `b` 所引用的对象,也会更改 `a` 所引用的对象。 + +```py +b[0] = 5 +a +``` + +```py +[5, 2, 3] +``` + +因此我们会说 `a` “看到”了这个变化。虽然这种行为可能很有用,但也容易出错。一般来说,在处理可变对象时最好避免使用别名。 + +对于像字符串这样的不可变对象,别名不是太大的问题。在这个例子中: + +```py +a = 'banana' +b = 'banana' +``` + +是否 `a` 和 `b` 引用同一个字符串几乎没有影响。 + +## 9.11\. 列表参数 + +当你将列表传递给函数时,函数会得到对列表的引用。如果函数修改了列表,则调用者会看到更改。例如,`pop_first` 使用列表方法 `pop` 来删除列表中的第一个元素。 + +```py +def pop_first(lst): + return lst.pop(0) +``` + +我们可以这样使用它。 + +```py +letters = ['a', 'b', 'c'] +pop_first(letters) +``` + +```py +'a' +``` + +返回值是已从列表中删除的第一个元素,我们可以通过显示修改后的列表来看到。 + +```py +letters +``` + +```py +['b', 'c'] +``` + +在这个例子中,参数 `lst` 和变量 `letters` 是同一个对象的别名,所以状态图看起来是这样的: + +```py +[2.04, 1.24, 1.06, 0.85] +``` + +![_images/afbc48196f8175d6ba5fc8d3b6406623158939478a7457ea70ae7c0aab651eb8.png](img/7f7d1aba33d53ebfe1703790b81777d2.png) + +将对象的引用作为参数传递给函数会创建一种别名形式。如果函数修改了该对象,这些更改将在函数结束后持续存在。 + +## 9.12\. 创建单词列表 + +在上一章中,我们读取了 `words.txt` 文件并搜索了具有特定属性的单词,比如使用字母 `e`。但是我们多次读取整个文件,这样效率不高。更好的做法是只读取一次文件,并将单词存入列表。以下循环展示了如何操作。 + +```py +word_list = [] + +for line in open('words.txt'): + word = line.strip() + word_list.append(word) + +len(word_list) +``` + +```py +113783 +``` + +在循环之前,`word_list` 被初始化为空列表。每次循环时,`append` 方法会将一个单词添加到列表末尾。当循环结束时,列表中有超过 113,000 个单词。 + +另一种做法是使用 `read` 将整个文件读取为一个字符串。 + +```py +string = open('words.txt').read() +len(string) +``` + +```py +1016511 +``` + +结果是一个包含超过百万个字符的单一字符串。我们可以使用 `split` 方法将其拆分为一个单词列表。 + +```py +word_list = string.split() +len(word_list) +``` + +```py +113783 +``` + +现在,为了检查一个字符串是否出现在列表中,我们可以使用 `in` 运算符。例如,`'demotic'` 在列表中。 + +```py +'demotic' in word_list +``` + +```py +True +``` + +但是,`'contrafibularities'` 不是。 + +```py +'contrafibularities' in word_list +``` + +```py +False +``` + +我得说,我对它感到有点麻木。 + +## 9.13\. 调试 + +请注意,大多数列表方法修改参数并返回 `None`。这与字符串方法相反,后者返回一个新字符串,并且不修改原始字符串。 + +如果你习惯于编写像这样的字符串代码: + +```py +word = 'plumage!' +word = word.strip('!') +word +``` + +```py +'plumage' +``` + +很容易写出像这样的列表代码: + +```py +t = [1, 2, 3] +t = t.remove(3) # WRONG! +``` + +`remove` 修改列表并返回 `None`,因此你接下来在 `t` 上执行的操作可能会失败。 + +```py +t.remove(2) +``` + +```py +AttributeError: 'NoneType' object has no attribute 'remove' +``` + +这个错误信息需要一些解释。一个**属性**是与对象关联的变量或方法。在这个案例中,`t` 的值是 `None`,它是一个 `NoneType` 对象,并没有一个名为 `remove` 的属性,因此结果是 `AttributeError`。 + +如果你看到这样的错误信息,应该向后检查程序,看看你是否错误地调用了列表方法。 + +## 9.14\. 术语表 + +**列表:** 一种包含一系列值的对象。 + +**元素:** 列表或其他序列中的一个值。 + +**嵌套列表:** 作为另一个列表的元素的列表。 + +**分隔符:** 用于指示字符串应该在哪儿拆分的字符或字符串。 + +**等价的:** 具有相同的值。 + +**相同的:** 是指相同的对象(这意味着等价性)。 + +**引用:** 变量与其值之间的关联。 + +**别名化:** 如果有多个变量引用同一个对象,那么这个对象就是别名化的。 + +**属性:** 与对象关联的命名值之一。 + +## 9.15\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +### 9.15.1\. 向虚拟助手提问 + +在本章中,我使用了“contrafibularities”和“anaspeptic”这两个词,但它们实际上并不是英语单词。它们出现在英国电视节目*黑爵士*第 3 季第 2 集“墨水与无能”中。 + +然而,当我询问 ChatGPT 3.5(2023 年 8 月 3 日版本)这些单词的来源时,它最初声称这些单词来自《蒙提·派森》,后来又声称它们来自汤姆·斯托帕德的剧作*《罗斯恩·克兰茨与吉尔登斯特恩死了》*。 + +如果你现在提问,你可能会得到不同的结果。但这个例子提醒我们,虚拟助手并不总是准确的,因此你应该检查结果是否正确。随着经验的积累,你会对哪些问题虚拟助手能够可靠回答有一个直觉。在这个例子中,常规的网络搜索可以迅速识别这些单词的来源。 + +如果在本章的任何练习中遇到困难,可以考虑向虚拟助手寻求帮助。如果你得到的结果使用了我们还没有学过的功能,你可以为虚拟助手分配一个“角色”。 + +例如,在你提问之前,尝试输入“Role: Basic Python Programming Instructor”。之后,你得到的回答应该仅使用基本功能。如果你仍然看到我们还没有学过的功能,你可以跟进询问:“能否只用基本的 Python 功能编写那个?” + +### 9.15.2\. 练习 + +如果两个单词的字母可以重新排列使其拼写为另一个单词,则这两个单词是字谜。例如,`tops`是`stop`的字谜。 + +检查两个单词是否是字谜的一种方法是将两个单词中的字母排序。如果排序后的字母列表相同,则这两个单词是字谜。 + +编写一个名为`is_anagram`的函数,该函数接受两个字符串,并返回`True`(如果它们是字谜)或`False`(如果它们不是字谜)。 + +使用你的函数和单词列表,找到`takes`的所有字谜。 + +### 9.15.3\. 练习 + +Python 提供了一个名为`reversed`的内置函数,它接受一个序列(如列表或字符串)作为参数,并返回一个`reversed`对象,其中包含按相反顺序排列的元素。 + +```py +reversed('parrot') +``` + +```py + +``` + +如果你希望反转的元素以列表形式返回,可以使用`list`函数。 + +```py +list(reversed('parrot')) +``` + +```py +['t', 'o', 'r', 'r', 'a', 'p'] +``` + +或者,如果你希望它们以字符串形式呈现,可以使用`join`方法。 + +```py +''.join(reversed('parrot')) +``` + +```py +'torrap' +``` + +所以我们可以这样编写一个函数来反转一个单词。 + +```py +def reverse_word(word): + return ''.join(reversed(word)) +``` + +回文是指正着读和反着读都一样的单词,如“noon”和“rotator”。编写一个名为`is_palindrome`的函数,该函数接受一个字符串作为参数,如果它是回文,返回`True`,否则返回`False`。 + +你可以使用以下循环查找单词列表中至少包含 7 个字母的所有回文。 + +```py +for word in word_list: + if len(word) >= 7 and is_palindrome(word): + print(word) +``` + +### 9.15.4\. 练习 + +编写一个名为`reverse_sentence`的函数,该函数接受一个字符串作为参数,该字符串包含由空格分隔的若干单词。它应该返回一个新的字符串,其中包含按相反顺序排列的单词。例如,如果参数是“Reverse this sentence”,结果应该是“Sentence this reverse”。 + +提示:你可以使用`capitalize`方法将第一个单词首字母大写,并将其他单词转换为小写。 + +### 9.15.5\. 练习 + +编写一个名为`total_length`的函数,接受一个字符串列表,并返回这些字符串的总长度。`word_list`中单词的总长度应该是\(902{,}728\)。 + +[《思考 Python:第三版》](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可证:[MIT 许可证](https://mit-license.org/) + +文本许可证:[创意共享署名-非商业性使用-相同方式共享 4.0 国际版](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_12.md b/translations/zh/tnkpy3e_12.md new file mode 100644 index 0000000..7625d41 --- /dev/null +++ b/translations/zh/tnkpy3e_12.md @@ -0,0 +1,676 @@ +# 10\. 字典 + +> 原文:[`allendowney.github.io/ThinkPython/chap10.html`](https://allendowney.github.io/ThinkPython/chap10.html) + +本章介绍了一种内置类型——字典。它是 Python 最棒的特性之一,也是许多高效优雅算法的构建模块。 + +我们将使用字典来计算书中独特单词的数量以及每个单词出现的次数。在练习中,我们还将使用字典来解决单词谜题。 + +## 10.1\. 字典是一个映射 + +**字典**像列表,但更为通用。在列表中,索引必须是整数;在字典中,索引可以是(几乎)任何类型。例如,假设我们创建一个数字单词的列表,如下所示。 + +```py +lst = ['zero', 'one', 'two'] +``` + +我们可以使用整数作为索引来获取对应的单词。 + +```py +lst[1] +``` + +```py +'one' +``` + +但假设我们想反过来查找一个单词,以得到对应的整数。我们不能用列表做到这一点,但可以用字典。我们首先创建一个空字典,并将其赋值给 `numbers`。 + +```py +numbers = {} +numbers +``` + +```py +{} +``` + +花括号 `{}` 表示一个空字典。要向字典中添加项目,我们将使用方括号。 + +```py +numbers['zero'] = 0 +``` + +本作业向字典添加了一个**项目**,表示**键**与**值**的关联。在这个例子中,键是字符串 `'zero'`,值是整数 `0`。如果我们显示字典,会看到它包含一个项目,项目中的键和值由冒号 `:` 分隔。 + +```py +numbers +``` + +```py +{'zero': 0} +``` + +我们可以像这样添加更多项目。 + +```py +numbers['one'] = 1 +numbers['two'] = 2 +numbers +``` + +```py +{'zero': 0, 'one': 1, 'two': 2} +``` + +现在字典包含了三个项目。 + +要查找键并获取对应的值,我们使用括号运算符。 + +```py +numbers['two'] +``` + +```py +2 +``` + +如果键不在字典中,我们会得到一个 `KeyError`。 + +```py +numbers['three'] +``` + +```py +KeyError: 'three' +``` + +`len` 函数适用于字典;它返回项目的数量。 + +```py +len(numbers) +``` + +```py +3 +``` + +用数学语言来说,字典表示从键到值的**映射**,因此你也可以说每个键“映射到”一个值。在这个例子中,每个数字单词映射到对应的整数。 + +下图展示了 `numbers` 的状态图。 + +![_images/b4f6ce142833c443434f72ebc060ad7736df776d1330d5455522f6c53397a0d0.png](img/8084970e2e60b78c27070840c99e88f3.png) + +字典通过一个框表示,框外有“dict”字样,框内是各项内容。每个项目由一个键和指向值的箭头表示。引号表明这里的键是字符串,而不是变量名。 + +## 10.2\. 创建字典 + +在前一节中,我们创建了一个空字典,并使用括号运算符一次添加一个项目。相反,我们也可以像这样一次性创建字典。 + +```py +numbers = {'zero': 0, 'one': 1, 'two': 2} +``` + +每个项目由键和值组成,键和值之间用冒号分隔。项目之间用逗号分隔,并被花括号括起来。 + +另一种创建字典的方法是使用 `dict` 函数。我们可以像这样创建一个空字典。 + +```py +empty = dict() +empty +``` + +```py +{} +``` + +我们也可以像这样复制一个字典。 + +```py +numbers_copy = dict(numbers) +numbers_copy +``` + +```py +{'zero': 0, 'one': 1, 'two': 2} +``` + +在执行修改字典的操作之前,通常建议先创建一个字典的副本。 + +## 10.3\. `in`操作符 + +`in`操作符也适用于字典;它会告诉你某个元素是否作为*键*出现在字典中。 + +```py +'one' in numbers +``` + +```py +True +``` + +`in`操作符*不会*检查某个元素是否作为值出现。 + +```py +1 in numbers +``` + +```py +False +``` + +要查看某个元素是否作为字典中的值出现,你可以使用`values`方法,它返回一个值序列,然后使用`in`操作符。 + +```py +1 in numbers.values() +``` + +```py +True +``` + +Python 字典中的项存储在一个**哈希表**中,哈希表是一种组织数据的方式,具有一个显著的特性:无论字典中有多少项,`in`操作符所需的时间大致相同。这使得编写一些高效的算法成为可能。 + +为了演示,我们将比较两种算法,用于寻找一对单词,其中一个是另一个的反转——比如`stressed`和`desserts`。我们将从读取单词列表开始。 + +```py +word_list = open('words.txt').read().split() +len(word_list) +``` + +```py +113783 +``` + +这是上一章中的`reverse_word`函数。 + +```py +def reverse_word(word): + return ''.join(reversed(word)) +``` + +以下函数遍历列表中的单词。对于每个单词,它会反转字母,然后检查反转后的单词是否在单词列表中。 + +```py +def too_slow(): + count = 0 + for word in word_list: + if reverse_word(word) in word_list: + count += 1 + return count +``` + +这个函数运行需要超过一分钟。问题在于,`in`操作符会逐个检查列表中的单词,从头开始。如果它没有找到需要的内容——大多数情况下是这样——它就必须一直搜索到末尾。 + +并且`in`操作符位于循环内部,因此它会为每个单词执行一次。由于列表中有超过 10 万个单词,而对于每个单词我们检查超过 10 万个单词,总的比较次数是单词数的平方——大约是 130 亿次。 + +```py +len(word_list)**2 +``` + +```py +12946571089 +``` + +我们可以通过字典使这个函数变得更快。以下循环创建一个字典,将单词作为键存储。 + +```py +word_dict = {} +for word in word_list: + word_dict[word] = 1 +``` + +`word_dict`中的值都是`1`,但它们可以是任何值,因为我们永远不会查找它们——我们仅仅使用这个字典来检查键是否存在。 + +现在这里有一个版本的函数,它将`word_list`替换为`word_dict`。 + +```py +def much_faster(): + count = 0 + for word in word_dict: + if reverse_word(word) in word_dict: + count += 1 + return count +``` + +这个函数运行时间不到一秒钟,因此比之前的版本快大约 10,000 倍。 + +通常,在列表中查找元素所需的时间与列表的长度成正比。而在字典中查找一个键的时间几乎是恒定的——无论项目的数量是多少。 + +## 10.4\. 计数器集合 + +假设你有一个字符串,并且想要统计每个字母出现的次数。字典是做这件事的一个好工具。我们从一个空字典开始。 + +```py +counter = {} +``` + +当我们遍历字符串中的字母时,假设我们第一次看到字母`'a'`。我们可以像这样将它添加到字典中。 + +```py +counter['a'] = 1 +``` + +值 `1` 表示我们已看到该字母一次。稍后,如果我们再次看到相同的字母,可以像这样增加计数器。 + +```py +counter['a'] += 1 +``` + +现在与 `'a'` 相关联的值是 `2`,因为我们已经看到了两次该字母。 + +```py +counter +``` + +```py +{'a': 2} +``` + +以下函数使用这些功能来计算每个字母在字符串中出现的次数。 + +```py +def value_counts(string): + counter = {} + for letter in string: + if letter not in counter: + counter[letter] = 1 + else: + counter[letter] += 1 + return counter +``` + +每次循环时,如果 `letter` 不在字典中,我们就创建一个键为 `letter`、值为 `1` 的新项。如果 `letter` 已经在字典中,我们就增加与 `letter` 相关联的值。 + +这是一个例子。 + +```py +counter = value_counts('brontosaurus') +counter +``` + +```py +{'b': 1, 'r': 2, 'o': 2, 'n': 1, 't': 1, 's': 2, 'a': 1, 'u': 2} +``` + +`counter` 中的项目显示字母 `'b'` 出现一次,字母 `'r'` 出现两次,以此类推。 + +## 10.5\. 循环和字典 + +如果你在 `for` 语句中使用字典,它会遍历字典的键。为了演示,让我们创建一个字典来统计 `'banana'` 中字母的出现次数。 + +```py +counter = value_counts('banana') +counter +``` + +```py +{'b': 1, 'a': 3, 'n': 2} +``` + +以下循环打印的是键,它们是字母。 + +```py +for key in counter: + print(key) +``` + +```py +b +a +n +``` + +为了打印值,我们可以使用 `values` 方法。 + +```py +for value in counter.values(): + print(value) +``` + +```py +1 +3 +2 +``` + +为了打印键和值,我们可以遍历键并查找相应的值。 + +```py +for key in counter: + value = counter[key] + print(key, value) +``` + +```py +b 1 +a 3 +n 2 +``` + +在下一章,我们将看到一种更简洁的方法来完成相同的事情。 + +## 10.6\. 列表和字典 + +你可以将列表作为字典的值。例如,下面是一个字典,它将数字 `4` 映射到一个包含四个字母的列表。 + +```py +d = {4: ['r', 'o', 'u', 's']} +d +``` + +```py +{4: ['r', 'o', 'u', 's']} +``` + +但你不能将列表作为字典的键。以下是我们尝试时发生的情况。 + +```py +letters = list('abcd') +d[letters] = 4 +``` + +```py +TypeError: unhashable type: 'list' +``` + +我之前提到过,字典使用哈希表,这意味着键必须是**可哈希的**。 + +**哈希**是一个函数,它接受一个值(任何类型)并返回一个整数。字典使用这些整数,称为哈希值,用来存储和查找键。 + +这个系统只有在键是不可变的情况下才有效,因此它的哈希值始终相同。但如果键是可变的,它的哈希值可能会改变,字典将无法工作。这就是为什么键必须是可哈希的,以及为什么像列表这样的可变类型不能作为键的原因。 + +由于字典是可变的,它们也不能作为键使用。不过,它们*可以*作为值使用。 + +## 10.7\. 累积列表 + +对于许多编程任务,遍历一个列表或字典同时构建另一个是非常有用的。例如,我们将遍历 `word_dict` 中的单词,生成一个回文列表——即那些正反拼写相同的单词,如“noon”和“rotator”。 + +在上一章,其中一个练习要求你编写一个函数,检查一个单词是否是回文。这里是一个使用 `reverse_word` 的解决方案。 + +```py +def is_palindrome(word): + """Check if a word is a palindrome.""" + return reverse_word(word) == word +``` + +如果我们遍历 `word_dict` 中的单词,就可以像这样计算回文的数量。 + +```py +count = 0 + +for word in word_dict: + if is_palindrome(word): + count +=1 + +count +``` + +```py +91 +``` + +到现在为止,这个模式已经很熟悉了。 + ++ 在循环之前,`count` 被初始化为 `0`。 + ++ 在循环内,如果 `word` 是回文,我们就增加 `count`。 + ++ 当循环结束时,`count` 包含回文的总数。 + +我们可以使用类似的模式来列出回文。 + +```py +palindromes = [] + +for word in word_dict: + if is_palindrome(word): + palindromes.append(word) + +palindromes[:10] +``` + +```py +['aa', 'aba', 'aga', 'aha', 'ala', 'alula', 'ama', 'ana', 'anna', 'ava'] +``` + +下面是它的工作原理: + ++ 在循环之前,`palindromes` 被初始化为空列表。 + ++ 在循环中,如果 `word` 是回文,我们将它添加到 `palindromes` 的末尾。 + ++ 当循环结束时,`palindromes` 是一个回文列表。 + +在这个循环中,`palindromes` 被用作 **累加器**,即一个在计算过程中收集或累积数据的变量。 + +现在假设我们只想选择具有七个或更多字母的回文。我们可以遍历 `palindromes`,并生成一个新列表,其中只包含长回文。 + +```py +long_palindromes = [] + +for word in palindromes: + if len(word) >= 7: + long_palindromes.append(word) + +long_palindromes +``` + +```py +['deified', 'halalah', 'reifier', 'repaper', 'reviver', 'rotator', 'sememes'] +``` + +遍历列表,选择一些元素并忽略其他元素,这个过程称为 **过滤**。 ## 10.8\. 备忘录 + +如果你运行了来自 第六章 的 `fibonacci` 函数,也许你注意到,提供的参数越大,函数运行的时间就越长。 + +```py +def fibonacci(n): + if n == 0: + return 0 + + if n == 1: + return 1 + + return fibonacci(n-1) + fibonacci(n-2) +``` + +此外,运行时间会迅速增加。为了理解原因,请考虑以下图示,它展示了 `fibonacci` 的 **调用图**,其中 `n=4`: + +![_images/80cfcd12d7c8cde149103a1fe25d2b12bfce1b7cc33163b9e5582f9e32789f5d.png](img/698ff08186ba3a8f135d7c7b88b97009.png) + +调用图展示了一组函数框架,框架之间通过连接线显示每个框架与其调用的函数框架之间的关系。在图的顶部,`fibonacci` 的 `n=4` 调用 `fibonacci` 的 `n=3` 和 `n=2`。接下来,`fibonacci` 的 `n=3` 调用 `fibonacci` 的 `n=2` 和 `n=1`,依此类推。 + +计算 `fibonacci(0)` 和 `fibonacci(1)` 被调用的次数。这是一个低效的解决方案,且随着参数的增大,效率会更差。 + +一种解决方案是通过将已经计算的值存储在字典中来跟踪这些值。一个先前计算过并为以后使用而存储的值被称为 **备忘录**。这是一个“备忘录化”版本的 `fibonacci`: + +```py +known = {0:0, 1:1} + +def fibonacci_memo(n): + if n in known: + return known[n] + + res = fibonacci_memo(n-1) + fibonacci_memo(n-2) + known[n] = res + return res +``` + +`known` 是一个字典,用来追踪我们已经知道的斐波那契数。它一开始有两个条目:`0` 映射到 `0`,`1` 映射到 `1`。 + +每当调用 `fibonacci_memo` 时,它都会检查 `known`。如果结果已经存在,它可以立即返回。否则,它必须计算新的值,将其添加到字典中,并返回该值。 + +比较这两个函数,`fibonacci(40)` 运行大约需要 30 秒,而 `fibonacci_memo(40)` 只需大约 30 微秒,因此速度快了约一百万倍。在本章的笔记本中,你会看到这些测量的来源。 + +## 10.9\. 调试 + +当你处理更大的数据集时,通过打印输出并手动检查调试可能变得非常繁琐。以下是一些调试大数据集的建议: + +1. 缩小输入数据:如果可能,减小数据集的大小。例如,如果程序读取一个文本文件,可以从前 10 行开始,或者从你能找到的最小示例开始。你可以编辑文件本身,或者(更好)修改程序,使其只读取前 `n` 行。 + + 如果发生错误,可以将`n`减小到发生错误的最小值。当你找到并修正错误后,可以逐渐增加`n`。 + +1. 检查摘要和类型:与其打印并检查整个数据集,不如考虑打印数据的摘要,例如字典中的项数或数字列表的总和。 + + 运行时错误的一个常见原因是值的类型不正确。调试此类错误时,通常只需打印出值的类型即可。 + +1. 编写自检代码:有时你可以编写代码来自动检查错误。例如,如果你正在计算一个数字列表的平均值,你可以检查结果是否不大于列表中的最大值,或不小于最小值。这被称为“合理性检查”,因为它能够检测出“不合理”的结果。 + + 另一种检查方法是比较两个不同计算结果的差异,以查看它们是否一致。这叫做“一致性检查”。 + +1. 格式化输出:格式化调试输出可以让错误更容易被发现。在第六章中,我们看到过一个例子。你可能会觉得有用的另一个工具是`pprint`模块,它提供了一个`pprint`函数,以更人性化的格式显示内建类型(`pprint`代表“漂亮打印”)。 + + 再次强调,花时间构建框架可以减少调试时所花费的时间。 + +## 10.10\. 词汇表 + +**字典:** 包含键值对的对象,也叫做项。 + +**项:** 在字典中,键值对的另一种说法。 + +**键:** 在字典中,作为键值对的第一部分出现的对象。 + +**值:** 在字典中,作为键值对的第二部分出现的对象。这个概念比我们之前使用的“值”更为具体。 + +**映射:** 一种关系,其中一个集合的每个元素与另一个集合的元素相对应。 + +**哈希表:** 一种由键值对组成的集合,组织方式使得我们可以高效地查找键并找到其对应的值。 + +**可哈希:** 像整数、浮点数和字符串这样的不可变类型是可哈希的。像列表和字典这样的可变类型则不是。 + +**哈希函数:** 一个接受对象并计算出一个整数,用来在哈希表中定位键的函数。 + +**累加器:** 在循环中用于累加结果的变量。 + +**过滤:** 遍历一个序列并选择或省略元素。 + +**调用图:** 显示程序执行过程中每个帧的图示,图中从每个调用者指向每个被调用者。 + +**备忘录:** 为了避免不必要的未来计算,存储的计算结果。 + +## 10.11\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +### 10.11.1\. 请求助手 + +在本章中,我提到字典中的键必须是可哈希的,并给出了简短的解释。如果你想了解更多细节,可以问虚拟助手:“为什么 Python 字典中的键必须是可哈希的?” + +在前一节中,我们将一组单词存储为字典的键,以便能够使用更高效的`in`运算符版本。我们也可以使用`set`,这是另一种内建的数据类型,来完成同样的操作。你可以问虚拟助手:“如何从字符串列表创建一个 Python 集合,并检查一个字符串是否是集合的元素?” + +### 10.11.2\. 练习 + +字典有一个名为`get`的方法,它接受一个键和一个默认值。如果键出现在字典中,`get`返回对应的值;否则,它返回默认值。例如,这里有一个字典,将字符串中的字母映射到它们出现的次数。 + +```py +counter = value_counts('brontosaurus') +``` + +如果我们查找一个出现在单词中的字母,`get`方法会返回它出现的次数。 + +```py +counter.get('b', 0) +``` + +```py +1 +``` + +如果我们查找一个没有出现的字母,我们会得到默认值`0`。 + +```py +counter.get('c', 0) +``` + +```py +0 +``` + +使用`get`编写`value_counts`的简洁版。你应该能够消除`if`语句。 + +### 10.11.3\. 练习 + +你能想到的最长的单词是什么?其中每个字母只出现一次。让我们看看能否找到一个比`unpredictably`更长的单词。 + +编写一个名为`has_duplicates`的函数,该函数接受一个序列(如列表或字符串)作为参数,并返回`True`,如果序列中有任何元素出现超过一次。 + +### 10.11.4\. 练习 + +编写一个名为`find_repeats`的函数,它接受一个字典,该字典将每个键映射到一个计数器(类似`value_counts`的结果)。该函数应遍历字典并返回一个包含计数大于`1`的键的列表。你可以使用以下框架开始编写代码。 + +```py +def find_repeats(counter): + """Makes a list of keys with values greater than 1. + + counter: dictionary that maps from keys to counts + + returns: list of keys + """ + return [] +``` + +### 10.11.5\. 练习 + +假设你用两个不同的单词运行`value_counts`并将结果保存在两个字典中。 + +```py +counter1 = value_counts('brontosaurus') +counter2 = value_counts('apatosaurus') +``` + +每个字典都将一组字母映射到它们出现的次数。编写一个名为`add_counters`的函数,该函数接受两个这样的字典,并返回一个新的字典,包含所有字母以及它们在两个单词中出现的总次数。 + +有许多方法可以解决这个问题。解决后,你可以考虑问虚拟助手提供其他不同的解决方案。 + +### 10.11.6\. 练习 + +如果一个单词是“交错的”,那么我们可以通过交替取字母将其拆分为两个单词。例如,“schooled”是一个交错词,因为它可以拆分为“shoe”和“cold”。 + +要从一个字符串中选择交替字母,你可以使用切片操作符,它有三个组成部分:起始位置、结束位置和字母之间的“步长”。 + +在以下切片中,第一个组件是`0`,所以我们从第一个字母开始。第二个组件是`None`,这意味着我们应该一直选择到字符串的末尾。第三个组件是`2`,因此我们选择的字母之间有两个步长。 + +```py +word = 'schooled' +first = word[0:None:2] +first +``` + +```py +'shoe' +``` + +我们可以通过完全省略第二个组件,而不是提供`None`,来达到相同的效果。例如,下面的切片选择交替的字母,从第二个字母开始。 + +```py +second = word[1::2] +second +``` + +```py +'cold' +``` + +编写一个名为`is_interlocking`的函数,它接受一个单词作为参数,如果该单词可以被拆分为两个交织的单词,则返回`True`。 + +```py +for word in word_list: + if len(word) >= 8 and is_interlocking(word): + first = word[0::2] + second = word[1::2] + print(word, first, second) +``` + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权 2024 [Allen B. Downey](https://allendowney.com) + +代码许可证: [MIT 许可证](https://mit-license.org/) + +文本许可证: [创意共享署名-非商业性使用-相同方式共享 4.0 国际版](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_13.md b/translations/zh/tnkpy3e_13.md new file mode 100644 index 0000000..4bcc49f --- /dev/null +++ b/translations/zh/tnkpy3e_13.md @@ -0,0 +1,947 @@ +# 11\. 元组 + +> 原文:[`allendowney.github.io/ThinkPython/chap11.html`](https://allendowney.github.io/ThinkPython/chap11.html) + +本章介绍了另一个内建类型——元组,并展示了列表、字典和元组如何协同工作。它还介绍了元组赋值和一个对具有可变长度参数列表的函数非常有用的特性:打包和解包操作符。 + +在练习中,我们将使用元组以及列表和字典来解决更多的单词谜题,并实现高效的算法。 + +有一点需要注意:“tuple”有两种发音方式。有些人说“tuh-ple”,与“supple”押韵。但在编程的上下文中,大多数人说“too-ple”,与“quadruple”押韵。 + +## 11.1\. 元组像列表一样 + +元组是一个值的序列。这些值可以是任何类型,并且按整数索引,因此元组与列表非常相似。重要的区别是元组是不可变的。 + +创建元组时,可以编写一个由逗号分隔的值列表。 + +```py +t = 'l', 'u', 'p', 'i', 'n' +type(t) +``` + +```py +tuple +``` + +尽管不是必须的,但通常会将元组括在圆括号中。 + +```py +t = ('l', 'u', 'p', 'i', 'n') +type(t) +``` + +```py +tuple +``` + +要创建一个包含单一元素的元组,必须包括一个结尾逗号。 + +```py +t1 = 'p', +type(t1) +``` + +```py +tuple +``` + +括号中的单个值不是元组。 + +```py +t2 = ('p') +type(t2) +``` + +```py +str +``` + +创建元组的另一种方法是使用内建函数`tuple`。如果没有参数,它会创建一个空元组。 + +```py +t = tuple() +t +``` + +```py +() +``` + +如果参数是一个序列(字符串、列表或元组),则结果是一个包含序列元素的元组。 + +```py +t = tuple('lupin') +t +``` + +```py +('l', 'u', 'p', 'i', 'n') +``` + +因为`tuple`是一个内建函数的名称,所以应该避免将其用作变量名。 + +大多数列表操作符也适用于元组。例如,方括号操作符可以索引一个元素。 + +```py +t[0] +``` + +```py +'l' +``` + +而切片操作符用于选择一系列元素。 + +```py +t[1:3] +``` + +```py +('u', 'p') +``` + +`+`操作符用于连接元组。 + +```py +tuple('lup') + ('i', 'n') +``` + +```py +('l', 'u', 'p', 'i', 'n') +``` + +`*`操作符将元组重复给定次数。 + +```py +tuple('spam') * 2 +``` + +```py +('s', 'p', 'a', 'm', 's', 'p', 'a', 'm') +``` + +`sorted`函数也适用于元组——但结果是一个列表,而不是元组。 + +```py +sorted(t) +``` + +```py +['i', 'l', 'n', 'p', 'u'] +``` + +`reversed`函数也可以用于元组。 + +```py +reversed(t) +``` + +```py + +``` + +结果是一个`reversed`对象,我们可以将其转换为列表或元组。 + +```py +tuple(reversed(t)) +``` + +```py +('n', 'i', 'p', 'u', 'l') +``` + +根据目前的例子,元组看起来可能与列表相同。 + +## 11.2\. 但是元组是不可变的 + +如果你尝试使用方括号操作符修改元组,会得到一个`TypeError`。 + +```py +t[0] = 'L' +``` + +```py +TypeError: 'tuple' object does not support item assignment +``` + +而且元组没有修改列表的任何方法,例如`append`和`remove`。 + +```py +t.remove('l') +``` + +```py +AttributeError: 'tuple' object has no attribute 'remove' +``` + +记住,“属性”是与对象相关联的变量或方法——这个错误信息意味着元组没有名为`remove`的方法。 + +由于元组是不可变的,它们是可哈希的,这意味着它们可以用作字典中的键。例如,下面的字典包含两个作为键的元组,它们映射到整数值。 + +```py +d = {} +d[1, 2] = 3 +d[3, 4] = 7 +``` + +我们可以像这样在字典中查找元组: + +```py +d[1, 2] +``` + +```py +3 +``` + +或者,如果我们有一个指向元组的变量,也可以将其作为键使用。 + +```py +t = (3, 4) +d[t] +``` + +```py +7 +``` + +元组也可以作为字典中的值出现。 + +```py +t = tuple('abc') +d = {'key': t} +d +``` + +```py +{'key': ('a', 'b', 'c')} +``` + +## 11.3\. 元组赋值 + +你可以在赋值的左边放一个变量元组,在右边放一个值元组。 + +```py +a, b = 1, 2 +``` + +值会从左到右赋给变量——在这个例子中,`a`得到值`1`,`b`得到值`2`。我们可以这样展示结果: + +```py +a, b +``` + +```py +(1, 2) +``` + +更一般地说,如果赋值的左边是一个元组,右边可以是任何类型的序列——字符串、列表或元组。例如,要将电子邮件地址拆分成用户名和域名,你可以这样写: + +```py +email = 'monty@python.org' +username, domain = email.split('@') +``` + +`split`的返回值是一个包含两个元素的列表——第一个元素赋给`username`,第二个赋给`domain`。 + +```py +username, domain +``` + +```py +('monty', 'python.org') +``` + +左边的变量数量和右边的值数量必须相同——否则会引发`ValueError`。 + +```py +a, b = 1, 2, 3 +``` + +```py +ValueError: too many values to unpack (expected 2) +``` + +如果你想交换两个变量的值,元组赋值非常有用。使用常规赋值时,你需要使用临时变量,像这样: + +```py +temp = a +a = b +b = temp +``` + +这样是可行的,但使用元组赋值我们可以在不使用临时变量的情况下完成相同的操作。 + +```py +a, b = b, a +``` + +之所以可行,是因为右边的所有表达式会在任何赋值操作之前计算。 + +我们还可以在`for`语句中使用元组赋值。例如,要遍历字典中的项,我们可以使用`items`方法。 + +```py +d = {'one': 1, 'two': 2} + +for item in d.items(): + key, value = item + print(key, '->', value) +``` + +```py +one -> 1 +two -> 2 +``` + +每次循环时,`item`会被赋值为一个包含键和对应值的元组。 + +我们可以像这样更简洁地写这个循环: + +```py +for key, value in d.items(): + print(key, '->', value) +``` + +```py +one -> 1 +two -> 2 +``` + +每次循环时,一个键和值会直接赋给`key`和`value`。 + +## 11.4\. 元组作为返回值 + +严格来说,一个函数只能返回一个值,但如果这个值是一个元组,效果上就和返回多个值相同。例如,如果你想除以两个整数并计算商和余数,计算`x//y`然后再计算`x%y`效率不高。最好同时计算它们。 + +内置函数`divmod`接受两个参数,返回一个包含商和余数的元组。 + +```py +divmod(7, 3) +``` + +```py +(2, 1) +``` + +我们可以使用元组赋值将元组中的元素存储在两个变量中。 + +```py +quotient, remainder = divmod(7, 3) +quotient +``` + +```py +2 +``` + +```py +remainder +``` + +```py +1 +``` + +下面是一个返回元组的函数示例。 + +```py +def min_max(t): + return min(t), max(t) +``` + +`max`和`min`是内置函数,用于找到序列中最大的和最小的元素。`min_max`同时计算并返回一个包含两个值的元组。 + +```py +min_max([2, 4, 1, 3]) +``` + +```py +(1, 4) +``` + +我们可以像这样将结果赋给变量: + +```py +low, high = min_max([2, 4, 1, 3]) +low, high +``` + +```py +(1, 4) +``` + +## 11.5\. 参数打包 + +函数可以接受可变数量的参数。以`*`操作符开头的参数名称**打包**参数为元组。例如,下面的函数接受任意数量的参数并计算它们的算术平均值——也就是它们的和除以参数的数量。 + +```py +def mean(*args): + return sum(args) / len(args) +``` + +参数可以有任何你喜欢的名字,但`args`是惯用的。我们可以像这样调用函数: + +```py +mean(1, 2, 3) +``` + +```py +2.0 +``` + +如果你有一个值的序列,并且想将它们作为多个参数传递给一个函数,你可以使用`*`操作符来**解包**元组。例如,`divmod`函数需要两个精确的参数——如果你传递一个元组作为参数,就会报错。 + +```py +t = (7, 3) +divmod(t) +``` + +```py +TypeError: divmod expected 2 arguments, got 1 +``` + +尽管元组包含两个元素,它仍然被视为一个单独的参数。但如果你解包元组,它会被当作两个参数来处理。 + +```py +divmod(*t) +``` + +```py +(2, 1) +``` + +打包和解包可以非常有用,特别是当你想调整一个现有函数的行为时。例如,这个函数接受任意数量的参数,去掉最低和最高的值,然后计算剩余部分的平均值。 + +```py +def trimmed_mean(*args): + low, high = min_max(args) + trimmed = list(args) + trimmed.remove(low) + trimmed.remove(high) + return mean(*trimmed) +``` + +首先,它使用`min_max`找到最低和最高的元素。然后,它将`args`转换为列表,这样就可以使用`remove`方法。最后,它解包这个列表,将元素作为单独的参数传递给`mean`,而不是作为一个列表。 + +这里有一个例子展示了这种影响。 + +```py +mean(1, 2, 3, 10) +``` + +```py +4.0 +``` + +```py +trimmed_mean(1, 2, 3, 10) +``` + +```py +2.5 +``` + +这种“修剪过的”平均值在一些主观评分的体育项目中使用——比如跳水和体操——用来减少评分偏离其他裁判的影响。 + +## 11.6\. Zip + +元组在遍历两个序列的元素并对对应元素执行操作时非常有用。例如,假设两支队伍进行七场比赛,并且我们将它们的得分记录在两个列表中,每支队伍一个。 + +```py +scores1 = [1, 2, 4, 5, 1, 5, 2] +scores2 = [5, 5, 2, 2, 5, 2, 3] +``` + +让我们看看每个队伍赢了多少场比赛。我们将使用`zip`,这是一个内置函数,它接受两个或多个序列并返回一个**zip 对象**,因为它像拉链的齿一样将序列的元素配对在一起。 + +```py +zip(scores1, scores2) +``` + +```py + +``` + +我们可以使用 zip 对象按对遍历序列中的值。 + +```py +for pair in zip(scores1, scores2): + print(pair) +``` + +```py +(1, 5) +(2, 5) +(4, 2) +(5, 2) +(1, 5) +(5, 2) +(2, 3) +``` + +每次进入循环时,`pair`都会被赋值为一个包含分数的元组。因此,我们可以将分数赋值给变量,并统计第一支队伍的胜利场数,像这样: + +```py +wins = 0 +for team1, team2 in zip(scores1, scores2): + if team1 > team2: + wins += 1 + +wins +``` + +```py +3 +``` + +可惜的是,第一支队伍只赢了三场比赛并输掉了系列赛。 + +如果你有两个列表,并且想要一个包含配对元素的列表,可以使用`zip`和`list`。 + +```py +t = list(zip(scores1, scores2)) +t +``` + +```py +[(1, 5), (2, 5), (4, 2), (5, 2), (1, 5), (5, 2), (2, 3)] +``` + +结果是一个元组列表,因此我们可以像这样获取最后一场比赛的结果: + +```py +t[-1] +``` + +```py +(2, 3) +``` + +如果你有一个键的列表和一个值的列表,可以使用`zip`和`dict`来创建一个字典。例如,下面是我们如何创建一个字典,将每个字母映射到它在字母表中的位置。 + +```py +letters = 'abcdefghijklmnopqrstuvwxyz' +numbers = range(len(letters)) +letter_map = dict(zip(letters, numbers)) +``` + +现在我们可以查找一个字母并获取它在字母表中的索引。 + +```py +letter_map['a'], letter_map['z'] +``` + +```py +(0, 25) +``` + +在这个映射中,`'a'`的索引是`0`,而`'z'`的索引是`25`。 + +如果你需要遍历一个序列的元素及其索引,可以使用内置函数`enumerate`。 + +```py +enumerate('abc') +``` + +```py + +``` + +结果是一个**enumerate 对象**,它遍历一个由索引(从 0 开始)和给定序列中的元素组成的配对序列。 + +```py +for index, element in enumerate('abc'): + print(index, element) +``` + +```py +0 a +1 b +2 c +``` + +## 11.7\. 比较和排序 + +关系运算符适用于元组和其他序列。例如,如果你用`<`运算符比较元组,它会先比较每个序列中的第一个元素。如果它们相等,则继续比较第二对元素,以此类推,直到找到一对不同的元素。 + +```py +(0, 1, 2) < (0, 3, 4) +``` + +```py +True +``` + +后续的元素不会被考虑——即使它们真的很大。 + +```py +(0, 1, 2000000) < (0, 3, 4) +``` + +```py +True +``` + +这种比较元组的方式对于排序元组列表或找到最小值或最大值非常有用。作为示例,让我们找到一个单词中最常见的字母。在前一章中,我们编写了`value_counts`,它接受一个字符串并返回一个字典,该字典将每个字母映射到其出现的次数。 + +```py +def value_counts(string): + counter = {} + for letter in string: + if letter not in counter: + counter[letter] = 1 + else: + counter[letter] += 1 + return counter +``` + +这是字符串`'banana'`的结果。 + +```py +counter = value_counts('banana') +counter +``` + +```py +{'b': 1, 'a': 3, 'n': 2} +``` + +只有三个项时,我们可以很容易地看到最常见的字母是`'a'`,它出现了三次。但如果有更多项,自动排序将会非常有用。 + +我们可以像这样从`counter`中获取项。 + +```py +items = counter.items() +items +``` + +```py +dict_items([('b', 1), ('a', 3), ('n', 2)]) +``` + +结果是一个`dict_items`对象,它表现得像一个元组列表,因此我们可以像这样对其进行排序。 + +```py +sorted(items) +``` + +```py +[('a', 3), ('b', 1), ('n', 2)] +``` + +默认行为是使用每个元组的第一个元素来排序列表,并使用第二个元素来解决相同的情况。 + +然而,为了找到出现次数最多的项,我们想要使用第二个元素对列表进行排序。我们可以通过编写一个函数来实现,该函数接受一个元组并返回第二个元素。 + +```py +def second_element(t): + return t[1] +``` + +然后我们可以将该函数作为可选参数`key`传递给`sorted`,该参数表示此函数应用于计算每个项的**排序关键字**。 + +```py +sorted_items = sorted(items, key=second_element) +sorted_items +``` + +```py +[('b', 1), ('n', 2), ('a', 3)] +``` + +排序关键字决定了列表中项的顺序。出现次数最少的字母排在前面,出现次数最多的字母排在最后。因此,我们可以像这样找到最常见的字母。 + +```py +sorted_items[-1] +``` + +```py +('a', 3) +``` + +如果我们只需要最大值,我们就不必排序列表。我们可以使用`max`,它也接受`key`作为可选参数。 + +```py +max(items, key=second_element) +``` + +```py +('a', 3) +``` + +要找到出现次数最少的字母,我们可以用`min`来进行相同的操作。 + +## 11.8\. 反转字典 + +假设你想要反转一个字典,以便通过查找一个值来得到对应的键。例如,如果你有一个单词计数器,它将每个单词映射到该单词出现的次数,你可以创建一个字典,将整数映射到出现相应次数的单词。 + +但是有一个问题——字典中的键必须是唯一的,但值不一定是唯一的。例如,在一个单词计数器中,可能有许多单词的出现次数相同。 + +所以反转字典的一种方法是创建一个新字典,其中值是原字典中键的列表。作为示例,让我们统计`parrot`中字母的出现次数。 + +```py +d = value_counts('parrot') +d +``` + +```py +{'p': 1, 'a': 1, 'r': 2, 'o': 1, 't': 1} +``` + +如果我们反转这个字典,结果应该是`{1: ['p', 'a', 'o', 't'], 2: ['r']}`,这表示出现一次的字母是`'p'`、`'a'`、`'o'`和`'t'`,出现两次的字母是`'r'`。 + +以下函数接受一个字典并将其反转为一个新的字典。 + +```py +def invert_dict(d): + new = {} + for key, value in d.items(): + if value not in new: + new[value] = [key] + else: + new[value].append(key) + return new +``` + +`for`语句遍历`d`中的键和值。如果该值尚未在新字典中,则将其添加并与包含单个元素的列表相关联。否则,它将被追加到现有的列表中。 + +我们可以这样测试它: + +```py +invert_dict(d) +``` + +```py +{1: ['p', 'a', 'o', 't'], 2: ['r']} +``` + +我们得到了预期的结果。 + +这是我们看到的第一个字典中的值是列表的例子。我们会看到更多类似的例子! + +## 11.9\. 调试 + +列表、字典和元组是**数据结构**。在本章中,我们开始看到复合数据结构,例如元组的列表,或者包含元组作为键和列表作为值的字典。复合数据结构很有用,但容易因数据结构的类型、大小或结构错误而导致错误。例如,如果一个函数期望一个整数列表,而你给它一个普通的整数(不是列表),它可能无法正常工作。 + +为了帮助调试这些错误,我编写了一个名为`structshape`的模块,它提供了一个同名的函数,可以将任何类型的数据结构作为参数,并返回一个字符串来总结其结构。你可以从[`raw.githubusercontent.com/AllenDowney/ThinkPython/v3/structshape.py`](https://raw.githubusercontent.com/AllenDowney/ThinkPython/v3/structshape.py)下载它。 + +我们可以这样导入它。 + +```py +from structshape import structshape +``` + +这是一个简单列表的例子。 + +```py +t = [1, 2, 3] +structshape(t) +``` + +```py +'list of 3 int' +``` + +这里有一个列表的列表。 + +```py +t2 = [[1,2], [3,4], [5,6]] +structshape(t2) +``` + +```py +'list of 3 list of 2 int' +``` + +如果列表中的元素类型不同,`structshape`会按类型将它们分组。 + +```py +t3 = [1, 2, 3, 4.0, '5', '6', [7], [8], 9] +structshape(t3) +``` + +```py +'list of (3 int, float, 2 str, 2 list of int, int)' +``` + +这里有一个元组的列表。 + +```py +s = 'abc' +lt = list(zip(t, s)) +structshape(lt) +``` + +```py +'list of 3 tuple of (int, str)' +``` + +这是一个包含三个项的字典,将整数映射到字符串。 + +```py +d = dict(lt) +structshape(d) +``` + +```py +'dict of 3 int->str' +``` + +如果你在跟踪数据结构时遇到困难,`structshape`可以帮助你。 + +## 11.10\. 术语表 + +**打包:** 将多个参数收集到一个元组中。 + +**解包:** 将元组(或其他序列)视为多个参数。 + +**zip 对象:** 调用内置函数`zip`的结果,可以用来遍历一系列元组。 + +**enumerate 对象:** 调用内置函数`enumerate`的结果,可以用来遍历一系列元组。 + +**排序键:** 用于排序集合元素的值或计算该值的函数。 + +**数据结构:** 一组有组织的值,用于高效地执行某些操作。 + +## 11.11\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +### 11.11.1\. 向虚拟助手提问 + +本章中的练习可能比前几章的练习更难,因此我鼓励你向虚拟助手寻求帮助。当你提出更难的问题时,可能会发现答案第一次并不正确,这是一个练习编写良好提示并进行有效跟进的机会。 + +你可以考虑的一种策略是将一个大问题拆解成可以通过简单函数解决的小问题。让虚拟助手编写这些函数并测试它们。然后,一旦它们工作正常,再请求解决原始问题。 + +对于下面的一些练习,我会建议使用哪些数据结构和算法。你可能会发现这些建议在解决问题时有用,但它们也是传递给虚拟助手的良好提示。 + +### 11.11.2\. 练习 + +在本章中,我提到过元组可以作为字典中的键,因为它们是可哈希的,而它们之所以可哈希,是因为它们是不可变的。但这并不总是正确的。 + +如果元组包含可变值,例如列表或字典,则该元组不再是可哈希的,因为它包含了不可哈希的元素。举个例子,下面是一个包含两个整数列表的元组。 + +```py +list0 = [1, 2, 3] +list1 = [4, 5] + +t = (list0, list1) +t +``` + +```py +([1, 2, 3], [4, 5]) +``` + +编写一行代码,将值`6`附加到`t`中第二个列表的末尾。如果你显示`t`,结果应为`([1, 2, 3], [4, 5, 6])`。 + +尝试创建一个将`t`映射到字符串的字典,并确认你会遇到`TypeError`。 + +```py +d = {t: 'this tuple contains two lists'} +``` + +```py +--------------------------------------------------------------------------- +TypeError Traceback (most recent call last) +Cell In[77], line 1 +----> 1 d = {t: 'this tuple contains two lists'} + d = {1: 'a', 2: 'b', 3: 'c'} + t = ([1, 2, 3], [4, 5, 6]) + +TypeError: unhashable type: 'list' +``` + +更多关于此主题的内容,可以向虚拟助手询问:“Python 元组总是可哈希的吗?” + +### 11.11.3\. 练习 + +在本章中,我们创建了一个字典,将每个字母映射到它在字母表中的索引。 + +```py +letters = 'abcdefghijklmnopqrstuvwxyz' +numbers = range(len(letters)) +letter_map = dict(zip(letters, numbers)) +``` + +例如,`'a'`的索引是`0`。 + +```py +letter_map['a'] +``` + +```py +0 +``` + +要朝另一个方向移动,我们可以使用列表索引。例如,索引`1`处的字母是`'b'`。 + +```py +letters[1] +``` + +```py +'b' +``` + +我们可以使用`letter_map`和`letters`来使用凯撒密码对单词进行编码和解码。 + +凯撒密码是一种弱加密形式,它通过将每个字母按固定的位移数移动来加密,如果有需要,可以绕回字母表的开头。例如,`'a'`移动 2 位是`'c'`,而`'z'`移动 1 位是`'a'`。 + +编写一个名为`shift_word`的函数,接受一个字符串和一个整数作为参数,并返回一个新的字符串,其中的字母按给定的位移数移动。 + +为了测试你的函数,确认“cheer”移动 7 个位置后是“jolly”,而“melon”移动 16 个位置后是“cubed”。 + +提示:使用模运算符将字母从`'z'`回绕到`'a'`。循环遍历单词中的字母,移动每个字母,并将结果附加到字母列表中。然后使用`join`将字母连接成一个字符串。 + +### 11.11.4\. 练习 + +编写一个名为`most_frequent_letters`的函数,该函数接受一个字符串并按频率递减顺序打印字母。 + +要按递减顺序获取项目,你可以使用`reversed`与`sorted`一起,或者你可以将`reverse=True`作为关键字参数传递给`sorted`。 + +### 11.11.5\. 练习 + +在之前的练习中,我们通过对两个单词的字母进行排序并检查排序后的字母是否相同,来判断这两个字符串是否是字谜词。现在让我们让这个问题更具挑战性。 + +我们将编写一个程序,该程序接受一个单词列表并打印出所有字谜词组。以下是输出可能的示例: + +```py +['deltas', 'desalt', 'lasted', 'salted', 'slated', 'staled'] +['retainers', 'ternaries'] +['generating', 'greatening'] +['resmelts', 'smelters', 'termless'] +``` + +提示:对于单词列表中的每个单词,先将字母排序,再将其连接回一个字符串。创建一个字典,将这个排序后的字符串映射到与之为字谜词的单词列表。 + +### 11.11.6\. 练习 + +编写一个名为`word_distance`的函数,该函数接受两个相同长度的单词,并返回两个单词在多少个位置上有所不同。 + +提示:使用`zip`函数来遍历单词中字母的对应位置。 + +### 11.11.7\. 练习 + +“元音交换”(Metathesis)是指单词中字母的交换。如果你可以通过交换两个字母将一个单词转换成另一个单词,那么这两个单词就是“元音交换对”,例如`converse`和`conserve`。编写一个程序,找出单词列表中的所有元音交换对。 + +提示:互换对中的单词必须是彼此的字谜词。 + +致谢:此练习的灵感来源于[`puzzlers.org`](http://puzzlers.org)上的一个示例。 + +[Think Python: 第三版](https://allendowney.github.io/ThinkPython/index.html) + +版权 2024 [Allen B. Downey](https://allendowney.com) + +代码许可:[MIT 许可证](https://mit-license.org/) + +文本许可:[知识共享署名-非商业性使用-相同方式共享 4.0 国际](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_14.md b/translations/zh/tnkpy3e_14.md new file mode 100644 index 0000000..ef62ac9 --- /dev/null +++ b/translations/zh/tnkpy3e_14.md @@ -0,0 +1,917 @@ +# 12\. 文本分析与生成 + +> 原文:[`allendowney.github.io/ThinkPython/chap12.html`](https://allendowney.github.io/ThinkPython/chap12.html) + +此时我们已经涵盖了 Python 的核心数据结构——列表、字典和元组——以及一些使用它们的算法。在本章中,我们将利用它们来探索文本分析和马尔科夫生成: + ++ 文本分析是一种描述文档中单词之间统计关系的方法,比如一个单词后面跟着另一个单词的概率,以及 + ++ 马尔科夫生成是一种使用与原始文本相似的单词和短语生成新文本的方法。 + +这些算法类似于大型语言模型(LLM)的部分内容,LLM 是聊天机器人的关键组成部分。 + +我们将从计算每个单词在书中出现的次数开始。然后我们将查看单词对,并列出每个单词后面可以跟随的单词。我们将制作一个简单版本的马尔科夫生成器,作为练习,你将有机会制作一个更通用的版本。 + +## 12.1\. 唯一单词 + +作为文本分析的第一步,让我们阅读一本书——罗伯特·路易斯·史蒂文森的《化身博士》——并统计唯一单词的数量。下载这本书的说明可以在本章的笔记本中找到。 + +```py +filename = 'dr_jekyll.txt' +``` + +我们将使用`for`循环从文件中读取行,并使用`split`将行分割成单词。然后,为了跟踪唯一单词,我们将每个单词作为字典中的一个键进行存储。 + +```py +unique_words = {} +for line in open(filename): + seq = line.split() + for word in seq: + unique_words[word] = 1 + +len(unique_words) +``` + +```py +6040 +``` + +字典的长度是唯一单词的数量——按照这种计算方式大约是`6000`。但如果我们检查它们,会发现有些并不是有效的单词。 + +例如,让我们看看`unique_words`中最长的单词。我们可以使用`sorted`来排序单词,将`len`函数作为关键字参数传入,以便按单词长度排序。 + +```py +sorted(unique_words, key=len)[-5:] +``` + +```py +['chocolate-coloured', + 'superiors—behold!”', + 'coolness—frightened', + 'gentleman—something', + 'pocket-handkerchief.'] +``` + +切片索引`[-5:]`选择排序后列表中的最后`5`个元素,即最长的单词。 + +这个列表包括一些合法的长单词,比如“circumscription”,以及一些带连字符的单词,比如“chocolate-coloured”。但一些最长的“单词”实际上是由连字符分隔的两个单词。而其他单词则包含像句号、感叹号和引号等标点符号。 + +所以,在我们继续之前,让我们处理一下连字符和其他标点符号。 + +## 12.2\. 标点符号 + +为了识别文本中的单词,我们需要解决两个问题: + ++ 当行中出现连字符时,我们应该将其替换为空格——然后当我们使用`split`时,单词就会被分开。 + ++ 分割单词后,我们可以使用`strip`来移除标点符号。 + +为了处理第一个问题,我们可以使用以下函数,它接受一个字符串,将连字符替换为空格,分割字符串,并返回结果列表。 + +```py +def split_line(line): + return line.replace('—', ' ').split() +``` + +注意,`split_line`只会替换连字符,而不会替换破折号。这里有一个例子。 + +```py +split_line('coolness—frightened') +``` + +```py +['coolness', 'frightened'] +``` + +现在,为了去除每个单词开头和结尾的标点符号,我们可以使用 `strip`,但是我们需要一个标点符号的字符列表。 + +Python 字符串中的字符使用 Unicode,这是一个国际标准,用于表示几乎所有字母表中的字母、数字、符号、标点符号等。`unicodedata` 模块提供了一个 `category` 函数,我们可以用它来判断字符是否为标点符号。给定一个字母,它会返回一个字符串,指示该字母属于哪个类别。 + +```py +import unicodedata + +unicodedata.category('A') +``` + +```py +'Lu' +``` + +字符 `'A'` 的类别字符串是 `'Lu'` —— `'L'` 表示它是一个字母,`'u'` 表示它是大写字母。 + +字符 `'.'` 的类别字符串是 `'Po'` —— `'P'` 表示它是标点符号,`'o'` 表示它的子类别是“其他”。 + +```py +unicodedata.category('.') +``` + +```py +'Po' +``` + +我们可以通过检查类别以 `'P'` 开头的字符,来找出书中的标点符号。下面的循环将唯一的标点符号存储在字典中。 + +```py +punc_marks = {} +for line in open(filename): + for char in line: + category = unicodedata.category(char) + if category.startswith('P'): + punc_marks[char] = 1 +``` + +为了制作一个标点符号的列表,我们可以将字典的键连接成一个字符串。 + +```py +punctuation = ''.join(punc_marks) +print(punctuation) +``` + +```py +.’;,-“”:?—‘!()_ +``` + +现在我们知道书中哪些字符是标点符号,我们可以编写一个函数,接受一个单词,去除开头和结尾的标点符号,并将其转换为小写。 + +```py +def clean_word(word): + return word.strip(punctuation).lower() +``` + +这是一个示例。 + +```py +clean_word('“Behold!”') +``` + +```py +'behold' +``` + +因为 `strip` 会删除字符串开头和结尾的字符,所以它不会影响带有连字符的单词。 + +```py +clean_word('pocket-handkerchief') +``` + +```py +'pocket-handkerchief' +``` + +现在,这是一个使用 `split_line` 和 `clean_word` 来识别书中唯一单词的循环。 + +```py +unique_words2 = {} +for line in open(filename): + for word in split_line(line): + word = clean_word(word) + unique_words2[word] = 1 + +len(unique_words2) +``` + +```py +4005 +``` + +根据对单词定义的严格标准,约有 4000 个唯一单词。我们可以确认最长单词的列表已经清理干净。 + +```py +sorted(unique_words2, key=len)[-5:] +``` + +```py +['circumscription', + 'unimpressionable', + 'fellow-creatures', + 'chocolate-coloured', + 'pocket-handkerchief'] +``` + +现在让我们看看每个单词的使用频率。 + +## 12.3\. 单词频率 + +以下循环计算每个唯一单词的频率。 + +```py +word_counter = {} +for line in open(filename): + for word in split_line(line): + word = clean_word(word) + if word not in word_counter: + word_counter[word] = 1 + else: + word_counter[word] += 1 +``` + +每当我们第一次遇到一个单词时,我们将其频率初始化为 `1`。如果之后再遇到相同的单词,我们就将其频率加一。 + +为了查看哪些单词最常出现,我们可以使用 `items` 从 `word_counter` 获取键值对,并按对中的第二个元素(即频率)进行排序。首先,我们将定义一个函数来选择第二个元素。 + +```py +def second_element(t): + return t[1] +``` + +现在我们可以使用 `sorted` 和两个关键字参数: + ++ `key=second_element` 表示项目将根据单词的频率进行排序。 + ++ `reverse=True` 表示项目将按反向顺序排序,最频繁的单词排在最前面。 + +```py +items = sorted(word_counter.items(), key=second_element, reverse=True) +``` + +这里是五个最常见的单词。 + +```py +for word, freq in items[:5]: + print(freq, word, sep='\t') +``` + +```py +1614 the +972 and +941 of +640 to +640 i +``` + +在接下来的部分,我们将把这个循环封装在一个函数中。我们还将用它来演示一个新功能——可选参数。 + +## 12.4\. 可选参数 + +我们已经使用了带有可选参数的内置函数。例如,`round` 有一个名为 `ndigits` 的可选参数,用于指示保留多少位小数。 + +```py +round(3.141592653589793, ndigits=3) +``` + +```py +3.142 +``` + +但这不仅仅是内置函数——我们也可以编写带有可选参数的函数。例如,下面的函数接受两个参数,`word_counter` 和 `num`。 + +```py +def print_most_common(word_counter, num=5): + items = sorted(word_counter.items(), key=second_element, reverse=True) + + for word, freq in items[:num]: + print(freq, word, sep='\t') +``` + +第二个参数看起来像一个赋值语句,但其实它不是——它是一个可选参数。 + +如果你用一个参数调用这个函数,`num`将获得**默认值**,即`5`。 + +```py +print_most_common(word_counter) +``` + +```py +1614 the +972 and +941 of +640 to +640 i +``` + +如果你用两个参数调用这个函数,第二个参数将被赋值给`num`,而不是默认值。 + +```py +print_most_common(word_counter, 3) +``` + +```py +1614 the +972 and +941 of +``` + +在这种情况下,我们可以说可选参数**覆盖**了默认值。 + +如果一个函数既有必需参数又有可选参数,所有必需的参数必须排在前面,后面跟着可选参数。 + +## 12.5\. 字典减法 + +假设我们想进行拼写检查——也就是说,找出可能拼写错误的单词列表。做这个的方法之一是找出书中那些不在有效单词列表中的单词。在之前的章节中,我们使用了一个在类似拼字游戏(如拼字游戏)中被认为有效的单词列表。现在我们将使用这个列表来进行罗伯特·路易斯·史蒂文森的拼写检查。 + +我们可以将这个问题看作集合减法——也就是说,我们想找出一个集合(书中的单词)中不在另一个集合(列表中的单词)中的所有单词。 + +正如我们之前做过的,我们可以读取`words.txt`的内容,并将其分割成一个字符串列表。 + +```py +word_list = open('words.txt').read().split() +``` + +然后我们将把单词作为键存储在字典中,以便我们可以使用`in`运算符快速检查一个单词是否有效。 + +```py +valid_words = {} +for word in word_list: + valid_words[word] = 1 +``` + +现在,为了识别书中出现但不在单词列表中的单词,我们将使用`subtract`,它接受两个字典作为参数,并返回一个新的字典,其中包含第一个字典中不在第二个字典中的所有键。 + +```py +def subtract(d1, d2): + res = {} + for key in d1: + if key not in d2: + res[key] = d1[key] + return res +``` + +下面是我们如何使用它的方法。 + +```py +diff = subtract(word_counter, valid_words) +``` + +要获取可能拼写错误的单词样本,我们可以打印出`diff`中最常见的单词。 + +```py +print_most_common(diff) +``` + +```py +640 i +628 a +128 utterson +124 mr +98 hyde +``` + +最常见的“拼写错误”单词大多是人名和一些单字母的单词(乌特森先生是杰基尔博士的朋友和律师)。 + +如果我们选择那些只出现一次的单词,它们更有可能是拼写错误。我们可以通过遍历项目,并列出频率为`1`的单词来做到这一点。 + +```py +singletons = [] +for word, freq in diff.items(): + if freq == 1: + singletons.append(word) +``` + +这是列表中的最后几个元素。 + +```py +singletons[-5:] +``` + +```py +['gesticulated', 'abjection', 'circumscription', 'reindue', 'fearstruck'] +``` + +它们中的大多数是有效的单词,但不在单词列表中。不过,`'reindue'`似乎是`'reinduce'`的拼写错误,所以至少我们发现了一个真正的错误。 + +## 12.6\. 随机数 + +作为迈向马尔科夫文本生成的一步,接下来我们将从`word_counter`中选择一个随机的单词序列。但首先,让我们谈谈随机性。 + +给定相同的输入,大多数计算机程序是**确定性的**,这意味着它们每次生成相同的输出。确定性通常是好事,因为我们希望相同的计算产生相同的结果。然而,对于某些应用程序,我们希望计算机能够不可预测。游戏就是一个例子,但还有更多。 + +让程序真正做到非确定性是很困难的,但有一些方法可以伪装成非确定性。一个方法是使用生成**伪随机**数的算法。伪随机数并不是真正的随机数,因为它们是通过确定性计算生成的,但仅凭查看这些数字,几乎无法将它们与真正的随机数区分开。 + +`random` 模块提供了生成伪随机数的函数——我将在这里简单地称其为“随机数”。我们可以这样导入它。 + +```py +import random +``` + +`random` 模块提供了一个名为 `choice` 的函数,可以从列表中随机选择一个元素,每个元素被选中的概率相同。 + +```py +t = [1, 2, 3] +random.choice(t) +``` + +```py +1 +``` + +如果你再次调用该函数,你可能会得到相同的元素,或者得到不同的元素。 + +```py +random.choice(t) +``` + +```py +2 +``` + +从长远来看,我们希望每个元素出现的次数大致相同。 + +如果你用字典调用 `choice`,你会得到一个 `KeyError`。 + +```py +random.choice(word_counter) +``` + +```py +KeyError: 422 +``` + +要选择一个随机键,你必须先将键放入列表中,然后调用 `choice`。 + +```py +words = list(word_counter) +random.choice(words) +``` + +```py +'posture' +``` + +如果我们生成一个随机的单词序列,它没有太大的意义。 + +```py +for i in range(6): + word = random.choice(words) + print(word, end=' ') +``` + +```py +ill-contained written apocryphal nor busy spoke +``` + +问题的一部分是我们没有考虑到某些单词比其他单词更常见。如果我们选择具有不同“权重”的单词,结果会更好,这样有些单词会比其他单词更频繁地被选中。 + +如果我们使用来自 `word_counter` 的值作为权重,每个单词的选择概率将取决于它的频率。 + +```py +weights = word_counter.values() +``` + +`random` 模块还提供了另一个名为 `choices` 的函数,接受权重作为一个可选参数。 + +```py +random.choices(words, weights=weights) +``` + +```py +['than'] +``` + +它还接受另一个可选参数 `k`,该参数指定要选择的单词数。 + +```py +random_words = random.choices(words, weights=weights, k=6) +random_words +``` + +```py +['reach', 'streets', 'edward', 'a', 'said', 'to'] +``` + +结果是一个字符串的列表,我们可以将其连接成更像句子的东西。 + +```py +' '.join(random_words) +``` + +```py +'reach streets edward a said to' +``` + +如果你从书中随机选择单词,你可以感知到词汇量,但一系列随机单词通常没有意义,因为连续单词之间没有关系。例如,在一个真实的句子中,你期望像“the”这样的冠词后面跟着形容词或名词,而可能不是动词或副词。所以,下一步是查看单词之间的这些关系。 + +## 12.7\. 二元组 + +我们将不再逐个单词查看,而是查看由两个单词组成的序列,这叫做**二元组**。由三个单词组成的序列叫做**三元组**,由若干个单词组成的序列叫做**n-元组**。 + +我们来写一个程序,找出书中的所有二元组以及每个二元组出现的次数。为了存储结果,我们将使用一个字典,其中 + ++ 键是表示二元组的大写字母字符串的元组, + ++ 这些值是表示频率的整数。 + +我们称之为 `bigram_counter`。 + +```py +bigram_counter = {} +``` + +以下函数接受两个字符串组成的列表作为参数。首先,它将这两个字符串组成一个元组,可以作为字典中的键。然后,如果该键不存在,它会将其添加到 `bigram_counter` 中;如果已经存在,它会增加该键的频率。 + +```py +def count_bigram(bigram): + key = tuple(bigram) + if key not in bigram_counter: + bigram_counter[key] = 1 + else: + bigram_counter[key] += 1 +``` + +在阅读本书的过程中,我们必须跟踪每一对连续的单词。因此,如果我们看到“man is not truly one”这一序列,我们将添加“大词组”:“man is”,“is not”,“not truly”等等。 + +为了跟踪这些大词组,我们将使用一个名为`window`的列表,因为它就像一本书的窗口,一次只显示两个单词。最初,`window`是空的。 + +```py +window = [] +``` + +我们将使用以下函数逐个处理单词。 + +```py +def process_word(word): + window.append(word) + + if len(window) == 2: + count_bigram(window) + window.pop(0) +``` + +当第一次调用此函数时,它会将给定的单词附加到`window`中。由于窗口中只有一个单词,我们还没有形成一个大词组,因此函数结束。 + +第二次调用时——以及以后每次调用——它会将第二个单词添加到`window`中。由于窗口中有两个单词,它会调用`count_bigram`来跟踪每个大词组出现的次数。然后,它使用`pop`移除窗口中的第一个单词。 + +以下程序循环遍历书中的单词并逐一处理它们。 + +```py +for line in open(filename): + for word in split_line(line): + word = clean_word(word) + process_word(word) +``` + +结果是一个字典,将每个大词组映射到它出现的次数。我们可以使用`print_most_common`来查看最常见的大词组。 + +```py +print_most_common(bigram_counter) +``` + +```py +178 ('of', 'the') +139 ('in', 'the') +94 ('it', 'was') +80 ('and', 'the') +73 ('to', 'the') +``` + +看着这些结果,我们可以感知哪些单词对最可能一起出现。我们还可以利用这些结果生成随机文本,像这样。 + +```py +bigrams = list(bigram_counter) +weights = bigram_counter.values() +random_bigrams = random.choices(bigrams, weights=weights, k=6) +``` + +`bigrams`是书中出现的大词组列表。`weights`是它们的频率列表,因此`random_bigrams`是一个样本,其中大词组被选中的概率与它的频率成正比。 + +这是结果。 + +```py +for pair in random_bigrams: + print(' '.join(pair), end=' ') +``` + +```py +to suggest this preface to detain fact is above all the laboratory +``` + +这种生成文本的方式比选择随机单词要好,但仍然没有太多意义。 + +## 12.8. 马尔科夫分析 + +我们可以通过马尔科夫链文本分析做得更好,它为文本中的每个单词计算接下来会出现的单词列表。作为示例,我们将分析《蒙提·派森》歌曲 *Eric, the Half a Bee* 的歌词: + +```py +song = """ +Half a bee, philosophically, +Must, ipso facto, half not be. +But half the bee has got to be +Vis a vis, its entity. D'you see? +""" +``` + +为了存储结果,我们将使用一个字典,它将每个单词映射到跟随它的单词列表。 + +```py +successor_map = {} +``` + +举个例子,让我们从歌曲的前两个单词开始。 + +```py +first = 'half' +second = 'a' +``` + +如果第一个单词不在`successor_map`中,我们必须添加一个新项,将第一个单词映射到包含第二个单词的列表。 + +```py +successor_map[first] = [second] +successor_map +``` + +```py +{'half': ['a']} +``` + +如果第一个单词已经在字典中,我们可以查找它,获取我们到目前为止看到的后继单词列表,并附加新的单词。 + +```py +first = 'half' +second = 'not' + +successor_map[first].append(second) +successor_map +``` + +```py +{'half': ['a', 'not']} +``` + +以下函数封装了这些步骤。 + +```py +def add_bigram(bigram): + first, second = bigram + + if first not in successor_map: + successor_map[first] = [second] + else: + successor_map[first].append(second) +``` + +如果相同的大词组出现多次,第二个单词会被多次添加到列表中。通过这种方式,`successor_map`会跟踪每个后继单词出现的次数。 + +正如我们在前一节中所做的,我们将使用一个名为`window`的列表来存储连续单词对。我们将使用以下函数逐个处理单词。 + +```py +def process_word_bigram(word): + window.append(word) + + if len(window) == 2: + add_bigram(window) + window.pop(0) +``` + +这是我们如何用它来处理歌曲中的单词。 + +```py +successor_map = {} +window = [] + +for word in song.split(): + word = clean_word(word) + process_word_bigram(word) +``` + +这是结果。 + +```py +successor_map +``` + +```py +{'half': ['a', 'not', 'the'], + 'a': ['bee', 'vis'], + 'bee': ['philosophically', 'has'], + 'philosophically': ['must'], + 'must': ['ipso'], + 'ipso': ['facto'], + 'facto': ['half'], + 'not': ['be'], + 'be': ['but', 'vis'], + 'but': ['half'], + 'the': ['bee'], + 'has': ['got'], + 'got': ['to'], + 'to': ['be'], + 'vis': ['a', 'its'], + 'its': ['entity'], + 'entity': ["d'you"], + "d'you": ['see']} +``` + +单词`'half'`可以跟随`'a'`、`'not'`或`'the'`。单词`'a'`可以跟随`'bee'`或`'vis'`。大多数其他单词只出现一次,因此它们只跟随一个单词。 + +现在,让我们分析这本书。 + +```py +successor_map = {} +window = [] + +for line in open(filename): + for word in split_line(line): + word = clean_word(word) + process_word_bigram(word) +``` + +我们可以查找任何单词,并找到可以跟随它的单词。 + +```py +successor_map['going'] +``` + +```py +['east', 'in', 'to', 'to', 'up', 'to', 'of'] +``` + +在这个后继列表中,请注意单词`'to'`出现了三次,而其他后继只出现了一次。 + +## 12.9\. 生成文本 + +我们可以使用前一部分的结果,生成与原文中连续单词之间关系相同的新文本。它是如何工作的: + ++ 从文本中出现的任何一个单词开始,我们查找它的可能后继,并随机选择一个。 + ++ 然后,使用选中的单词,我们查找它的可能后继,并随机选择一个。 + +我们可以重复这个过程,生成我们想要的任意多个单词。举个例子,让我们从单词`'although'`开始。以下是可以跟随它的单词。 + +```py +word = 'although' +successors = successor_map[word] +successors +``` + +```py +['i', 'a', 'it', 'the', 'we', 'they', 'i'] +``` + +我们可以使用`choice`从列表中以相等的概率选择。 + +```py +word = random.choice(successors) +word +``` + +```py +'i' +``` + +如果同一个单词在列表中出现多次,它被选中的概率更大。 + +重复这些步骤,我们可以使用以下循环生成更长的序列。 + +```py +for i in range(10): + successors = successor_map[word] + word = random.choice(successors) + print(word, end=' ') +``` + +```py +continue to hesitate and swallowed the smile withered from that +``` + +结果听起来更像一个真实的句子,但它仍然没有太大意义。 + +使用多个单词作为`successor_map`中的键,我们可以做得更好。例如,我们可以创建一个字典,将每个二元组(bigram)或三元组(trigram)映射到后续单词的列表。作为一个练习,你将有机会实现这个分析,并查看结果是什么样的。 + +## 12.10\. 调试 + +到这个阶段,我们正在编写更复杂的程序,你可能会发现你花更多时间进行调试。如果你在调试一个难题时卡住了,下面是一些可以尝试的办法: + ++ 阅读:检查你的代码,自己读一遍,确认它是否表达了你想表达的意思。 + ++ 运行:通过进行更改并运行不同的版本进行实验。通常,如果你在程序中的正确位置显示正确的内容,问题会变得明显,但有时你需要构建一些支架。 + ++ 沉思:花点时间思考一下!这是哪种错误:语法错误、运行时错误,还是语义错误?你能从错误信息或者程序的输出中获取哪些信息?是什么样的错误可能导致你看到的问题?在问题出现之前,你最后修改了什么? + ++ 橡皮鸭调试:如果你将问题解释给别人听,你有时会在还没问完问题之前就找到答案。通常你不需要另一个人;你可以只和一只橡皮鸭交谈。这就是著名的策略——**橡皮鸭调试**的来源。我不是在编造这个——请看 [`en.wikipedia.org/wiki/Rubber_duck_debugging`](https://en.wikipedia.org/wiki/Rubber_duck_debugging)。 + ++ 后退:在某些时候,最好的做法是后退——撤销最近的更改——直到你回到一个正常工作的程序。然后你可以重新开始构建。 + ++ 休息:如果你给大脑休息,有时候它会自己找到问题所在。 + +初学者程序员有时会在某个活动上卡住,忘记其他的活动。每个活动都有它自己的失败模式。 + +例如,读取你的代码如果问题是拼写错误的话是有效的,但如果问题是概念性误解,则无效。如果你不理解你的程序是做什么的,即使你读它 100 遍也看不出错误,因为错误在你的脑海里。 + +运行实验是有效的,特别是当你运行小的、简单的测试时。但如果你没有思考或阅读代码就去实验,可能会花费很长时间才能搞清楚发生了什么。 + +你必须抽时间思考。调试就像是实验科学。你应该对问题至少有一个假设。如果有两个或更多的可能性,尝试想出一个可以排除其中一个的测试。 + +但即使是最好的调试技术,也会因为错误太多,或者你试图修复的代码过于庞大复杂而失败。有时候最好的选择是退一步,简化程序,直到恢复到一个能正常工作的版本。 + +初学者程序员通常不愿意后退,因为他们无法忍受删除一行代码(即使它是错的)。如果这样能让你感觉好一点,可以在开始简化程序之前,把你的程序复制到另一个文件中。然后你可以逐个复制回来。 + +找到一个棘手的 bug 需要阅读、运行、沉思、退步,有时候还需要休息。如果你在某个活动中遇到困难,尝试其他的方法。 + +## 12.11\. 词汇表 + +**默认值(default value):** 如果没有提供参数,将赋给参数的值。 + +**覆盖(override):** 用一个参数替换默认值。 + +**确定性(deterministic):** 一个确定性的程序每次运行时,只要输入相同,就会做相同的事情。 + +**伪随机(pseudorandom):** 伪随机数列看起来像是随机的,但它是由一个确定性的程序生成的。 + +**二元组(bigram):** 一个包含两个元素的序列,通常是单词。 + +**三元组(trigram):** 一个包含三个元素的序列。 + +**n 元组(n-gram):** 一个包含不确定数量元素的序列。 + +**橡皮鸭调试(rubber duck debugging):** 通过大声向一个无生命物体解释问题来进行调试的一种方法。 + +## 12.12\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +### 12.12.1\. 向虚拟助手询问 + +在`add_bigram`中,`if`语句根据字典中是否已存在该键,来创建一个新的列表或将元素添加到现有列表中。 + +```py +def add_bigram(bigram): + first, second = bigram + + if first not in successor_map: + successor_map[first] = [second] + else: + successor_map[first].append(second) +``` + +字典提供了一种叫做`setdefault`的方法,我们可以用它更简洁地做同样的事情。你可以向虚拟助手询问它是如何工作的,或者把`add_word`复制到虚拟助手中并问:“你能用`setdefault`重写这个吗?” + +在本章中,我们实现了马尔科夫链文本分析和生成。如果你感兴趣,可以向虚拟助手询问更多关于该主题的信息。你可能学到的一件事是,虚拟助手使用的算法在许多方面是相似的——但在重要方面也有所不同。问一个虚拟助手:“像 GPT 这样的语言模型和马尔科夫链文本分析有什么区别?” + +### 12.12.2\. 练习 + +编写一个函数,计算每个三元组(由三个单词组成的序列)出现的次数。如果你使用《*化身博士*》的文本来测试你的函数,你应该会发现最常见的三元组是“said the lawyer”。 + +提示:编写一个名为`count_trigram`的函数,它类似于`count_bigram`。然后编写一个名为`process_word_trigram`的函数,它类似于`process_word_bigram`。 + +### 12.12.3\. 练习 + +现在让我们通过从每个二元组映射到可能的后继词列表来实现马尔科夫链文本分析。 + +从`add_bigram`开始,编写一个名为`add_trigram`的函数,该函数接收一个包含三个单词的列表,并使用前两个单词作为键,第三个单词作为可能的后继词,在`successor_map`中添加或更新一个条目。 + +这是一个调用`add_trigram`的`process_word_trigram`版本。 + +```py +def process_word_trigram(word): + window.append(word) + + if len(window) == 3: + add_trigram(window) + window.pop(0) +``` + +你可以使用以下循环来测试你的函数,使用来自书本的单词。 + +```py +successor_map = {} +window = [] + +for line in open(filename): + for word in split_line(line): + word = clean_word(word) + process_word_trigram(word) +``` + +在下一个练习中,你将使用这些结果生成新的随机文本。 + +### 12.12.4\. 练习 + +对于这个练习,我们假设`successor_map`是一个字典,它将每个二元组映射到后继词的列表。 + +为了生成随机文本,我们将从`successor_map`中随机选择一个键。 + +```py +successors = list(successor_map) +bigram = random.choice(successors) +bigram +``` + +```py +('doubted', 'if') +``` + +现在编写一个循环,按照这些步骤生成更多的 50 个单词: + +1. 在`successor_map`中查找可以跟随`bigram`的单词列表。 + +1. 随机选择其中一个并打印出来。 + +1. 对于下一个迭代,创建一个新的二元组,该二元组包含`bigram`中的第二个单词和所选的后继词。 + +例如,如果我们从二元组`('doubted', 'if')`开始,并选择`'from'`作为其后继词,则下一个二元组是`('if', 'from')`。 + +如果一切正常,你应该会发现生成的文本在风格上与原文相似,一些短语是有意义的,但文本可能会从一个话题跳到另一个话题。 + +作为附加练习,修改你对最后两个练习的解决方案,使用三元组作为`successor_map`中的键,看看它对结果产生了什么影响。 + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可证:[MIT 许可证](https://mit-license.org/) + +文本许可证:[知识共享署名-非商业性使用-相同方式共享 4.0 国际](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_15.md b/translations/zh/tnkpy3e_15.md new file mode 100644 index 0000000..303e439 --- /dev/null +++ b/translations/zh/tnkpy3e_15.md @@ -0,0 +1,823 @@ +# 13\. 文件与数据库 + +> 原文:[`allendowney.github.io/ThinkPython/chap13.html`](https://allendowney.github.io/ThinkPython/chap13.html) + +我们迄今为止看到的大多数程序都是**临时的**,因为它们运行时间很短,生成输出,但当它们结束时,它们的数据会消失。每次运行临时程序时,它都会从一个干净的状态开始。 + +其他程序是**持久的**:它们运行时间很长(或者一直运行);它们将至少一部分数据保存在长期存储中;如果它们关闭并重新启动,它们会从上次停止的地方继续。 + +程序保持数据的一种简单方式是通过读取和写入文本文件。一个更通用的替代方案是将数据存储在数据库中。数据库是专门的文件,比文本文件更高效地读取和写入,并且提供了额外的功能。 + +在本章中,我们将编写读取和写入文本文件及数据库的程序,并且作为一个练习,你将编写一个程序,搜索照片集中的重复文件。但在你可以操作文件之前,首先要找到它,因此我们将从文件名、路径和目录开始。 + +## 13.1\. 文件名和路径 + +文件被组织成**目录**,也叫做“文件夹”。每个正在运行的程序都有一个**当前工作目录**,这是大多数操作的默认目录。例如,当你打开一个文件时,Python 会在当前工作目录中查找它。 + +`os`模块提供了用于操作文件和目录的函数(`os`代表“操作系统”)。它提供了一个名为`getcwd`的函数,用于获取当前工作目录的名称。 + +```py +import os + +os.getcwd() +``` + +```py +'/home/dinsdale' +``` + +本例中的结果是一个名为`dinsdale`的用户的主目录。像`'/home/dinsdale'`这样的字符串,它标识了一个文件或目录,称为**路径**。 + +像`'memo.txt'`这样的简单文件名也被视为路径,但它是一个**相对路径**,因为它指定了相对于当前目录的文件名。在本例中,当前目录是`/home/dinsdale`,所以`'memo.txt'`等同于完整路径`'/home/dinsdale/memo.txt'`。 + +以`/`开头的路径不依赖于当前目录——它被称为**绝对路径**。要找到文件的绝对路径,可以使用`abspath`。 + +```py +os.path.abspath('memo.txt') +``` + +```py +'/home/dinsdale/memo.txt' +``` + +`os`模块还提供了其他用于操作文件名和路径的函数。`listdir`返回给定目录的内容列表,包括文件和其他目录。下面是列出名为`photos`目录内容的示例。 + +```py +os.listdir('photos') +``` + +```py +['digests.dat', + 'digests.dir', + 'notes.txt', + 'new_notes.txt', + 'mar-2023', + 'digests.bak', + 'jan-2023', + 'feb-2023'] +``` + +这个目录包含一个名为`notes.txt`的文本文件和三个目录。目录中包含 JPEG 格式的图像文件。 + +```py +os.listdir('photos/jan-2023') +``` + +```py +['photo3.jpg', 'photo2.jpg', 'photo1.jpg'] +``` + +要检查文件或目录是否存在,可以使用`os.path.exists`。 + +```py +os.path.exists('photos') +``` + +```py +True +``` + +```py +os.path.exists('photos/apr-2023') +``` + +```py +False +``` + +要检查路径是否指向文件或目录,我们可以使用`isdir`,它返回`True`如果路径指向一个目录。 + +```py +os.path.isdir('photos') +``` + +```py +True +``` + +还有`isfile`,如果路径指向一个文件,它返回`True`。 + +```py +os.path.isfile('photos/notes.txt') +``` + +```py +True +``` + +处理路径的一个挑战是,不同操作系统上的路径表示不同。在 macOS 和类似 Linux 的 UNIX 系统中,路径中的目录和文件名是由正斜杠`/`分隔的。Windows 使用反斜杠`\`。因此,如果你在 Windows 上运行这些示例,你会看到路径中的反斜杠,并且你需要将示例中的正斜杠替换为反斜杠。 + +或者,为了编写在两个系统上都能运行的代码,可以使用`os.path.join`,它将目录和文件名连接成一个路径,使用正斜杠或反斜杠,具体取决于你使用的操作系统。 + +```py +os.path.join('photos', 'jan-2023', 'photo1.jpg') +``` + +```py +'photos/jan-2023/photo1.jpg' +``` + +在本章稍后,我们将使用这些函数来搜索一组目录并找到所有图像文件。 + +## 13.2\. f-strings + +程序存储数据的一种方式是将其写入文本文件。例如,假设你是一个骆驼观察员,想要记录在一段观察期内看到的骆驼数量。假设在一年半的时间里,你已经观察到`23`只骆驼。你在骆驼观察本中的数据可能看起来是这样的。 + +```py +num_years = 1.5 +num_camels = 23 +``` + +要将这些数据写入文件,可以使用`write`方法,我们在第八章中见过。`write`的参数必须是一个字符串,因此如果我们想将其他值放入文件中,就必须将它们转换为字符串。最简单的方式是使用内置函数`str`。 + +这看起来是这样的: + +```py +writer = open('camel-spotting-book.txt', 'w') +writer.write(str(num_years)) +writer.write(str(num_camels)) +writer.close() +``` + +这有效,但`write`不会添加空格或换行,除非你明确地包含它。如果我们重新读取文件,会发现两个数字被连在一起。 + +```py +open('camel-spotting-book.txt').read() +``` + +```py +'1.523' +``` + +至少,我们应该在数字之间添加空格。顺便提一下,让我们添加一些说明文字。 + +要编写一个字符串和其他值的组合,可以使用**f-string**,它是一个在开头有字母`f`的字符串,并且包含一个或多个用大括号括起来的 Python 表达式。以下的 f-string 包含一个表达式,即一个变量名。 + +```py +f'I have spotted {num_camels} camels' +``` + +```py +'I have spotted 23 camels' +``` + +结果是一个字符串,其中的表达式已被求值并替换为结果。可以有多个表达式。 + +```py +f'In {num_years} years I have spotted {num_camels} camels' +``` + +```py +'In 1.5 years I have spotted 23 camels' +``` + +而且这些表达式可以包含运算符和函数调用。 + +```py +line = f'In {round(num_years * 12)} months I have spotted {num_camels} camels' +line +``` + +```py +'In 18 months I have spotted 23 camels' +``` + +所以我们可以像这样将数据写入文本文件。 + +```py +writer = open('camel-spotting-book.txt', 'w') +writer.write(f'Years of observation: {num_years}\n') +writer.write(f'Camels spotted: {num_camels}\n') +writer.close() +``` + +两个 f-string 都以序列`\n`结尾,这会添加一个换行符。 + +我们可以像这样读取文件: + +```py +data = open('camel-spotting-book.txt').read() +print(data) +``` + +```py +Years of observation: 1.5 +Camels spotted: 23 +``` + +在 f-string 中,大括号中的表达式会被转换为字符串,因此你可以包含列表、字典和其他类型。 + +```py +t = [1, 2, 3] +d = {'one': 1} +f'Here is a list {t} and a dictionary {d}' +``` + +```py +"Here is a list [1, 2, 3] and a dictionary {'one': 1}" +``` + +## 13.3\. YAML + +程序读取和写入文件的原因之一是存储**配置信息**,这是一种指定程序应该做什么以及如何做的数据信息。 + +例如,在一个搜索重复照片的程序中,我们可能有一个名为`config`的字典,它包含了要搜索的目录名称、另一个目录的名称(用于存储结果),以及识别图片文件所用的文件扩展名列表。 + +这可能看起来像这样: + +```py +config = { + 'photo_dir': 'photos', + 'data_dir': 'photo_info', + 'extensions': ['jpg', 'jpeg'], +} +``` + +为了将这些数据写入文本文件,我们可以像上一节那样使用 f-string。但使用一个名为`yaml`的模块会更方便,它专为处理这类事情而设计。 + +`yaml`模块提供了用于处理 YAML 文件的函数,YAML 文件是格式化为便于人类*和*程序阅读和写入的文本文件。 + +这里有一个示例,使用`dump`函数将`config`字典写入 YAML 文件。 + +```py +import yaml + +config_filename = 'config.yaml' +writer = open(config_filename, 'w') +yaml.dump(config, writer) +writer.close() +``` + +如果我们读取文件的内容,我们可以看到 YAML 格式的样子。 + +```py +readback = open(config_filename).read() +print(readback) +``` + +```py +data_dir: photo_info +extensions: +- jpg +- jpeg +photo_dir: photos +``` + +现在,我们可以使用`safe_load`来读取回 YAML 文件。 + +```py +reader = open(config_filename) +config_readback = yaml.safe_load(reader) +config_readback +``` + +```py +{'data_dir': 'photo_info', + 'extensions': ['jpg', 'jpeg'], + 'photo_dir': 'photos'} +``` + +结果是一个包含与原始字典相同信息的新字典,但它不是同一个字典。 + +```py +config is config_readback +``` + +```py +False +``` + +将字典之类的对象转换为字符串称为**序列化**。将字符串转换回对象称为**反序列化**。如果你先序列化再反序列化一个对象,结果应该与原始对象等效。 + +## 13.4\. Shelve + +到目前为止,我们一直在读取和写入文本文件——现在让我们来考虑数据库。**数据库**是一个用于存储数据的组织化文件。有些数据库像表格一样,包含行和列的信息。其他的则像字典一样,通过键映射到值,它们有时被称为**键值存储**。 + +`shelve`模块提供了创建和更新称为“shelf”的键值存储的功能。作为示例,我们将创建一个 shelf 来存储`photos`目录中图片的标题。我们将使用`config`字典来获取应该放置 shelf 的目录名称。 + +```py +config['data_dir'] +``` + +```py +'photo_info' +``` + +如果目录不存在,我们可以使用`os.makedirs`来创建这个目录。 + +```py +os.makedirs(config['data_dir'], exist_ok=True) +``` + +以及使用`os.path.join`来创建一个包含目录名称和 shelf 文件名称`captions`的路径。 + +```py +db_file = os.path.join(config['data_dir'], 'captions') +db_file +``` + +```py +'photo_info/captions' +``` + +现在我们可以使用`shelve.open`打开 shelf 文件。参数`c`表示如果文件不存在,则创建该文件。 + +```py +import shelve + +db = shelve.open(db_file, 'c') +db +``` + +```py + +``` + +返回值官方称为`DbfilenameShelf`对象,更通俗地称为 shelf 对象。 + +shelf 对象在许多方面像字典。例如,我们可以使用括号操作符添加一个条目,它是一个从键到值的映射。 + +```py +key = 'jan-2023/photo1.jpg' +db[key] = 'Cat nose' +``` + +在这个示例中,键是图像文件的路径,值是描述图像的字符串。 + +我们还使用括号操作符来查找一个键并获取对应的值。 + +```py +value = db[key] +value +``` + +```py +'Cat nose' +``` + +如果你对现有的键进行重新赋值,`shelve`会替换旧值。 + +```py +db[key] = 'Close up view of a cat nose' +db[key] +``` + +```py +'Close up view of a cat nose' +``` + +一些字典方法,如`keys`、`values`和`items`,也适用于 shelf 对象。 + +```py +list(db.keys()) +``` + +```py +['jan-2023/photo1.jpg'] +``` + +```py +list(db.values()) +``` + +```py +['Close up view of a cat nose'] +``` + +我们可以使用`in`操作符检查一个键是否出现在 shelf 中。 + +```py +key in db +``` + +```py +True +``` + +我们还可以使用`for`语句来遍历键。 + +```py +for key in db: + print(key, ':', db[key]) +``` + +```py +jan-2023/photo1.jpg : Close up view of a cat nose +``` + +和其他文件一样,使用完数据库后,应该关闭它。 + +```py +db.close() +``` + +现在,如果我们列出数据目录的内容,我们会看到两个文件。 + +```py +os.listdir(config['data_dir']) +``` + +```py +['captions.dir', 'captions.dat'] +``` + +`captions.dat`包含我们刚刚存储的数据。`captions.dir`包含有关数据库组织的信息,这使得访问更高效。后缀`dir`代表“目录”,但它与我们之前处理的包含文件的目录无关。 + +## 13.5\. 存储数据结构 + +在之前的例子中,架子中的键和值是字符串。但我们也可以使用架子来存储像列表和字典这样的数据结构。 + +作为例子,让我们重新回顾一下第十一章练习中的字谜例子。回想一下,我们创建了一个字典,它将字母的排序字符串映射到可以用这些字母拼写出来的单词列表。例如,键`'opst'`映射到列表`['opts', 'post', 'pots', 'spot', 'stop', 'tops']`。 + +我们将使用以下函数来排序一个单词中的字母。 + +```py +def sort_word(word): + return ''.join(sorted(word)) +``` + +这里有一个例子。 + +```py +word = 'pots' +key = sort_word(word) +key +``` + +```py +'opst' +``` + +现在让我们打开一个名为`anagram_map`的架子。参数`'n'`意味着我们应该始终创建一个新的空架子,即使已经存在一个。 + +```py +db = shelve.open('anagram_map', 'n') +``` + +现在我们可以像这样向架子中添加一个项目。 + +```py +db[key] = [word] +db[key] +``` + +```py +['pots'] +``` + +在这个条目中,键是一个字符串,值是一个字符串列表。 + +现在假设我们找到另一个包含相同字母的单词,比如`tops`。 + +```py +word = 'tops' +key = sort_word(word) +key +``` + +```py +'opst' +``` + +这个键与之前的例子相同,所以我们想将第二个单词附加到同一个字符串列表中。如果`db`是一个字典,下面就是我们如何做的。 + +```py +db[key].append(word) # INCORRECT +``` + +但是,如果我们运行它并查看架子中的键,它看起来没有被更新。 + +```py +db[key] +``` + +```py +['pots'] +``` + +这里是问题:当我们查找键时,我们得到的是一个字符串列表,但如果我们修改这个字符串列表,它并不会影响架子。如果我们想要更新架子,必须先读取旧值,更新它,然后将新值写回架子。 + +```py +anagram_list = db[key] +anagram_list.append(word) +db[key] = anagram_list +``` + +现在架子中的值已更新。 + +```py +db[key] +``` + +```py +['pots', 'tops'] +``` + +作为练习,你可以通过读取单词列表并将所有的字谜存储到一个架子中来完成这个例子。## 13.6\. 检查等效文件 + +现在让我们回到本章的目标:搜索包含相同数据的不同文件。检查的一种方法是读取两个文件的内容并进行比较。 + +如果文件包含图像,我们必须以`'rb'`模式打开它们,其中`'r'`表示我们想要读取内容,而`'b'`表示**二进制模式**。在二进制模式下,内容不会被解释为文本,而是作为字节序列处理。 + +这是一个打开并读取图像文件的例子。 + +```py +path1 = 'photos/jan-2023/photo1.jpg' +data1 = open(path1, 'rb').read() +type(data1) +``` + +```py +bytes +``` + +`read`的结果是一个`bytes`对象——顾名思义,它包含一个字节序列。 + +一般来说,图像文件的内容是不可读的。但如果我们从第二个文件中读取内容,我们可以使用`==`运算符进行比较。 + +```py +path2 = 'photos/jan-2023/photo2.jpg' +data2 = open(path2, 'rb').read() +data1 == data2 +``` + +```py +False +``` + +这两个文件并不相等。 + +让我们将目前为止的内容封装成一个函数。 + +```py +def same_contents(path1, path2): + data1 = open(path1, 'rb').read() + data2 = open(path2, 'rb').read() + return data1 == data2 +``` + +如果我们只有两个文件,这个函数是一个不错的选择。但假设我们有大量的文件,并且想知道是否有任何两个文件包含相同的数据。逐一比较每对文件将是低效的。 + +另一种选择是使用**哈希函数**,它接受文件内容并计算一个**摘要**,通常是一个大整数。如果两个文件包含相同的数据,它们将有相同的摘要。如果两个文件不同,它们*几乎总是*会有不同的摘要。 + +`hashlib`模块提供了几种哈希函数——我们将使用的叫做`md5`。我们将通过使用`hashlib.md5`来创建一个`HASH`对象。 + +```py +import hashlib + +md5_hash = hashlib.md5() +type(md5_hash) +``` + +```py +_hashlib.HASH +``` + +`HASH`对象提供了一个`update`方法,该方法以文件内容作为参数。 + +```py +md5_hash.update(data1) +``` + +现在我们可以使用`hexdigest`来获取摘要,作为一个十六进制数字的字符串,表示一个基数为 16 的整数。 + +```py +digest = md5_hash.hexdigest() +digest +``` + +```py +'aa1d2fc25b7ae247b2931f5a0882fa37' +``` + +以下函数封装了这些步骤。 + +```py +def md5_digest(filename): + data = open(filename, 'rb').read() + md5_hash = hashlib.md5() + md5_hash.update(data) + digest = md5_hash.hexdigest() + return digest +``` + +如果我们对不同文件的内容进行哈希处理,我们可以确认我们得到的是不同的摘要。 + +```py +filename2 = 'photos/feb-2023/photo2.jpg' +md5_digest(filename2) +``` + +```py +'6a501b11b01f89af9c3f6591d7f02c49' +``` + +现在我们几乎拥有了找到等效文件所需的所有内容。最后一步是搜索一个目录并找到所有的图片文件。 ## 13.7\. 遍历目录 + +以下函数以我们想要搜索的目录作为参数。它使用`listdir`循环遍历目录的内容。当它找到一个文件时,它打印出完整路径。当它找到一个目录时,它递归调用自己以搜索子目录。 + +```py +def walk(dirname): + for name in os.listdir(dirname): + path = os.path.join(dirname, name) + + if os.path.isfile(path): + print(path) + elif os.path.isdir(path): + walk(path) +``` + +我们可以像这样使用它: + +```py +walk('photos') +``` + +```py +photos/digests.dat +photos/digests.dir +photos/notes.txt +photos/new_notes.txt +photos/mar-2023/photo2.jpg +photos/mar-2023/photo1.jpg +photos/digests.bak +photos/jan-2023/photo3.jpg +photos/jan-2023/photo2.jpg +photos/jan-2023/photo1.jpg +photos/feb-2023/photo2.jpg +photos/feb-2023/photo1.jpg +``` + +结果的顺序取决于操作系统的具体细节。 + +## 13.8\. 调试 + +当你在读取和写入文件时,可能会遇到空白字符的问题。这些错误可能很难调试,因为空白字符通常是不可见的。例如,这里有一个包含空格、由序列`\t`表示的制表符和由序列`\n`表示的新行的字符串。当我们打印它时,看不见空白字符。 + +```py +s = '1 2\t 3\n 4' +print(s) +``` + +```py +1 2 3 + 4 +``` + +内置函数`repr`可以提供帮助。它接受任何对象作为参数,并返回该对象的字符串表示。对于字符串,它用反斜杠序列表示空白字符。 + +```py +print(repr(s)) +``` + +```py +'1 2\t 3\n 4' +``` + +这对调试很有帮助。 + +另一个你可能遇到的问题是,不同的系统使用不同的字符来表示行结束。有些系统使用换行符,表示为`\n`。其他系统使用回车符,表示为`\r`。有些系统同时使用这两者。如果你在不同系统之间移动文件,这些不一致可能会导致问题。 + +文件名大小写是你在处理不同操作系统时可能遇到的另一个问题。在 macOS 和 UNIX 中,文件名可以包含小写字母、大写字母、数字和大多数符号。但是许多 Windows 应用程序忽略大小写字母之间的区别,而且在 macOS 和 UNIX 中允许的几个符号在 Windows 中不允许。 + +## 13.9\. 术语表 + +**短暂的:** 短暂程序通常运行一段时间,结束时,其数据会丢失。 + +**持久的:** 持久程序可以无限期运行,并将至少一部分数据保存在永久存储中。 + +**目录:** 一组文件和其他目录的集合。 + +**当前工作目录:** 程序使用的默认目录,除非指定了其他目录。 + +**路径:** 指定一系列目录的字符串,通常指向一个文件。 + +**相对路径:** 从当前工作目录或某个其他指定目录开始的路径。 + +**绝对路径:** 不依赖于当前目录的路径。 + +**f-string:** 在开头有字母`f`的字符串,其中包含一个或多个用大括号括起来的表达式。 + +**配置数据:** 通常存储在文件中,指定程序应该做什么以及如何做的数据。 + +**序列化:** 将对象转换为字符串。 + +**反序列化:** 将字符串转换为对象。 + +**数据库:** 一个文件,其内容被组织成能够高效执行特定操作的形式。 + +**键值存储:** 一种数据库,其内容像字典一样组织,键对应着值。 + +**二进制模式:** 打开文件的一种方式,使得文件内容被解释为字节序列而不是字符序列。 + +**哈希函数:** 一个接受对象并计算出整数的函数,这个整数有时被称为摘要。 + +**摘要:** 哈希函数的结果,尤其是在用来检查两个对象是否相同时。 + +## 13.10\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +### 13.10.1\. 向虚拟助手提问 + +本章中出现了几个我没有详细解释的主题。以下是一些你可以向虚拟助手提问的问题,获取更多信息。 + ++ “短暂程序和持久程序有什么区别?” + ++ “什么是持久程序的例子?” + ++ “相对路径和绝对路径有什么区别?” + ++ “为什么`yaml`模块有名为`load`和`safe_load`的函数?” + ++ “当我写一个 Python shelf 时,`dat`和`dir`后缀的文件是什么?” + ++ “除了键值存储,还有哪些类型的数据库?” + ++ “当我读取一个文件时,二进制模式和文本模式有什么区别?” + ++ “字节对象和字符串有什么区别?” + ++ “什么是哈希函数?” + ++ “什么是 MD5 摘要?” + +和往常一样,如果你在以下练习中遇到困难,可以考虑向虚拟助手求助。除了提问之外,你可能还想粘贴本章中的相关函数。 + +### 13.10.2\. 练习 + +编写一个名为`replace_all`的函数,该函数接受一个模式字符串、一个替换字符串和两个文件名作为参数。它应该读取第一个文件,并将内容写入第二个文件(如果需要,创建它)。如果模式字符串出现在内容中的任何位置,它应被替换为替换字符串。 + +这是一个函数的概要,帮助你入门。 + +```py +def replace_all(old, new, source_path, dest_path): + # read the contents of the source file + reader = open(source_path) + + # replace the old string with the new + + # write the result into the destination file +``` + +为了测试你的函数,读取文件`photos/notes.txt`,将`'photos'`替换为`'images'`,并将结果写入文件`photos/new_notes.txt`。 + +### 13.10.3\. 练习 + +在前一节中,我们使用了`shelve`模块创建了一个键值存储,将排序后的字母字符串映射到一个变位词的列表。为了完成示例,编写一个名为`add_word`的函数,该函数接受一个字符串和一个架子对象作为参数。 + +它应该对单词的字母进行排序以生成一个键,然后检查该键是否已存在于架子中。如果不存在,它应该创建一个包含新单词的列表并将其添加到架子中。如果存在,它应该将新单词附加到现有值的列表中。 + +### 13.10.4\. 练习 + +在一个大型文件集合中,可能存在多个相同文件的副本,存储在不同的目录或使用不同的文件名。这个练习的目标是搜索重复文件。作为示例,我们将处理`photos`目录中的图像文件。 + +下面是它的工作原理: + ++ 我们将使用来自遍历目录的`walk`函数来搜索该目录中的文件,这些文件扩展名与`config['extensions']`中的某个扩展名匹配。 + ++ 对于每个文件,我们将使用来自检查等效文件的`md5_digest`来计算内容的摘要。 + ++ 使用架子,我们将从每个摘要映射到包含该摘要的路径列表。 + ++ 最后,我们将搜索架子,查找映射到多个文件的任何摘要。 + ++ 如果找到任何匹配项,我们将使用`same_contents`来确认文件是否包含相同的数据。 + +我将首先建议编写一些函数,然后我们将把所有内容结合在一起。 + +1. 为了识别图像文件,编写一个名为`is_image`的函数,该函数接受一个路径和一个文件扩展名列表,并在路径以列表中的某个扩展名结尾时返回`True`。提示:使用`os.path.splitext`,或者让虚拟助手为你编写这个函数。 + +1. 编写一个名为`add_path`的函数,该函数接受一个路径和一个架子作为参数。它应该使用`md5_digest`来计算文件内容的摘要。然后,它应该更新架子,要么创建一个新的项,将摘要映射到包含路径的列表,要么将路径附加到已存在的列表中。 + +1. 编写一个名为`walk_images`的`walk`函数变体,它接受一个目录并遍历该目录及其子目录中的文件。对于每个文件,它应使用`is_image`来检查它是否是图像文件,并使用`add_path`将其添加到架子中。 + +当一切正常时,你可以使用以下程序来创建书架,搜索`photos`目录并将路径添加到书架中,然后检查是否有多个文件具有相同的摘要。 + +```py +db = shelve.open('photos/digests', 'n') +walk_images('photos') + +for digest, paths in db.items(): + if len(paths) > 1: + print(paths) +``` + +你应该找到一对具有相同摘要的文件。使用`same_contents`来检查它们是否包含相同的数据。 + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权 2024 [Allen B. Downey](https://allendowney.com) + +代码许可:[MIT 许可](https://mit-license.org/) + +文本许可:[创意共享署名-非商业性使用-相同方式共享 4.0 国际版](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_16.md b/translations/zh/tnkpy3e_16.md new file mode 100644 index 0000000..08c27a0 --- /dev/null +++ b/translations/zh/tnkpy3e_16.md @@ -0,0 +1,659 @@ +# 14\. 类和函数 + +> 原文:[`allendowney.github.io/ThinkPython/chap14.html`](https://allendowney.github.io/ThinkPython/chap14.html) + +到目前为止,你已经学会了如何使用函数组织代码,以及如何使用内置类型组织数据。下一步是**面向对象编程**,它使用程序员定义的类型来组织代码和数据。 + +面向对象编程是一个庞大的话题,因此我们将逐步进行。在本章中,我们将从不规范的代码开始——也就是说,它不是经验丰富的程序员所写的那种代码——但这是一个不错的起点。在接下来的两章中,我们将使用更多的特性来编写更规范的代码。 + +## 14.1\. 程序员定义的类型 + +我们已经使用了许多 Python 的内置类型——现在我们将定义一个新类型。作为第一个例子,我们将创建一个名为`Time`的类型,表示一天中的时间。程序员定义的类型也叫做**类**。一个类的定义如下: + +```py +class Time: + """Represents a time of day.""" +``` + +头部表示新类的名称是`Time`。主体部分是一个文档字符串,用来说明这个类的用途。定义一个类会创建一个**类对象**。 + +类对象就像是一个创建对象的工厂。要创建一个`Time`对象,你可以像调用函数一样调用`Time`。 + +```py +lunch = Time() +``` + +结果是一个新对象,它的类型是`__main__.Time`,其中`__main__`是定义`Time`的模块的名称。 + +```py +type(lunch) +``` + +```py +__main__.Time +``` + +当你打印一个对象时,Python 会告诉你它的类型以及它在内存中的存储位置(前缀`0x`表示后面的数字是十六进制的)。 + +```py +print(lunch) +``` + +```py +<__main__.Time object at 0x7f31440ad0c0> +``` + +创建一个新对象称为**实例化**,该对象是类的**实例**。 + +## 14.2\. 属性 + +一个对象可以包含变量,这些变量被称为**属性**,重音在第一个音节上,发音为“AT-trib-ute”,而不是重音在第二个音节上,发音为“a-TRIB-ute”。我们可以使用点符号来创建属性。 + +```py +lunch.hour = 11 +lunch.minute = 59 +lunch.second = 1 +``` + +这个例子创建了名为`hour`、`minute`和`second`的属性,它们分别表示时间`11:59:01`的小时、分钟和秒,按我个人的理解,这是午餐时间。 + +以下图表显示了在这些赋值之后,`lunch`及其属性的状态。 + +![_images/79b2dbc2a66bedacd471711c8756ef7145d45b5c0d6850b443c583976fa3b41f.png](img/77ecdbb368a95f38f325abb612a85465.png) + +变量`lunch`引用一个`Time`对象,该对象包含三个属性。每个属性都引用一个整数。像这样的状态图——展示了对象及其属性——被称为**对象图**。 + +你可以使用点操作符来读取属性的值。 + +```py +lunch.hour +``` + +```py +11 +``` + +你可以将一个属性作为任何表达式的一部分。 + +```py +total_minutes = lunch.hour * 60 + lunch.minute +total_minutes +``` + +```py +719 +``` + +你还可以在 f-string 表达式中使用点操作符。 + +```py +f'{lunch.hour}:{lunch.minute}:{lunch.second}' +``` + +```py +'11:59:1' +``` + +但请注意,之前的例子并不符合标准格式。为了解决这个问题,我们需要在打印 `minute` 和 `second` 属性时加上前导零。我们可以通过在大括号中的表达式后面添加 **格式说明符** 来实现。以下示例中的格式说明符表示 `minute` 和 `second` 应该至少显示两位数字,并在需要时加上前导零。 + +```py +f'{lunch.hour}:{lunch.minute:02d}:{lunch.second:02d}' +``` + +```py +'11:59:01' +``` + +我们将使用这个 f-string 来编写一个函数,显示 `Time` 对象的值。你可以像往常一样将一个对象作为参数传递。例如,下面的函数将 `Time` 对象作为参数。 + +```py +def print_time(time): + s = f'{time.hour:02d}:{time.minute:02d}:{time.second:02d}' + print(s) +``` + +当我们调用它时,我们可以将 `lunch` 作为参数传递。 + +```py +print_time(lunch) +``` + +```py +11:59:01 +``` + +## 14.3\. 对象作为返回值 + +函数可以返回对象。例如,`make_time` 接受名为 `hour`、`minute` 和 `second` 的参数,将它们作为属性存储在 `Time` 对象中,并返回新对象。 + +```py +def make_time(hour, minute, second): + time = Time() + time.hour = hour + time.minute = minute + time.second = second + return time +``` + +可能会让人惊讶的是,参数和属性的名称相同,但这是编写此类函数的常见方式。下面是我们如何使用 `make_time` 来创建一个 `Time` 对象。 + +```py +time = make_time(11, 59, 1) +print_time(time) +``` + +```py +11:59:01 +``` + +## 14.4\. 对象是可变的 + +假设你去看一场电影,比如 *Monty Python and the Holy Grail*,它从 `9:20 PM` 开始,持续 `92` 分钟,也就是 `1` 小时 `32` 分钟。电影什么时候结束? + +首先,我们将创建一个表示开始时间的 `Time` 对象。 + +```py +start = make_time(9, 20, 0) +print_time(start) +``` + +```py +09:20:00 +``` + +为了找到结束时间,我们可以修改 `Time` 对象的属性,加入电影的时长。 + +```py +start.hour += 1 +start.minute += 32 +print_time(start) +``` + +```py +10:52:00 +``` + +电影将在 `10:52 PM` 结束。 + +让我们将这个计算封装成一个函数,并将其通用化,以接受电影时长的三个参数:`hours`、`minutes` 和 `seconds`。 + +```py +def increment_time(time, hours, minutes, seconds): + time.hour += hours + time.minute += minutes + time.second += seconds +``` + +这是一个演示效果的示例。 + +```py +start = make_time(9, 20, 0) +increment_time(start, 1, 32, 0) +print_time(start) +``` + +```py +10:52:00 +``` + +以下堆栈图显示了在 `increment_time` 修改对象之前,程序的状态。 + +![_images/ec55f16021188c53e3422f59671281ee881bbcc825c63d318957ce747f8e27e1.png](img/9d23255e2fd43eab2ce7769453b1414b.png) + +在函数内部,`time` 是 `start` 的别名,因此当 `time` 被修改时,`start` 也会改变。 + +这个函数是有效的,但运行后,我们会留下一个名为 `start` 的变量,它指向表示 *结束* 时间的对象,而我们不再拥有表示开始时间的对象。最好不要改变 `start`,而是创建一个新的对象来表示结束时间。我们可以通过复制 `start` 并修改副本来实现。 + +## 14.5\. 复制 + +`copy` 模块提供了一个名为 `copy` 的函数,可以复制任何对象。我们可以像这样导入它。 + +```py +from copy import copy +``` + +为了查看它是如何工作的,我们从一个新的 `Time` 对象开始,表示电影的开始时间。 + +```py +start = make_time(9, 20, 0) +``` + +并且制作一个副本。 + +```py +end = copy(start) +``` + +现在 `start` 和 `end` 包含相同的数据。 + +```py +print_time(start) +print_time(end) +``` + +```py +09:20:00 +09:20:00 +``` + +但 `is` 运算符确认它们不是同一个对象。 + +```py +start is end +``` + +```py +False +``` + +让我们看看 `==` 运算符的作用。 + +```py +start == end +``` + +```py +False +``` + +你可能会期望`==`返回`True`,因为这些对象包含相同的数据。但对于程序员自定义的类,`==`运算符的默认行为与`is`运算符相同——它检查的是身份,而不是等价性。 + +## 14.6\. 纯函数 + +我们可以使用`copy`来编写不修改其参数的纯函数。例如,下面是一个函数,它接受一个`Time`对象和一个持续时间(小时、分钟和秒)。它复制原始对象,使用`increment_time`来修改副本,并返回它。 + +```py +def add_time(time, hours, minutes, seconds): + total = copy(time) + increment_time(total, hours, minutes, seconds) + return total +``` + +下面是我们如何使用它。 + +```py +end = add_time(start, 1, 32, 0) +print_time(end) +``` + +```py +10:52:00 +``` + +返回值是一个表示电影结束时间的新对象。我们可以确认`start`没有改变。 + +```py +print_time(start) +``` + +```py +09:20:00 +``` + +`add_time`是一个**纯函数**,因为它不会修改任何传入的对象,其唯一的作用是返回一个值。 + +任何可以通过不纯函数完成的事情,也可以通过纯函数完成。事实上,一些编程语言只允许使用纯函数。使用纯函数的程序可能更不容易出错,但不纯函数有时也很方便,并且可能更高效。 + +一般来说,我建议你在合理的情况下编写纯函数,并且只有在有充分的优势时才使用不纯函数。这种方法可能被称为**函数式编程风格**。 + +## 14.7\. 原型和修补 + +在前面的示例中,`increment_time`和`add_time`似乎可以工作,但如果我们尝试另一个例子,就会发现它们并不完全正确。 + +假设你到达电影院,发现电影的开始时间是`9:40`,而不是`9:20`。当我们计算更新后的结束时间时,情况如下。 + +```py +start = make_time(9, 40, 0) +end = add_time(start, 1, 32, 0) +print_time(end) +``` + +```py +10:72:00 +``` + +结果不是一个有效的时间。问题在于`increment_time`没有处理秒数或分钟数加到超过`60`的情况。 + +这是一个改进版本,检查`second`是否大于或等于`60`——如果是,它会增加`minute`——然后检查`minute`是否大于或等于`60`——如果是,它会增加`hour`。 + +```py +def increment_time(time, hours, minutes, seconds): + time.hour += hours + time.minute += minutes + time.second += seconds + + if time.second >= 60: + time.second -= 60 + time.minute += 1 + + if time.minute >= 60: + time.minute -= 60 + time.hour += 1 +``` + +修复`increment_time`也修复了使用它的`add_time`。所以现在之前的示例可以正确运行。 + +```py +end = add_time(start, 1, 32, 0) +print_time(end) +``` + +```py +11:12:00 +``` + +但是这个函数仍然不正确,因为参数可能大于`60`。例如,假设我们给出的运行时间是`92`分钟,而不是`1`小时`32`分钟。我们可能像这样调用`add_time`。 + +```py +end = add_time(start, 0, 92, 0) +print_time(end) +``` + +```py +10:72:00 +``` + +结果不是一个有效的时间。所以我们尝试不同的方法,使用`divmod`函数。我们将复制`start`并通过增加`minute`属性来修改它。 + +```py +end = copy(start) +end.minute = start.minute + 92 +end.minute +``` + +```py +132 +``` + +现在`minute`是`132`,相当于`2`小时`12`分钟。我们可以使用`divmod`除以`60`,返回整数小时数和剩余的分钟数。 + +```py +carry, end.minute = divmod(end.minute, 60) +carry, end.minute +``` + +```py +(2, 12) +``` + +现在`minute`是正确的,我们可以将小时数加到`hour`中。 + +```py +end.hour += carry +print_time(end) +``` + +```py +11:12:00 +``` + +结果是一个有效的时间。我们可以对`hour`和`second`做同样的事情,并将整个过程封装成一个函数。 + +```py +def increment_time(time, hours, minutes, seconds): + time.hour += hours + time.minute += minutes + time.second += seconds + + carry, time.second = divmod(time.second, 60) + carry, time.minute = divmod(time.minute + carry, 60) + carry, time.hour = divmod(time.hour + carry, 60) +``` + +在这个版本的`increment_time`中,即使参数超过`60`,`add_time`也能正常工作。 + +```py +end = add_time(start, 0, 90, 120) +print_time(end) +``` + +```py +11:12:00 +``` + +本节展示了一种我称之为**原型与修补**的程序开发计划。我们从一个简单的原型开始,它在第一个例子中工作正常。然后我们用更复杂的例子进行了测试——当发现错误时,我们修改程序来修复它,就像给有破洞的轮胎打补丁一样。 + +这种方法可能有效,特别是当你对问题的理解还不够深入时。但增量修正可能会产生不必要复杂的代码——因为它处理了许多特殊情况——而且不可靠——因为很难确定你是否已经找到了所有错误。 + +## 14.8\. 设计优先开发 + +另一种方案是**设计优先开发**,这种方法在原型设计之前涉及更多的规划。在设计优先的过程中,有时对问题的高层次洞察能让编程变得更加容易。 + +在这个例子中,洞察力在于我们可以将`Time`对象视为一个 60 进制的三位数——也叫做性数字。`second`属性是“个位数”列,`minute`属性是“六十位数”列,`hour`属性是“三千六百位数”列。当我们编写`increment_time`时,我们实际上是在进行 60 进制的加法,这就是为什么我们必须从一个列进位到另一个列的原因。 + +这个观察结果暗示了另一种解决问题的方法——我们可以将`Time`对象转换为整数,利用 Python 处理整数运算的特性。 + +这里是一个将`Time`转换为整数的函数。 + +```py +def time_to_int(time): + minutes = time.hour * 60 + time.minute + seconds = minutes * 60 + time.second + return seconds +``` + +结果是自一天开始以来的秒数。例如,`01:01:01`是从一天开始算起的`1`小时、`1`分钟和`1`秒,这个值是`3600`秒、`60`秒和`1`秒的总和。 + +```py +time = make_time(1, 1, 1) +print_time(time) +time_to_int(time) +``` + +```py +01:01:01 +``` + +```py +3661 +``` + +这里有一个将整数转换为`Time`对象的函数——它使用了`divmod`函数。 + +```py +def int_to_time(seconds): + minute, second = divmod(seconds, 60) + hour, minute = divmod(minute, 60) + return make_time(hour, minute, second) +``` + +我们可以通过将前面的例子转换回`Time`对象来进行测试。 + +```py +time = int_to_time(3661) +print_time(time) +``` + +```py +01:01:01 +``` + +使用这些函数,我们可以编写一个更加简洁版的`add_time`。 + +```py +def add_time(time, hours, minutes, seconds): + duration = make_time(hours, minutes, seconds) + seconds = time_to_int(time) + time_to_int(duration) + return int_to_time(seconds) +``` + +第一行将参数转换为名为`duration`的`Time`对象。第二行将`time`和`duration`转换为秒并相加。第三行将结果转换为一个`Time`对象并返回。 + +这就是它的工作原理。 + +```py +start = make_time(9, 40, 0) +end = add_time(start, 1, 32, 0) +print_time(end) +``` + +```py +11:12:00 +``` + +在某些方面,从 60 进制转换到 10 进制再转换回来,比直接处理时间值要难一些。进制转换更为抽象,而我们对时间值的直觉理解要更强。 + +但是,如果我们有足够的洞察力,将时间视为以 60 为基数的数字——并投入精力编写转换函数`time_to_int`和`int_to_time`——我们就能得到一个更简洁、更易于阅读和调试、更可靠的程序。 + +它也更容易在之后添加新特性。例如,假设你要对两个`Time`对象进行相减,以求得它们之间的持续时间。直接实现减法操作需要借位处理,使用转换函数更简单,也更可能正确。 + +具有讽刺意味的是,有时将问题做得更复杂——或者更通用——反而能使问题更容易,因为特例更少,出错的机会也更少。 + +## 14.9\. 调试 + +Python 提供了多个内置函数,可以帮助测试和调试与对象相关的程序。例如,如果你不确定一个对象的类型,可以直接询问。 + +```py +type(start) +``` + +```py +__main__.Time +``` + +你还可以使用`isinstance`来检查一个对象是否是某个特定类的实例。 + +```py +isinstance(end, Time) +``` + +```py +True +``` + +如果你不确定一个对象是否具有某个特定属性,你可以使用内置函数`hasattr`。 + +```py +hasattr(start, 'hour') +``` + +```py +True +``` + +要获取字典中所有属性及其值,可以使用`vars`。 + +```py +vars(start) +``` + +```py +{'hour': 9, 'minute': 40, 'second': 0} +``` + +`structshape`模块,我们在第十一章中看到的,它也适用于程序员定义的类型。 + +```py +from structshape import structshape + +t = start, end +structshape(t) +``` + +```py +'tuple of 2 Time' +``` + +## 14.10\. 术语表 + +**面向对象编程:** 一种使用对象来组织代码和数据的编程风格。 + +**类:** 程序员定义的类型。类定义会创建一个新的类对象。 + +**类对象:** 表示一个类的对象——它是类定义的结果。 + +**实例化:** 创建属于某个类的对象的过程。 + +**实例:** 属于某个类的对象。 + +**属性:** 与对象相关联的变量,也叫实例变量。 + +**对象图:** 对象、其属性及其值的图形表示。 + +**格式化说明符:** 在 f-string 中,格式化说明符决定了值如何被转换为字符串。 + +**纯函数:** 一种不会修改其参数,也没有其他副作用的函数,唯一的作用是返回一个值。 + +**函数式编程风格:** 一种编程方式,尽可能使用纯函数。 + +**原型与修补:** 一种开发程序的方式,通过从粗略草图开始,逐步添加功能和修复错误。 + +**设计优先开发:** 一种开发程序的方式,通过更细致的规划,而不是原型开发和修补。 + +## 14.11\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +### 14.11.1\. 向虚拟助手提问 + +本章包含了很多新的词汇。与虚拟助手的对话有助于加深理解。可以考虑询问: + ++ “类和类型有什么区别?” + ++ “对象和实例有什么区别?” + ++ “变量和属性有什么区别?” + ++ “纯函数与非纯函数相比有哪些优缺点?” + +因为我们刚刚开始学习面向对象编程,本章中的代码并不符合惯用法——这不是经验丰富的程序员所写的代码。如果你向虚拟助手求助于这些练习,你可能会看到我们还没有介绍的特性。特别是,你可能会看到一个名为`__init__`的方法,用于初始化实例的属性。 + +如果这些特性对你来说有意义,尽管使用它们。但如果没有,耐心点——我们很快就会讲到。在此期间,试着仅用我们已学习过的特性来解决以下练习。 + +此外,在本章中我们看到一个格式说明符的例子。如需更多信息,请问:“Python f-string 中可以使用哪些格式说明符?” + +### 14.11.2\. 练习 + +编写一个名为`subtract_time`的函数,该函数接受两个`Time`对象,并返回它们之间的间隔(秒数)——假设它们是同一天的两个时间点。 + +### 14.11.3\. 练习 + +编写一个名为`is_after`的函数,该函数接受两个`Time`对象,并返回`True`如果第一个时间点比第二个时间点晚,反之返回`False`。 + +```py +def is_after(t1, t2): + """Checks whether `t1` is after `t2`. + + >>> is_after(make_time(3, 2, 1), make_time(3, 2, 0)) + True + >>> is_after(make_time(3, 2, 1), make_time(3, 2, 1)) + False + >>> is_after(make_time(11, 12, 0), make_time(9, 40, 0)) + True + """ + return None +``` + +### 14.11.4\. 练习 + +这里有一个`Date`类的定义,它表示一个日期——即年份、月份和日期。 + +```py +class Date: + """Represents a year, month, and day""" +``` + +1. 编写一个名为`make_date`的函数,该函数接受`year`、`month`和`day`作为参数,创建一个`Date`对象,将这些参数赋值给属性,并返回新对象。创建一个表示 1933 年 6 月 22 日的对象。 + +1. 编写一个名为`print_date`的函数,该函数接受一个`Date`对象,使用 f-string 格式化属性并打印结果。如果你用你创建的`Date`对象进行测试,结果应为`1933-06-22`。 + +1. 编写一个名为`is_after`的函数,该函数接受两个`Date`对象作为参数,并返回`True`如果第一个对象在第二个对象之后。创建一个表示 1933 年 9 月 17 日的第二个对象,并检查它是否在第一个对象之后。 + +提示:你可能会发现编写一个名为`date_to_tuple`的函数很有用,该函数接受一个`Date`对象并返回一个元组,包含按年份、月份、日期顺序排列的属性。 + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可:[MIT 许可证](https://mit-license.org/) + +文字许可:[知识共享署名-非商业性使用-相同方式共享 4.0 国际版](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_17.md b/translations/zh/tnkpy3e_17.md new file mode 100644 index 0000000..f1b2634 --- /dev/null +++ b/translations/zh/tnkpy3e_17.md @@ -0,0 +1,479 @@ +# 15\. 类与方法 + +> 原文:[`allendowney.github.io/ThinkPython/chap15.html`](https://allendowney.github.io/ThinkPython/chap15.html) + +Python 是一种**面向对象的语言**——也就是说,它提供支持面向对象编程的特性,具有以下这些定义性特征: + ++ 大部分计算是通过对对象执行操作来表达的。 + ++ 对象通常代表现实世界中的事物,方法通常对应于现实世界中事物之间的交互方式。 + ++ 程序包括类和方法的定义。 + +例如,在上一章中我们定义了一个`Time`类,它对应了人们记录时间的方式,并且我们定义了对应于人们与时间交互的功能。但`Time`类的定义和接下来的函数定义之间没有明确的联系。我们可以通过将函数重写为**方法**来明确这种联系,方法是在类定义内部定义的。 + +## 15.1\. 定义方法 + +在上一章中,我们定义了一个名为`Time`的类,并编写了一个名为`print_time`的函数,用于显示一天中的时间。 + +```py +class Time: + """Represents the time of day.""" + +def print_time(time): + s = f'{time.hour:02d}:{time.minute:02d}:{time.second:02d}' + print(s) +``` + +为了将`print_time`变成一个方法,我们所需要做的就是将函数定义移到类定义内部。请注意缩进的变化。 + +同时,我们会将参数名称从`time`改为`self`。这个改变不是必须的,但在方法的第一个参数通常命名为`self`。 + +```py +class Time: + """Represents the time of day.""" + + def print_time(self): + s = f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}' + print(s) +``` + +要调用这个方法,你必须传递一个`Time`对象作为参数。这里是我们用来创建`Time`对象的函数。 + +```py +def make_time(hour, minute, second): + time = Time() + time.hour = hour + time.minute = minute + time.second = second + return time +``` + +这里是一个`Time`实例。 + +```py +start = make_time(9, 40, 0) +``` + +现在有两种方式调用`print_time`。第一种(不太常见)是使用函数语法。 + +```py +Time.print_time(start) +``` + +```py +09:40:00 +``` + +在这个版本中,`Time`是类的名称,`print_time`是方法的名称,`start`作为参数传递。第二种(更符合惯例)是使用方法语法: + +```py +start.print_time() +``` + +```py +09:40:00 +``` + +在这个版本中,`start`是调用方法的对象,称为**接收者**,这个术语来源于将方法调用比作向对象发送消息的类比。 + +不管语法如何,该方法的行为是相同的。接收者被赋值为第一个参数,因此在方法内部,`self`指向与`start`相同的对象。 + +## 15.2\. 另一种方法 + +这里是上一章的`time_to_int`函数。 + +```py +def time_to_int(time): + minutes = time.hour * 60 + time.minute + seconds = minutes * 60 + time.second + return seconds +``` + +这里是将其重写为方法的版本。 + +```py +%%add_method_to Time + + def time_to_int(self): + minutes = self.hour * 60 + self.minute + seconds = minutes * 60 + self.second + return seconds +``` + +第一行使用了特殊命令`add_method_to`,它将方法添加到先前定义的类中。此命令在 Jupyter 笔记本中有效,但它不是 Python 的一部分,因此在其他环境中无法使用。通常,类的所有方法都在类定义内部,这样它们与类一起定义。但是为了本书的方便,我们一次定义一个方法。 + +如同前一个示例,方法定义是缩进的,参数名是`self`。除此之外,方法与函数是相同的。下面是我们如何调用它。 + +```py +start.time_to_int() +``` + +```py +34800 +``` + +通常我们说“调用”一个函数和“调用”一个方法,但它们的意思是一样的。 + +## 15.3\. 静态方法 + +作为另一个示例,假设我们考虑`int_to_time`函数。下面是上一章中的版本。 + +```py +def int_to_time(seconds): + minute, second = divmod(seconds, 60) + hour, minute = divmod(minute, 60) + return make_time(hour, minute, second) +``` + +这个函数接受`seconds`作为参数,并返回一个新的`Time`对象。如果我们将它转换为`Time`类的方法,我们必须在`Time`对象上调用它。但如果我们试图创建一个新的`Time`对象,我们应该在什么上调用它呢? + +我们可以通过使用**静态方法**来解决这个鸡生蛋问题,静态方法是一种不需要类的实例即可调用的方法。下面是我们如何将这个函数重写为静态方法。 + +```py +%%add_method_to Time + + def int_to_time(seconds): + minute, second = divmod(seconds, 60) + hour, minute = divmod(minute, 60) + return make_time(hour, minute, second) +``` + +因为它是一个静态方法,所以它没有`self`作为参数。要调用它,我们使用`Time`,即类对象。 + +```py +start = Time.int_to_time(34800) +``` + +结果是一个新对象,表示 9:40。 + +```py +start.print_time() +``` + +```py +09:40:00 +``` + +既然我们有了`Time.from_seconds`,我们可以利用它将`add_time`写成一个方法。下面是上一章的函数。 + +```py +def add_time(time, hours, minutes, seconds): + duration = make_time(hours, minutes, seconds) + seconds = time_to_int(time) + time_to_int(duration) + return int_to_time(seconds) +``` + +这是重写成方法的版本。 + +```py +%%add_method_to Time + + def add_time(self, hours, minutes, seconds): + duration = make_time(hours, minutes, seconds) + seconds = time_to_int(self) + time_to_int(duration) + return Time.int_to_time(seconds) +``` + +`add_time`有`self`作为参数,因为它不是静态方法。它是一个普通方法——也叫做**实例方法**。要调用它,我们需要一个`Time`实例。 + +```py +end = start.add_time(1, 32, 0) +print_time(end) +``` + +```py +11:12:00 +``` + +## 15.4\. 比较时间对象 + +作为另一个示例,假设我们将`is_after`写成一个方法。下面是`is_after`函数,这是上一章练习的一个解答。 + +```py +def is_after(t1, t2): + return time_to_int(t1) > time_to_int(t2) +``` + +这是作为方法的版本。 + +```py +%%add_method_to Time + + def is_after(self, other): + return self.time_to_int() > other.time_to_int() +``` + +因为我们在比较两个对象,而第一个参数是`self`,所以我们将第二个参数命名为`other`。要使用这个方法,我们必须在一个对象上调用它,并将另一个对象作为参数传入。 + +```py +end.is_after(start) +``` + +```py +True +``` + +这个语法的一个优点是,它几乎像在问一个问题:“`end` 在 `start` 之后吗?” + +## 15.5\. `__str__`方法 + +当你编写方法时,你几乎可以选择任何你想要的名字。然而,某些名字有特殊的含义。例如,如果一个对象有一个名为`__str__`的方法,Python 会使用这个方法将对象转换为字符串。例如,下面是一个时间对象的`__str__`方法。 + +```py +%%add_method_to Time + + def __str__(self): + s = f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}' + return s +``` + +这个方法与上一章的`print_time`类似,不同之处在于它返回字符串而不是打印它。 + +你可以用通常的方式调用这个方法。 + +```py +end.__str__() +``` + +```py +'11:12:00' +``` + +但 Python 也可以为你调用它。如果你使用内置函数`str`将一个`Time`对象转换为字符串,Python 会使用`Time`类中的`__str__`方法。 + +```py +str(end) +``` + +```py +'11:12:00' +``` + +如果你打印一个`Time`对象,它也会做相同的事情。 + +```py +print(end) +``` + +```py +11:12:00 +``` + +像`__str__`这样的函数被称为**特殊方法**。你可以通过它们的名字来识别它们,因为它们的名称前后都有两个下划线。 + +## 15.6\. **init**方法 + +最特殊的特殊方法是`__init__`,之所以如此称呼,是因为它初始化了新对象的属性。`Time`类的一个`__init__`方法可能是这样的: + +```py +%%add_method_to Time + + def __init__(self, hour=0, minute=0, second=0): + self.hour = hour + self.minute = minute + self.second = second +``` + +现在,当我们实例化一个`Time`对象时,Python 会调用`__init__`并传递参数。因此,我们可以在创建对象的同时初始化属性。 + +```py +time = Time(9, 40, 0) +print(time) +``` + +```py +09:40:00 +``` + +在这个例子中,参数是可选的,因此如果你调用`Time`时不传递任何参数,你将获得默认值。 + +```py +time = Time() +print(time) +``` + +```py +00:00:00 +``` + +如果你提供一个参数,它将覆盖`hour`: + +```py +time = Time(9) +print(time) +``` + +```py +09:00:00 +``` + +如果你提供两个参数,它们将覆盖`hour`和`minute`。 + +```py +time = Time(9, 45) +print(time) +``` + +```py +09:45:00 +``` + +如果你提供三个参数,它们将覆盖所有三个默认值。 + +当我编写一个新的类时,我几乎总是从编写`__init__`开始,这使得创建对象变得更容易,以及`__str__`,它对于调试非常有用。 + +## 15.7\. 运算符重载 + +通过定义其他特殊方法,你可以指定运算符在程序员定义类型上的行为。例如,如果你为`Time`类定义一个名为`__add__`的方法,你就可以在`Time`对象上使用`+`运算符。 + +这里是一个`__add__`方法。 + +```py +%%add_method_to Time + + def __add__(self, other): + seconds = self.time_to_int() + other.time_to_int() + return Time.int_to_time(seconds) +``` + +我们可以像这样使用它。 + +```py +duration = Time(1, 32) +end = start + duration +print(end) +``` + +```py +11:12:00 +``` + +当我们运行这三行代码时,发生了很多事情: + ++ 当我们实例化一个`Time`对象时,`__init__`方法被调用。 + ++ 当我们在`Time`对象上使用`+`运算符时,它的`__add__`方法被调用。 + ++ 当我们打印一个`Time`对象时,它的`__str__`方法被调用。 + +改变运算符的行为,使其与程序员定义的类型一起工作,这被称为**运算符重载**。对于每个运算符,比如`+`,都有一个相应的特殊方法,如`__add__`。 + +## 15.8\. 调试 + +如果`minute`和`second`的值在`0`到`60`之间(包括`0`但不包括`60`),并且`hour`是正数,则`Time`对象是有效的。此外,`hour`和`minute`应该是整数,但我们可能允许`second`有小数部分。像这样的要求被称为**不变量**,因为它们应该始终为真。换句话说,如果它们不为真,那就意味着出了问题。 + +编写代码来检查不变量可以帮助检测错误并找出其原因。例如,你可能有一个名为`is_valid`的方法,它接受一个`Time`对象,如果它违反了不变量,返回`False`。 + +```py +%%add_method_to Time + + def is_valid(self): + if self.hour < 0 or self.minute < 0 or self.second < 0: + return False + if self.minute >= 60 or self.second >= 60: + return False + if not isinstance(self.hour, int): + return False + if not isinstance(self.minute, int): + return False + return True +``` + +然后,在每个方法的开始部分,你可以检查参数,以确保它们是有效的。 + +```py +%%add_method_to Time + + def is_after(self, other): + assert self.is_valid(), 'self is not a valid Time' + assert other.is_valid(), 'self is not a valid Time' + return self.time_to_int() > other.time_to_int() +``` + +`assert`语句会计算后面的表达式。如果结果为`True`,它什么都不做;如果结果为`False`,则会引发`AssertionError`。这里是一个例子。 + +```py +duration = Time(minute=132) +print(duration) +``` + +```py +00:132:00 +``` + +```py +start.is_after(duration) +``` + +```py +AssertionError: self is not a valid Time +``` + +`assert`语句很有用,因为它们区分了处理正常情况的代码和检查错误的代码。 + +## 15.9\. 词汇表 + +**面向对象语言:** 一种提供支持面向对象编程特性的语言,特别是用户定义类型。 + +**方法(method):** 定义在类中的函数,并在该类的实例上调用。 + +**接收者(receiver):** 方法所调用的对象。 + +**静态方法(static method):** 可以在没有对象作为接收者的情况下调用的方法。 + +**实例方法(instance method):** 必须在一个对象上调用的方法。 + +**特殊方法(special method):** 改变运算符和某些函数与对象交互方式的方法。 + +**运算符重载(operator overloading):** 使用特殊方法改变运算符与用户自定义类型之间的交互方式。 + +**不变式(invariant):** 程序执行过程中始终应该为真的条件。 + +## 15.10\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +### 15.10.1\. 向虚拟助手提问 + +想了解更多关于静态方法的信息,可以向虚拟助手询问: + ++ “实例方法和静态方法有什么区别?” + ++ “为什么静态方法被称为静态方法?” + +如果你请求虚拟助手生成一个静态方法,结果可能会以`@staticmethod`开头,这是一种“装饰器”,表示这是一个静态方法。本书没有涉及装饰器的内容,但如果你感兴趣,可以向虚拟助手询问更多信息。 + +在本章中,我们将几个函数重写为方法。虚拟助手通常擅长这种代码转换。举个例子,将以下函数粘贴到虚拟助手中,并询问:“将此函数重写为`Time`类的方法。” + +```py +def subtract_time(t1, t2): + return time_to_int(t1) - time_to_int(t2) +``` + +### 15.10.2\. 练习 + +在上一章中,一系列练习要求你编写一个`Date`类和一些与`Date`对象一起使用的函数。现在,让我们练习将这些函数重写为方法。 + +1. 编写一个`Date`类的定义,用于表示一个日期——即一个年份、月份和日期。 + +1. 编写一个`__init__`方法,接受`year`、`month`和`day`作为参数,并将这些参数赋值给属性。创建一个表示 1933 年 6 月 22 日的对象。 + +1. 编写`__str__`方法,使用 f-string 格式化属性并返回结果。如果你用你创建的`Date`对象进行测试,结果应该是`1933-06-22`。 + +1. 编写一个名为`is_after`的方法,接受两个`Date`对象,如果第一个对象的日期晚于第二个对象,则返回`True`。创建一个表示 1933 年 9 月 17 日的第二个对象,并检查它是否晚于第一个对象。 + +提示:你可能会发现编写一个名为`to_tuple`的方法很有用,它返回一个包含`Date`对象属性(以年-月-日顺序)的元组。 + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可证:[MIT 许可证](https://mit-license.org/) + +文字许可证:[创作共用许可证 署名-非商业性使用-相同方式共享 4.0 国际](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_18.md b/translations/zh/tnkpy3e_18.md new file mode 100644 index 0000000..759124e --- /dev/null +++ b/translations/zh/tnkpy3e_18.md @@ -0,0 +1,784 @@ +# 16\. 类和对象 + +> 原文:[`allendowney.github.io/ThinkPython/chap16.html`](https://allendowney.github.io/ThinkPython/chap16.html) + +到目前为止,我们已经定义了类,并创建了表示一天中的时间和一年中的某一天的对象。我们还定义了可以创建、修改以及进行计算的这些对象的方法。 + +在本章中,我们将继续探索面向对象编程(OOP),通过定义表示几何对象的类,包括点、线、矩形和圆形。我们将编写方法来创建和修改这些对象,并使用 `jupyturtle` 模块来绘制它们。 + +我将使用这些类来演示面向对象编程(OOP)主题,包括对象身份与等价性、浅拷贝与深拷贝、多态等。 + +## 16.1\. 创建一个点 + +在计算机图形学中,屏幕上的位置通常通过一对坐标在 `x`-`y` 平面中表示。按照惯例,点 `(0, 0)` 通常表示屏幕的左上角,而 `(x, y)` 表示从原点出发,向右移动 `x` 单位,向下移动 `y` 单位的点。与数学课上可能见过的笛卡尔坐标系相比,`y` 轴是上下颠倒的。 + +在 Python 中,我们可以通过几种方式来表示一个点: + ++ 我们可以将坐标分别存储在两个变量 `x` 和 `y` 中。 + ++ 我们可以将坐标作为列表或元组中的元素存储。 + ++ 我们可以创建一个新的类型来表示点作为对象。 + +在面向对象编程中,最符合惯例的做法是创建一个新类型。为此,我们将从 `Point` 的类定义开始。 + +```py +class Point: + """Represents a point in 2-D space.""" + + def __init__(self, x, y): + self.x = x + self.y = y + + def __str__(self): + return f'Point({self.x}, {self.y})' +``` + +`__init__` 方法将坐标作为参数并将其赋值给属性 `x` 和 `y`。`__str__` 方法返回 `Point` 对象的字符串表示。 + +现在我们可以像这样实例化并显示一个 `Point` 对象。 + +```py +start = Point(0, 0) +print(start) +``` + +```py +Point(0, 0) +``` + +以下图显示了新对象的状态。 + +![_images/6e851969c74483fc4efb36d87b6fcdd9ee1479e2274f2efebc840e7f3520ce6f.png](img/48dbb9e8e1d723217cd7f02dafdf41bb.png) + +像往常一样,程序员定义的类型由一个外部有类型名称、内部有属性的框表示。 + +通常,程序员定义的类型是可变的,因此我们可以编写一个像 `translate` 这样的函数,它接受两个数字 `dx` 和 `dy`,并将它们加到属性 `x` 和 `y` 上。 + +```py +%%add_method_to Point + + def translate(self, dx, dy): + self.x += dx + self.y += dy +``` + +这个函数将 `Point` 从平面中的一个位置平移到另一个位置。如果我们不想修改现有的 `Point`,可以使用 `copy` 来复制原始对象,然后修改副本。 + +```py +from copy import copy + +end1 = copy(start) +end1.translate(300, 0) +print(end1) +``` + +```py +Point(300, 0) +``` + +我们可以将这些步骤封装到另一个名为 `translated` 的方法中。 + +```py +%%add_method_to Point + + def translated(self, dx=0, dy=0): + point = copy(self) + point.translate(dx, dy) + return point +``` + +与内置函数 `sort` 修改列表,`sorted` 函数创建一个新列表类似,我们现在有了一个 `translate` 方法来修改 `Point`,还有一个 `translated` 方法来创建一个新的 `Point`。 + +这里有一个例子: + +```py +end2 = start.translated(0, 150) +print(end2) +``` + +```py +Point(0, 150) +``` + +在下一节中,我们将使用这些点来定义并绘制一条线。 + +## 16.2\. 创建一个 Line + +现在让我们定义一个表示两个点之间线段的类。像往常一样,我们将从`__init__`方法和`__str__`方法开始。 + +```py +class Line: + def __init__(self, p1, p2): + self.p1 = p1 + self.p2 = p2 + + def __str__(self): + return f'Line({self.p1}, {self.p2})' +``` + +有了这两个方法,我们可以实例化并显示一个`Line`对象,我们将用它来表示`x`轴。 + +```py +line1 = Line(start, end1) +print(line1) +``` + +```py +Line(Point(0, 0), Point(300, 0)) +``` + +当我们调用`print`并传入`line`作为参数时,`print`会在`line`上调用`__str__`方法。`__str__`方法使用 f-string 来创建`line`的字符串表示。 + +f-string 包含了两个大括号中的表达式`self.p1`和`self.p2`。当这些表达式被求值时,结果是`Point`对象。然后,当它们被转换为字符串时,会调用`Point`类中的`__str__`方法。 + +这就是为什么,当我们显示一个`Line`时,结果包含了`Point`对象的字符串表示。 + +以下对象图展示了这个`Line`对象的状态。 + +![_images/4aaaffd556f4fee05dc8c25d40d9a66f559504d4c1b89fbe148c631e206580b0.png](img/0516eb6de9935e024c0b540a33ccc2f5.png) + +字符串表示和对象图对于调试很有用,但这个示例的重点是生成图形,而不是文本!所以我们将使用`jupyturtle`模块在屏幕上绘制线条。 + +正如我们在第四章中所做的那样,我们将使用`make_turtle`来创建一个`Turtle`对象以及一个可以绘制的画布。为了绘制线条,我们将使用`jupyturtle`模块中的两个新函数: + ++ `jumpto`,它接受两个坐标并将`Turtle`移动到给定位置,而不绘制线条,和 + ++ `moveto`,它将`Turtle`从当前位置移动到给定位置,并在它们之间绘制一条线段。 + +这是我们如何导入它们。 + +```py +from jupyturtle import make_turtle, jumpto, moveto +``` + +这里有一个方法,它绘制了一个`Line`。 + +```py +%%add_method_to Line + + def draw(self): + jumpto(self.p1.x, self.p1.y) + moveto(self.p2.x, self.p2.y) +``` + +为了展示它是如何使用的,我将创建第二条代表`y`轴的线。 + +```py +line2 = Line(start, end2) +print(line2) +``` + +```py +Line(Point(0, 0), Point(0, 150)) +``` + +然后绘制坐标轴。 + +```py +make_turtle() +line1.draw() +line2.draw() +``` + +随着我们定义并绘制更多对象,我们将再次使用这些线条。但首先,让我们来讨论对象的等价性和标识。 + +## 16.3\. 等价性和标识 + +假设我们创建了两个坐标相同的点。 + +```py +p1 = Point(200, 100) +p2 = Point(200, 100) +``` + +如果我们使用`==`运算符来比较它们,我们会得到程序员定义类型的默认行为——结果只有在它们是同一个对象时才为`True`,而它们并不是。 + +```py +p1 == p2 +``` + +```py +False +``` + +如果我们想改变这种行为,我们可以提供一个特殊的方法`__eq__`,定义两个`Point`对象相等的标准。 + +```py +%%add_method_to Point + +def __eq__(self, other): + return (self.x == other.x) and (self.y == other.y) +``` + +这个定义认为,当两个`Point`对象的属性相等时,它们被认为是相等的。现在当我们使用`==`运算符时,它会调用`__eq__`方法,这表示`p1`和`p2`被认为是相等的。 + +```py +p1 == p2 +``` + +```py +True +``` + +但是`is`运算符仍然表示它们是不同的对象。 + +```py +p1 is p2 +``` + +```py +False +``` + +不可能重载 `is` 运算符 —— 它始终检查对象是否相同。但对于程序员定义的类型,你可以重载 `==` 运算符,以便它检查对象是否等价。并且你可以定义什么是“等价”。 + +## 16.4\. 创建一个矩形 + +现在让我们定义一个类来表示和绘制矩形。为了简化起见,我们假设矩形要么是垂直的,要么是水平的,而不是倾斜的。你认为我们应该使用什么属性来指定矩形的位置和大小? + +至少有两种可能性: + ++ 你可以指定矩形的宽度和高度,以及一个角的位置。 + ++ 你可以指定两个对角的角。 + +此时,很难说哪种方式比另一种更好,因此让我们先实现第一种。以下是类的定义。 + +```py +class Rectangle: + """Represents a rectangle. + + attributes: width, height, corner. + """ + def __init__(self, width, height, corner): + self.width = width + self.height = height + self.corner = corner + + def __str__(self): + return f'Rectangle({self.width}, {self.height}, {self.corner})' +``` + +和往常一样,`__init__` 方法将参数赋值给属性,而 `__str__` 返回对象的字符串表示。现在我们可以实例化一个 `Rectangle` 对象,使用一个 `Point` 作为左上角的位置。 + +```py +corner = Point(30, 20) +box1 = Rectangle(100, 50, corner) +print(box1) +``` + +```py +Rectangle(100, 50, Point(30, 20)) +``` + +以下图展示了该对象的状态。 + +![_images/93ab30dffba5edf8630e6bc3afd2c786600c5a1461f0695f96fd869a561a08c7.png](img/fcebfc419783d3fbf78fbf7d44c4e86e.png) + +为了绘制一个矩形,我们将使用以下方法来创建四个 `Point` 对象,表示矩形的四个角。 + +```py +%%add_method_to Rectangle + + def make_points(self): + p1 = self.corner + p2 = p1.translated(self.width, 0) + p3 = p2.translated(0, self.height) + p4 = p3.translated(-self.width, 0) + return p1, p2, p3, p4 +``` + +然后我们将创建四个 `Line` 对象来表示矩形的边。 + +```py +%%add_method_to Rectangle + + def make_lines(self): + p1, p2, p3, p4 = self.make_points() + return Line(p1, p2), Line(p2, p3), Line(p3, p4), Line(p4, p1) +``` + +然后我们将绘制矩形的边。 + +```py +%%add_method_to Rectangle + + def draw(self): + lines = self.make_lines() + for line in lines: + line.draw() +``` + +这是一个示例。 + +```py +make_turtle() +line1.draw() +line2.draw() +box1.draw() +``` + +图中包含两条线来表示坐标轴。 + +## 16.5\. 修改矩形 + +现在让我们考虑两种修改矩形的方法,`grow` 和 `translate`。我们将看到 `grow` 按预期工作,但 `translate` 存在一个细微的 bug。在我解释之前,看看你能否先找出这个问题。 + +`grow` 需要两个数字,`dwidth` 和 `dheight`,并将它们加到矩形的 `width` 和 `height` 属性上。 + +```py +%%add_method_to Rectangle + + def grow(self, dwidth, dheight): + self.width += dwidth + self.height += dheight +``` + +这是一个示例,通过复制 `box1` 并在复制对象上调用 `grow` 来演示效果。 + +```py +box2 = copy(box1) +box2.grow(60, 40) +print(box2) +``` + +```py +Rectangle(160, 90, Point(30, 20)) +``` + +如果我们绘制 `box1` 和 `box2`,可以确认 `grow` 按预期工作。 + +```py +make_turtle() +line1.draw() +line2.draw() +box1.draw() +box2.draw() +``` + +现在让我们看看 `translate`。它需要两个数字,`dx` 和 `dy`,并将矩形在 `x` 和 `y` 方向上移动给定的距离。 + +```py +%%add_method_to Rectangle + + def translate(self, dx, dy): + self.corner.translate(dx, dy) +``` + +为了演示效果,我们将 `box2` 向右和向下移动。 + +```py +box2.translate(30, 20) +print(box2) +``` + +```py +Rectangle(160, 90, Point(60, 40)) +``` + +现在让我们再看看如果我们重新绘制 `box1` 和 `box2` 会发生什么。 + +```py +make_turtle() +line1.draw() +line2.draw() +box1.draw() +box2.draw() +``` + +看起来两个矩形都移动了,这并不是我们想要的结果!下一节将解释出了什么问题。 + +## 16.6\. 深拷贝 + +当我们使用 `copy` 来复制 `box1` 时,它复制了 `Rectangle` 对象,但没有复制其中包含的 `Point` 对象。所以 `box1` 和 `box2` 是不同的对象,这正是我们想要的效果。 + +```py +box1 is box2 +``` + +```py +False +``` + +但是它们的 `corner` 属性指向相同的对象。 + +```py +box1.corner is box2.corner +``` + +```py +True +``` + +以下图展示了这些对象的状态。 + +![_images/351c7b94fa9021934acda94ae1dd3d5b3af81e1fc228a8aaee3ea80575486ff0.png](img/fa9acdcb0f0d6c848961d6dd6343293a.png) + +`copy`所做的操作称为**浅拷贝**,因为它复制了对象本身,而不是对象内部包含的其他对象。因此,改变一个`Rectangle`的`width`或`height`不会影响另一个`Rectangle`,但改变共享的`Point`属性会影响两个对象!这种行为容易导致混淆和错误。 + +幸运的是,`copy`模块提供了另一个函数,叫做`deepcopy`,它不仅复制对象本身,还会复制它所引用的对象,甚至是它们所引用的对象,依此类推。这个操作称为**深拷贝**。 + +为了演示,我们从一个新的`Rectangle`开始,并包含一个新的`Point`。 + +```py +corner = Point(20, 20) +box3 = Rectangle(100, 50, corner) +print(box3) +``` + +```py +Rectangle(100, 50, Point(20, 20)) +``` + +然后我们将进行深拷贝。 + +```py +from copy import deepcopy + +box4 = deepcopy(box3) +``` + +我们可以确认这两个`Rectangle`对象分别引用了不同的`Point`对象。 + +```py +box3.corner is box4.corner +``` + +```py +False +``` + +因为`box3`和`box4`是完全独立的对象,我们可以修改一个而不影响另一个。为了演示,我们将移动`box3`并增大`box4`。 + +```py +box3.translate(50, 30) +box4.grow(100, 60) +``` + +我们可以确认效果如预期。 + +```py +make_turtle() +line1.draw() +line2.draw() +box3.draw() +box4.draw() +``` + +## 16.7\. 多态性 + +在前面的例子中,我们对两个`Line`对象和两个`Rectangle`对象调用了`draw`方法。我们可以通过将对象列表化来更简洁地做同样的事情。 + +```py +shapes = [line1, line2, box3, box4] +``` + +这个列表中的元素是不同类型的,但它们都提供了一个`draw`方法,因此我们可以遍历这个列表并对每个元素调用`draw`。 + +```py +make_turtle() + +for shape in shapes: + shape.draw() +``` + +在循环的第一次和第二次中,`shape`指向一个`Line`对象,因此当调用`draw`时,执行的是`Line`类中定义的方法。 + +第三次和第四次循环时,`shape`指向一个`Rectangle`对象,因此当调用`draw`时,执行的是`Rectangle`类中定义的方法。 + +从某种意义上讲,每个对象都知道如何绘制自己。这个特性被称为**多态性**。这个词来源于希腊语,意思是“多形态”。在面向对象编程中,多态性是指不同类型的对象能够提供相同的方法,这使得我们可以通过在不同类型的对象上调用相同的方法来执行许多计算——比如绘制图形。 + +作为本章末尾的练习,你将定义一个新的类,表示一个圆形并提供一个`draw`方法。然后,你可以利用多态性来绘制线条、矩形和圆形。 + +## 16.8\. 调试 + +在本章中,我们遇到了一个微妙的 bug,因为我们创建了一个`Point`对象,它被两个`Rectangle`对象共享,然后我们修改了这个`Point`。通常,有两种方法可以避免此类问题:要么避免共享对象,要么避免修改对象。 + +为了避免共享对象,我们可以使用深拷贝,正如我们在本章中所做的那样。 + +为了避免修改对象,考虑用纯函数如`translated`替换不纯函数如`translate`。例如,这是一个创建新`Point`并且永不修改其属性的`translated`版本。 + +```py + def translated(self, dx=0, dy=0): + x = self.x + dx + y = self.y + dy + return Point(x, y) +``` + +Python 提供的功能使得避免修改对象变得更容易。虽然这些功能超出了本书的范围,但如果你感兴趣,可以询问虚拟助手:“如何让一个 Python 对象变为不可变?” + +创建一个新对象比修改现有对象花费更多时间,但在实际应用中,这种差异通常并不重要。避免共享对象和不纯函数的程序通常更容易开发、测试和调试——而最好的调试方式是你不需要进行调试。 + +## 16.9\. 词汇表 + +**浅拷贝:** 一种拷贝操作,不会拷贝嵌套对象。 + +**深拷贝:** 一种拷贝操作,也会拷贝嵌套对象。 + +**多态:** 方法或运算符能够与多种类型的对象一起工作。 + +## 16.10\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +### 16.10.1\. 请求虚拟助手 + +对于以下所有练习,可以考虑请求虚拟助手的帮助。如果这样做,你需要将`Point`、`Line`和`Rectangle`类的定义作为提示的一部分提供——否则虚拟助手会猜测它们的属性和方法,生成的代码将无法正常工作。 + +### 16.10.2\. 练习 + +为`Line`类编写一个`__eq__`方法,如果`Line`对象引用的`Point`对象是相等的(无论顺序如何),则返回`True`。 + +你可以使用以下大纲来开始。 + +```py +%%add_method_to Line + +def __eq__(self, other): + return None +``` + +你可以使用这些示例来测试你的代码。 + +```py +start1 = Point(0, 0) +start2 = Point(0, 0) +end = Point(200, 100) +``` + +这个示例应该是`True`,因为`Line`对象引用的`Point`对象是相等的,并且顺序相同。 + +```py +line_a = Line(start1, end) +line_b = Line(start2, end) +line_a == line_b # should be True +``` + +```py +True +``` + +```py +line_c = Line(end, start1) +line_a == line_c # should be True +``` + +```py +True +``` + +等价关系应始终具有传递性——也就是说,如果`line_a`和`line_b`是相等的,且`line_a`和`line_c`是相等的,那么`line_b`和`line_c`也应该是相等的。 + +```py +line_b == line_c # should be True +``` + +```py +True +``` + +这个示例应该是`False`,因为`Line`对象引用的`Point`对象是不相等的。 + +```py +line_d = Line(start1, start2) +line_a == line_d # should be False +``` + +```py +False +``` + +### 16.10.3\. 练习 + +编写一个名为`midpoint`的`Line`方法,该方法计算线段的中点并将结果作为`Point`对象返回。 + +你可以使用以下大纲来开始。 + +```py +%%add_method_to Line + + def midpoint(self): + return Point(0, 0) +``` + +你可以使用以下示例来测试你的代码并绘制结果。 + +```py +start = Point(0, 0) +end1 = Point(300, 0) +end2 = Point(0, 150) +line1 = Line(start, end1) +line2 = Line(start, end2) +``` + +```py +mid1 = line1.midpoint() +print(mid1) +``` + +```py +Point(150.0, 0.0) +``` + +```py +mid2 = line2.midpoint() +print(mid2) +``` + +```py +Point(0.0, 75.0) +``` + +```py +line3 = Line(mid1, mid2) +``` + +```py +make_turtle() + +for shape in [line1, line2, line3]: + shape.draw() +``` + +### 16.10.4\. 练习 + +编写一个名为`midpoint`的`Rectangle`方法,该方法找到矩形中心的点并将结果作为`Point`对象返回。 + +你可以使用以下大纲来开始。 + +```py +%%add_method_to Rectangle + + def midpoint(self): + return Point(0, 0) +``` + +你可以使用以下示例来测试你的代码。 + +```py +corner = Point(30, 20) +rectangle = Rectangle(100, 80, corner) +``` + +```py +mid = rectangle.midpoint() +print(mid) +``` + +```py +Point(80.0, 60.0) +``` + +```py +diagonal = Line(corner, mid) +``` + +```py +make_turtle() + +for shape in [line1, line2, rectangle, diagonal]: + shape.draw() +``` + +### 16.10.5\. 练习 + +编写一个名为`make_cross`的`Rectangle`方法,该方法: + +1. 使用`make_lines`来获取表示矩形四个边的`Line`对象列表。 + +1. 计算四条线的中点。 + +1. 创建并返回一个包含两个`Line`对象的列表,这些对象代表连接相对中点的线,形成一个穿过矩形中部的十字。 + +你可以使用以下大纲来开始。 + +```py +%%add_method_to Rectangle + + def make_diagonals(self): + return [] +``` + +你可以使用以下示例来测试你的代码。 + +```py +corner = Point(30, 20) +rectangle = Rectangle(100, 80, corner) +``` + +```py +lines = rectangle.make_cross() +``` + +```py +make_turtle() + +rectangle.draw() +for line in lines: + line.draw() +``` + +### 16.10.6\. 练习 + +编写一个名为`Circle`的类的定义,具有属性`center`和`radius`,其中`center`是一个 Point 对象,`radius`是一个数字。包括特殊方法`__init__`和`__str__`,以及一个名为`draw`的方法,使用`jupyturtle`函数绘制圆形。 + +你可以使用以下函数,这是我们在第四章中编写的`circle`函数的版本。 + +```py +from jupyturtle import make_turtle, forward, left, right +import math + +def draw_circle(radius): + circumference = 2 * math.pi * radius + n = 30 + length = circumference / n + angle = 360 / n + left(angle / 2) + for i in range(n): + forward(length) + left(angle) +``` + +你可以使用以下示例来测试你的代码。我们将从一个宽度和高度为`100`的正方形`Rectangle`开始。 + +```py +corner = Point(20, 20) +rectangle = Rectangle(100, 100, corner) +``` + +以下代码应该创建一个可以适应正方形的`Circle`。 + +```py +center = rectangle.midpoint() +radius = rectangle.height / 2 + +circle = Circle(center, radius) +print(circle) +``` + +```py +Circle(Point(70.0, 70.0), 50.0) +``` + +如果一切正常,以下代码应该会在正方形内部绘制一个圆(触及四个边)。 + +```py +make_turtle(delay=0.01) + +rectangle.draw() +circle.draw() +``` + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可证:[MIT 许可证](https://mit-license.org/) + +文本许可证:[知识共享署名-非商业性使用-相同方式共享 4.0 国际版](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_19.md b/translations/zh/tnkpy3e_19.md new file mode 100644 index 0000000..5318e97 --- /dev/null +++ b/translations/zh/tnkpy3e_19.md @@ -0,0 +1,982 @@ +# 17. 继承 + +> 原文:[`allendowney.github.io/ThinkPython/chap17.html`](https://allendowney.github.io/ThinkPython/chap17.html) + +与面向对象编程最常关联的语言特性是**继承**。继承是定义一个新的类,该类是现有类的修改版本的能力。在本章中,我将通过表示扑克牌、扑克牌组和扑克手牌的类来演示继承。如果你不玩扑克,不用担心——我会告诉你需要了解的内容。 + +## 17.1. 表示牌 + +一副标准的扑克牌有 52 张——每一张牌属于四种花色之一和十三种点数之一。花色有黑桃、红桃、方块和梅花。点数有 Ace(王牌)、2、3、4、5、6、7、8、9、10、J(杰克)、Q(女王)和 K(国王)。根据你玩的游戏规则,Ace 可以比 K 高,也可以比 2 低。 + +如果我们想定义一个新的对象来表示一张扑克牌,属性应该是显而易见的:`rank` 和 `suit`。然而,属性应该是什么类型就不那么明显了。一个可能的选择是使用字符串,比如用 `'Spade'` 表示花色,`'Queen'` 表示点数。这个实现的问题在于,比较牌的大小,看看哪张牌的点数或花色更高,将变得不那么容易。 + +另一种选择是使用整数来**编码**点数和花色。在这里,“编码”意味着我们将定义一个数字与花色之间,或者数字与点数之间的映射。这种编码并不意味着是保密的(那是“加密”)。 + +例如,这个表格展示了花色和相应的整数代码: + +| 花色 | 代码 | +| --- | --- | +| 黑桃 | 3 | +| 红桃 | 2 | +| 方块 | 1 | +| 梅花 | 0 | + +使用这种编码,我们可以通过比较它们的代码来比较花色。 + +为了编码点数,我们将使用整数 `2` 来表示点数 `2`,`3` 来表示 `3`,依此类推,一直到 `10`。下面的表格展示了面牌的代码。 + +| 点数 | 代码 | +| --- | --- | +| 杰克 | 11 | +| 女王 | 12 | +| 国王 | 13 | + +我们可以使用 `1` 或 `14` 来表示 Ace(王牌),具体取决于我们希望它被视为比其他点数低还是高。 + +为了表示这些编码,我们将使用两个字符串列表,一个包含花色的名称,另一个包含点数的名称。 + +这是一个表示扑克牌的类的定义,使用这些字符串列表作为**类变量**,类变量是定义在类内部,但不在方法内部的变量。 + +```py +class Card: + """Represents a standard playing card.""" + + suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades'] + rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7', + '8', '9', '10', 'Jack', 'Queen', 'King', 'Ace'] +``` + +`rank_names` 的第一个元素是 `None`,因为没有点数为零的牌。通过包括 `None` 作为占位符,我们得到了一个很好的属性:索引 `2` 映射到字符串 `'2'`,以此类推。 + +类变量与类相关联,而不是与类的实例相关联,因此我们可以像这样访问它们。 + +```py +Card.suit_names +``` + +```py +['Clubs', 'Diamonds', 'Hearts', 'Spades'] +``` + +我们可以使用 `suit_names` 来查找花色并获取相应的字符串。 + +```py +Card.suit_names[0] +``` + +```py +'Clubs' +``` + +并且可以使用 `rank_names` 来查找点数。 + +```py +Card.rank_names[11] +``` + +```py +'Jack' +``` + +## 17.2\. 卡片属性 + +这是`Card`类的`__init__`方法——它接受`花色`和`点数`作为参数,并将它们分配给具有相同名称的属性。 + +```py +%%add_method_to Card + + def __init__(self, suit, rank): + self.suit = suit + self.rank = rank +``` + +现在我们可以这样创建一个`Card`对象。 + +```py +queen = Card(1, 12) +``` + +我们可以使用新的实例来访问属性。 + +```py +queen.suit, queen.rank +``` + +```py +(1, 12) +``` + +使用实例来访问类变量也是合法的。 + +```py +queen.suit_names +``` + +```py +['Clubs', 'Diamonds', 'Hearts', 'Spades'] +``` + +但如果使用类来访问,能够更清楚地表明它们是类变量,而不是实例属性。 + +## 17.3\. 打印卡片 + +这是一个`__str__`方法,用于`Card`对象。 + +```py +%%add_method_to Card + + def __str__(self): + rank_name = Card.rank_names[self.rank] + suit_name = Card.suit_names[self.suit] + return f'{rank_name} of {suit_name}' +``` + +当我们打印一个`Card`对象时,Python 会调用`__str__`方法来获取该卡片的可读表示。 + +```py +print(queen) +``` + +```py +Queen of Diamonds +``` + +以下是`Card`类对象和卡片实例的示意图。`Card`是一个类对象,所以它的类型是`type`。`queen`是`Card`的实例,所以它的类型是`Card`。为了节省空间,我没有画出`suit_names`和`rank_names`的内容。 + +![_images/5f1d5a265aab792dbe104aaedafa1a65dded15806c5dff8a8854f2f3896703eb.png](img/cf84277cd6a15970041c403cff3b289f.png) + +每个`Card`实例都有自己的`suit`和`rank`属性,但只有一个`Card`类对象,并且类变量`suit_names`和`rank_names`只有一份副本。 + +## 17.4\. 比较卡片 + +假设我们创建了第二个具有相同花色和点数的`Card`对象。 + +```py +queen2 = Card(1, 12) +print(queen2) +``` + +```py +Queen of Diamonds +``` + +如果我们使用`==`运算符来比较它们,它会检查`queen`和`queen2`是否指向同一个对象。 + +```py +queen == queen2 +``` + +```py +False +``` + +它们不相等,所以返回`False`。我们可以通过定义特殊方法`__eq__`来改变这种行为。 + +```py +%%add_method_to Card + + def __eq__(self, other): + return self.suit == other.suit and self.rank == other.rank +``` + +`__eq__`接受两个`Card`对象作为参数,如果它们具有相同的花色和点数,即使它们不是同一个对象,也会返回`True`。换句话说,它会检查它们是否等价,即使它们不是同一个对象。 + +当我们使用`==`运算符比较`Card`对象时,Python 会调用`__eq__`方法。 + +```py +queen == queen2 +``` + +```py +True +``` + +作为第二个测试,让我们创建一张具有相同花色但不同点数的卡片。 + +```py +six = Card(1, 6) +print(six) +``` + +```py +6 of Diamonds +``` + +我们可以确认`queen`和`six`不是等价的。 + +```py +queen == six +``` + +```py +False +``` + +如果我们使用`!=`运算符,Python 会调用一个叫做`__ne__`的特殊方法(如果存在)。如果没有,它会调用`__eq__`并反转结果——也就是说,如果`__eq__`返回`True`,那么`!=`运算符的结果就是`False`。 + +```py +queen != queen2 +``` + +```py +False +``` + +```py +queen != six +``` + +```py +True +``` + +现在假设我们想比较两张卡片,看看哪一张更大。如果我们使用关系运算符之一,将会出现`TypeError`。 + +```py +queen < queen2 +``` + +```py +TypeError: '<' not supported between instances of 'Card' and 'Card' +``` + +要改变`<`运算符的行为,我们可以定义一个特殊的方法叫做`__lt__`,它是“less than”(小于)的缩写。为了简单起见,假设花色比点数更重要——所以所有黑桃的等级高于所有红心,红心又高于所有方块,依此类推。如果两张卡片的花色相同,那么点数较大的卡片获胜。 + +为了实现这个逻辑,我们将使用以下方法,它返回一个元组,包含卡片的花色和点数,按此顺序。 + +```py +%%add_method_to Card + + def to_tuple(self): + return (self.suit, self.rank) +``` + +我们可以使用这个方法来编写`__lt__`。 + +```py +%%add_method_to Card + + def __lt__(self, other): + return self.to_tuple() < other.to_tuple() +``` + +元组比较会比较每个元组的第一个元素,这些元素表示花色。如果它们相同,则比较第二个元素,这些元素表示点数。 + +现在,如果我们使用`<`运算符,它会调用`__lt__`方法。 + +```py +six < queen +``` + +```py +True +``` + +如果我们使用`>`运算符,它会调用一个名为`__gt__`的特殊方法(如果存在)。否则,它会使用`__lt__`,并将参数顺序调换。 + +```py +queen < queen2 +``` + +```py +False +``` + +```py +queen > queen2 +``` + +```py +False +``` + +最后,如果我们使用`<=`运算符,它会调用一个名为`__le__`的特殊方法。 + +```py +%%add_method_to Card + + def __le__(self, other): + return self.to_tuple() <= other.to_tuple() +``` + +所以我们可以检查一张牌是否小于或等于另一张牌。 + +```py +queen <= queen2 +``` + +```py +True +``` + +```py +queen <= six +``` + +```py +False +``` + +如果我们使用`>=`运算符,它会使用`__ge__`(如果存在)。否则,它会使用`__le__`,并将参数顺序调换。 + +```py +queen >= six +``` + +```py +True +``` + +正如我们所定义的,这些方法是完整的,因为我们可以比较任何两个`Card`对象,而且是相容的,因为不同运算符的结果不互相矛盾。拥有这两个特性,我们可以说`Card`对象是**完全有序**的。这意味着,正如我们很快将看到的,它们可以被排序。 + +## 17.5\. 牌组 + +现在我们有了表示牌的对象,让我们定义表示牌组的对象。以下是`Deck`类的定义,其中`__init__`方法接收一个`Card`对象列表作为参数,并将其赋值给一个名为`cards`的属性。 + +```py +class Deck: + + def __init__(self, cards): + self.cards = cards +``` + +要创建一个包含标准牌组 52 张牌的列表,我们将使用以下静态方法。 + +```py +%%add_method_to Deck + + def make_cards(): + cards = [] + for suit in range(4): + for rank in range(2, 15): + card = Card(suit, rank) + cards.append(card) + return cards +``` + +在`make_cards`中,外循环枚举从`0`到`3`的花色,内循环枚举从`2`到`14`的点数——其中`14`表示比国王还大的 Ace。每次迭代都会用当前的花色和点数创建一张新的`Card`,并将其添加到`cards`列表中。 + +下面是我们如何制作一组牌并创建一个包含这些牌的`Deck`对象。 + +```py +cards = Deck.make_cards() +deck = Deck(cards) +len(deck.cards) +``` + +```py +52 +``` + +它包含 52 张牌,符合预期。 + +## 17.6\. 打印牌组 + +这是`Deck`的`__str__`方法。 + +```py +%%add_method_to Deck + + def __str__(self): + res = [] + for card in self.cards: + res.append(str(card)) + return '\n'.join(res) +``` + +这个方法展示了一种高效地积累大字符串的方式——先构建一个字符串列表,然后使用字符串方法`join`。 + +我们将用一副只包含两张牌的牌组来测试这个方法。 + +```py +small_deck = Deck([queen, six]) +``` + +如果我们调用`str`,它会调用`__str__`方法。 + +```py +str(small_deck) +``` + +```py +'Queen of Diamonds\n6 of Diamonds' +``` + +当 Jupyter 显示字符串时,它会显示字符串的“表示”形式,其中换行符用序列`\n`表示。 + +然而,如果我们打印结果,Jupyter 会显示字符串的“可打印”形式,其中换行符被显示为空格。 + +```py +print(small_deck) +``` + +```py +Queen of Diamonds +6 of Diamonds +``` + +所以这些牌会显示在不同的行上。 + +## 17.7\. 添加、删除、洗牌和排序 + +要发牌,我们需要一个方法,它从牌组中移除一张牌并返回。列表方法`pop`提供了一个方便的方式来实现这一点。 + +```py +%%add_method_to Deck + + def take_card(self): + return self.cards.pop() +``` + +下面是我们如何使用它。 + +```py +card = deck.take_card() +print(card) +``` + +```py +Ace of Spades +``` + +我们可以确认牌组中还剩下`51`张牌。 + +```py +len(deck.cards) +``` + +```py +51 +``` + +要添加一张牌,我们可以使用列表方法`append`。 + +```py +%%add_method_to Deck + + def put_card(self, card): + self.cards.append(card) +``` + +作为示例,我们可以把刚刚弹出的牌放回去。 + +```py +deck.put_card(card) +len(deck.cards) +``` + +```py +52 +``` + +要洗牌,我们可以使用`random`模块中的`shuffle`函数: + +```py +import random +``` + +```py +%%add_method_to Deck + + def shuffle(self): + random.shuffle(self.cards) +``` + +如果我们洗牌并打印前几张卡片,我们会看到它们的顺序看似随机。 + +```py +deck.shuffle() +for card in deck.cards[:4]: + print(card) +``` + +```py +2 of Diamonds +4 of Hearts +5 of Clubs +8 of Diamonds +``` + +要对卡片进行排序,我们可以使用列表方法`sort`,该方法会“就地”排序元素——也就是说,它修改原列表,而不是创建一个新的列表。 + +```py +%%add_method_to Deck + + def sort(self): + self.cards.sort() +``` + +当我们调用`sort`时,它会使用`__lt__`方法来比较卡片。 + +```py +deck.sort() +``` + +如果我们打印前几张卡片,可以确认它们是按升序排列的。 + +```py +for card in deck.cards[:4]: + print(card) +``` + +```py +2 of Clubs +3 of Clubs +4 of Clubs +5 of Clubs +``` + +在这个例子中,`Deck.sort`除了调用`list.sort`之外并不会做其他事情。将责任传递给其他方法的做法称为**委托**。 + +## 17.8\. 父类和子类 + +继承是定义一个新类的能力,这个新类是现有类的修改版。例如,假设我们想定义一个类来表示“手牌”,也就是一个玩家持有的卡片。 + ++ `Hand`类似于`Deck`——两者都是由卡片集合组成,并且都需要执行像添加和移除卡片这样的操作。 + ++ `Hand`和`Deck`也有不同之处——我们希望对`Hand`进行的操作在`Deck`上没有意义。例如,在扑克中,我们可能会比较两副牌,看看哪一副胜出。在桥牌中,我们可能会计算一副牌的分数,以便进行叫牌。 + +这种类之间的关系——其中一个是另一个的专门化版本——非常适合继承。 + +要定义一个基于现有类的新类,我们将现有类的名称放在括号中。 + +```py +class Hand(Deck): + """Represents a hand of playing cards.""" +``` + +这个定义表明`Hand`继承自`Deck`,这意味着`Hand`对象可以访问`Deck`中定义的方法,如`take_card`和`put_card`。 + +`Hand`也继承了`Deck`中的`__init__`方法,但如果我们在`Hand`类中定义了`__init__`,它将覆盖`Deck`类中的版本。 + +```py +%%add_method_to Hand + + def __init__(self, label=''): + self.label = label + self.cards = [] +``` + +这个版本的`__init__`方法接受一个可选的字符串作为参数,并且总是从一个空的卡片列表开始。当我们创建一个`Hand`对象时,Python 会调用这个方法,而不是在`Deck`中的方法——我们可以通过检查结果是否包含`label`属性来确认这一点。 + +```py +hand = Hand('player 1') +hand.label +``` + +```py +'player 1' +``` + +要发一张卡片,我们可以使用`take_card`从`Deck`中移除一张卡片,并使用`put_card`将卡片添加到`Hand`中。 + +```py +deck = Deck(cards) +card = deck.take_card() +hand.put_card(card) +print(hand) +``` + +```py +Ace of Spades +``` + +让我们将这段代码封装到一个名为`move_cards`的`Deck`方法中。 + +```py +%%add_method_to Deck + + def move_cards(self, other, num): + for i in range(num): + card = self.take_card() + other.put_card(card) +``` + +这个方法是多态的——也就是说,它可以与多种类型一起工作:`self`和`other`可以是`Hand`或`Deck`。因此,我们可以使用这个方法将一张卡片从`Deck`发给`Hand`,从一副`Hand`发给另一副,或者从`Hand`发回`Deck`。 + +当一个新类继承自现有类时,现有类称为**父类**,新类称为**子类**。一般来说: + ++ 子类的实例应该拥有父类的所有属性,但它们可以有额外的属性。 + ++ 子类应该拥有父类的所有方法,但它可以有额外的方法。 + ++ 如果子类重写了父类的方法,则新方法应该采用相同的参数,并返回兼容的结果。 + +这一套规则被称为“李斯科夫替代原则”,以计算机科学家芭芭拉·李斯科夫的名字命名。 + +如果你遵循这些规则,任何设计用来处理父类实例的函数或方法,比如`Deck`,也可以用来处理子类实例,比如`Hand`。如果违反这些规则,你的代码将像纸牌屋一样崩塌(抱歉)。 + +## 17.9\. 专门化 + +让我们创建一个名为`BridgeHand`的类,用来表示桥牌中的一手牌——这是一种广泛玩的纸牌游戏。我们将从`Hand`继承,并添加一个名为`high_card_point_count`的新方法,使用“高牌点数”方法来评估一手牌,该方法会为手中的高牌加总分数。 + +这是一个类定义,其中包含一个类变量,映射了从卡片名称到其点数值的字典。 + +```py +class BridgeHand(Hand): + """Represents a bridge hand.""" + + hcp_dict = { + 'Ace': 4, + 'King': 3, + 'Queen': 2, + 'Jack': 1, + } +``` + +给定一张卡片的等级,比如`12`,我们可以使用`Card.rank_names`获取该等级的字符串表示,然后使用`hcp_dict`获取它的分数。 + +```py +rank = 12 +rank_name = Card.rank_names[rank] +score = BridgeHand.hcp_dict.get(rank_name, 0) +rank_name, score +``` + +```py +('Queen', 2) +``` + +以下方法遍历`BridgeHand`中的卡片,并加总它们的分数。 + +```py +%%add_method_to BridgeHand + + def high_card_point_count(self): + count = 0 + for card in self.cards: + rank_name = Card.rank_names[card.rank] + count += BridgeHand.hcp_dict.get(rank_name, 0) + return count +``` + +为了进行测试,我们将发一手五张牌——桥牌通常有十三张,但使用小例子更容易测试代码。 + +```py +hand = BridgeHand('player 2') + +deck.shuffle() +deck.move_cards(hand, 5) +print(hand) +``` + +```py +4 of Diamonds +King of Hearts +10 of Hearts +10 of Clubs +Queen of Diamonds +``` + +这是国王和皇后的总分。 + +```py +hand.high_card_point_count() +``` + +```py +5 +``` + +`BridgeHand`继承了`Hand`的变量和方法,并增加了一个类变量和一个特定于桥牌的方法。使用这种方式进行继承被称为**专门化**,因为它定义了一个针对特定用途(如打桥牌)而专门化的新类。 + +## 17.10\. 调试 + +继承是一个有用的特性。一些如果没有继承就会重复的程序,可以用继承更简洁地编写。此外,继承有助于代码复用,因为你可以在不修改父类的情况下定制其行为。在某些情况下,继承结构反映了问题的自然结构,这使得设计更容易理解。 + +另一方面,继承可能会让程序变得难以阅读。当调用一个方法时,有时不清楚在哪里找到它的定义——相关代码可能分散在多个模块中。 + +每当你不确定程序的执行流程时,最简单的解决方法是,在相关方法的开始处添加打印语句。如果`Deck.shuffle`打印一条类似于`Running Deck.shuffle`的消息,那么程序运行时就能追踪执行流程。 + +作为替代,你可以使用以下函数,它接受一个对象和一个方法名(作为字符串),并返回提供该方法定义的类。 + +```py +def find_defining_class(obj, method_name): + """Find the class where the given method is defined.""" + for typ in type(obj).mro(): + if method_name in vars(typ): + return typ + return f'Method {method_name} not found.' +``` + +`find_defining_class`使用`mro`方法获取类对象(类型)的列表,该列表将用于搜索方法。“MRO”代表“方法解析顺序”,即 Python 搜索的类的顺序,用于“解析”方法名——也就是说,找到该名称所引用的函数对象。 + +作为示例,我们实例化一个 `BridgeHand`,然后找到 `shuffle` 的定义类。 + +```py +hand = BridgeHand('player 3') +find_defining_class(hand, 'shuffle') +``` + +```py +__main__.Deck +``` + +`BridgeHand` 对象的 `shuffle` 方法是 `Deck` 中的那个。 + +## 17.11\. 词汇表 + +**继承:** 定义一个新类,该类是先前定义的类的修改版本。 + +**编码:** 使用另一组值来表示一组值,通过在它们之间构建映射。 + +**类变量:** 在类定义内部定义的变量,但不在任何方法内部。 + +**完全有序:** 如果我们能比较任何两个元素并且比较结果是一致的,那么该集合就是完全有序的。 + +**委托:** 当一个方法将责任传递给另一个方法来完成大部分或所有工作时。 + +**父类:** 被继承的类。 + +**子类:** 继承自另一个类的类。 + +**专门化:** 使用继承来创建一个新类,该类是现有类的专门化版本。 + +## 17.12\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +```py +Exception reporting mode: Verbose +``` + +### 17.12.1\. 请求虚拟助手 + +当它运行顺利时,面向对象编程可以使程序更易读、可测试和可重用。但它也可能使程序变得复杂,难以维护。因此,面向对象编程是一个有争议的话题——一些人喜欢它,而另一些人则不喜欢。 + +要了解更多关于该主题的信息,请请求虚拟助手: + ++ 面向对象编程有哪些优缺点? + ++ 当人们说“偏好组合而非继承”时,这是什么意思? + ++ 里氏替换原则是什么? + ++ Python 是面向对象的语言吗? + ++ 一个集合要是完全有序的,需要满足哪些要求? + +和往常一样,考虑使用虚拟助手来帮助完成以下练习。 + +### 17.12.2\. 练习 + +在桥牌中,“trick” 是一轮比赛,其中四名玩家各出一张牌。为了表示这些牌,我们将定义一个继承自 `Deck` 的类。 + +```py +class Trick(Deck): + """Represents a trick in contract bridge.""" +``` + +作为示例,考虑这个技巧,第一位玩家以方块 3 出牌,这意味着方块是“领先花色”。第二位和第三位玩家“跟花色”,也就是出与领先花色相同的牌。第四位玩家出了一张不同花色的牌,这意味着他们不能赢得这一轮。所以这轮的赢家是第三位玩家,因为他们出了领先花色中的最大牌。 + +```py +cards = [Card(1, 3), + Card(1, 10), + Card(1, 12), + Card(2, 13)] +trick = Trick(cards) +print(trick) +``` + +```py +3 of Diamonds +10 of Diamonds +Queen of Diamonds +King of Hearts +``` + +编写一个 `Trick` 方法,名为 `find_winner`,它遍历 `Trick` 中的牌,并返回获胜牌的索引。在前面的示例中,获胜牌的索引是 `2`。 + +### 17.12.3\. 练习 + +接下来的几个练习要求你编写函数来分类扑克牌型。如果你不熟悉扑克牌,我会解释你需要知道的内容。我们将使用以下类来表示扑克牌型。 + +```py +class PokerHand(Hand): + """Represents a poker hand.""" + + def get_suit_counts(self): + counter = {} + for card in self.cards: + key = card.suit + counter[key] = counter.get(key, 0) + 1 + return counter + + def get_rank_counts(self): + counter = {} + for card in self.cards: + key = card.rank + counter[key] = counter.get(key, 0) + 1 + return counter +``` + +`PokerHand` 提供了两个方法,帮助完成练习。 + ++ `get_suit_counts`循环遍历`PokerHand`中的牌,计算每种花色的牌数,并返回一个字典,将每个花色代码映射到它出现的次数。 + ++ `get_rank_counts`与牌的等级执行相同的操作,返回一个字典,将每个等级代码映射到它出现的次数。 + +所有接下来的练习都可以仅使用我们迄今学到的 Python 特性完成,但其中一些比以前的练习更难。我鼓励你寻求虚拟助手的帮助。 + +对于这样的问题,通常很好地寻求关于策略和算法的一般建议。然后你可以自己编写代码,或者请求代码。如果你请求代码,你可能需要在提示的一部分中提供相关的类定义。 + +作为第一练习,我们将编写一个名为`has_flush`的方法,检查一手牌是否有“同花” - 即是否包含至少五张同一花色的牌。 + +在大多数扑克牌的变体中,一手牌通常包含五张或七张牌,但也有一些异国情调的变体,一手牌包含其他数量的牌。但不管一手牌有多少张牌,只有五张牌才算在内,这五张牌可以组成最好的一手牌。 + +### 17.12.4\. 练习 + +编写一个名为`has_straight`的方法,检查一手牌是否包含顺子,即五张具有连续等级的牌。例如,如果一手牌包含等级`5`、`6`、`7`、`8`和`9`,那么它就包含顺子。 + +一张 A 可以出现在 2 之前或 K 之后,所以`A`、`2`、`3`、`4`、`5`是顺子,`10`、`J`、`Q`、`K`、`A`也是顺子。但顺子不能“绕过”,所以`K`、`A`、`2`、`3`、`4`不是顺子。 + +### 17.12.5\. 练习 + +一手牌有一个顺子同花顺,如果它包含五张既是顺子又是同一花色的牌 - 也就是说,五张具有连续等级的相同花色的牌。编写一个`PokerHand`方法,检查一手牌是否有顺子同花顺。 + +### 17.12.6\. 练习 + +一手扑克牌有一对,如果它包含两张或更多张同等级的牌。编写一个`PokerHand`方法,检查一手牌是否包含一对。 + +要测试你的方法,这里有一个有一对的手牌。 + +```py +pair = deepcopy(bad_hand) +pair.put_card(Card(1, 2)) +print(pair) +``` + +```py +2 of Clubs +3 of Clubs +4 of Hearts +5 of Spades +7 of Clubs +2 of Diamonds +``` + +```py +pair.has_pair() # should return True +``` + +```py +True +``` + +```py +bad_hand.has_pair() # should return False +``` + +```py +False +``` + +```py +good_hand.has_pair() # should return False +``` + +```py +False +``` + +### 17.12.7\. 练习 + +一手牌有一个葫芦,如果它包含一组三张同一等级的牌和两张另一等级的牌。编写一个`PokerHand`方法,检查一手牌是否有葫芦。 + +### 17.12.8\. 练习 + +这个练习是一个关于一个常见错误的警示故事,这种错误往往很难调试。考虑以下的类定义。 + +```py +class Kangaroo: + """A Kangaroo is a marsupial.""" + + def __init__(self, name, contents=[]): + """Initialize the pouch contents. + + name: string + contents: initial pouch contents. + """ + self.name = name + self.contents = contents + + def __str__(self): + """Return a string representaion of this Kangaroo. + """ + t = [ self.name + ' has pouch contents:' ] + for obj in self.contents: + s = ' ' + object.__str__(obj) + t.append(s) + return '\n'.join(t) + + def put_in_pouch(self, item): + """Adds a new item to the pouch contents. + + item: object to be added + """ + self.contents.append(item) +``` + +`__init__`接受两个参数:`name`是必需的,但`contents`是可选的 - 如果没有提供,则默认值为空列表。 + +`__str__`返回对象的字符串表示,包括袋子的名称和内容。 + +`put_in_pouch`接受任何对象并将其附加到`contents`中。 + +现在让我们看看这个类是如何工作的。我们将创建两个名为'Kanga'和'Roo'的`Kangaroo`对象。 + +```py +kanga = Kangaroo('Kanga') +roo = Kangaroo('Roo') +``` + +我们将向 Kanga 的袋子中添加两个字符串和 Roo。 + +```py +kanga.put_in_pouch('wallet') +kanga.put_in_pouch('car keys') +kanga.put_in_pouch(roo) +``` + +如果我们打印`kanga`,似乎一切正常。 + +```py +print(kanga) +``` + +```py +Kanga has pouch contents: + 'wallet' + 'car keys' + <__main__.Kangaroo object at 0x7f44f9b4e500> +``` + +但是如果我们打印`roo`会发生什么呢? + +```py +print(roo) +``` + +```py +Roo has pouch contents: + 'wallet' + 'car keys' + <__main__.Kangaroo object at 0x7f44f9b4e500> +``` + +Roo 的袋子里包含与 Kanga 的袋子相同的内容,包括对`roo`的引用! + +看看你能否弄清楚哪里出了问题。然后问虚拟助手:“以下程序有什么问题?”并粘贴`Kangaroo`的定义。 + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可:[MIT 许可证](https://mit-license.org/) + +文本许可:[创作共用 署名-非商业性使用-相同方式共享 4.0 国际版](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_20.md b/translations/zh/tnkpy3e_20.md new file mode 100644 index 0000000..2273439 --- /dev/null +++ b/translations/zh/tnkpy3e_20.md @@ -0,0 +1,984 @@ +# 18\. Python 附加功能 + +> 原文:[`allendowney.github.io/ThinkPython/chap18.html`](https://allendowney.github.io/ThinkPython/chap18.html) + +本书的目标之一是尽量少教你 Python。当有两种方法可以做某事时,我选择了一种并避免提到另一种。有时,我会将第二种方法放进练习中。 + +现在我想回过头去补充一些被遗忘的好点子。Python 提供了一些不是真正必要的功能——你可以不使用它们写出好的代码——但使用它们,你可以写出更简洁、可读或高效的代码,有时甚至是三者兼具。 + +## 18.1\. 集合 + +Python 提供了一个名为`set`的类,用于表示一组唯一的元素。要创建一个空集合,我们可以像使用函数一样使用类对象。 + +```py +s1 = set() +s1 +``` + +```py +set() +``` + +我们可以使用 `add` 方法添加元素。 + +```py +s1.add('a') +s1.add('b') +s1 +``` + +```py +{'a', 'b'} +``` + +或者我们可以将任何类型的序列传递给 `set`。 + +```py +s2 = set('acd') +s2 +``` + +```py +{'a', 'c', 'd'} +``` + +一个元素在 `set` 中只能出现一次。如果你添加一个已经存在的元素,它将没有任何效果。 + +```py +s1.add('a') +s1 +``` + +```py +{'a', 'b'} +``` + +或者,如果你用一个包含重复元素的序列创建一个集合,结果将只包含唯一元素。 + +```py +set('banana') +``` + +```py +{'a', 'b', 'n'} +``` + +本书中的一些练习可以通过集合高效简洁地完成。例如,以下是第十一章中的一个练习解决方案,使用字典来检查序列中是否存在重复元素。 + +```py +def has_duplicates(t): + d = {} + for x in t: + d[x] = True + return len(d) < len(t) +``` + +这个版本将 `t` 中的元素作为字典中的键添加,然后检查键是否比元素少。使用集合,我们可以像这样编写相同的函数。 + +```py +def has_duplicates(t): + s = set(t) + return len(s) < len(t) +``` + +一个元素在集合中只能出现一次,因此,如果 `t` 中的某个元素出现多次,集合将比 `t` 小。如果没有重复元素,集合的大小将与 `t` 相同。 + +`set` 对象提供了一些方法来执行集合操作。例如,`union` 计算两个集合的并集,它是一个包含两个集合中所有元素的新集合。 + +```py +s1.union(s2) +``` + +```py +{'a', 'b', 'c', 'd'} +``` + +一些算术运算符可以与集合一起使用。例如,`-` 运算符执行集合差集运算——结果是一个新集合,包含第一个集合中所有*不*在第二个集合中的元素。 + +```py +s1 - s2 +``` + +```py +{'b'} +``` + +在第十二章中,我们使用字典查找文档中出现但不在单词列表中的单词。我们使用了以下函数,它接收两个字典,并返回一个仅包含第一个字典中不出现在第二个字典中的键的新字典。 + +```py +def subtract(d1, d2): + res = {} + for key in d1: + if key not in d2: + res[key] = d1[key] + return res +``` + +使用集合,我们不必自己编写这个函数。如果 `word_counter` 是一个包含文档中唯一单词的字典,`word_list` 是一个有效单词的列表,我们可以像这样计算集合差异。 + +```py +set(word_counter) - set(word_list) +``` + +结果是一个包含文档中未出现在单词列表中的单词的集合。 + +关系运算符可以与集合一起使用。例如,`<=` 用于检查一个集合是否是另一个集合的子集,包括它们相等的情况。 + +```py +set('ab') <= set('abc') +``` + +```py +True +``` + +使用这些运算符,我们可以利用集合来完成第七章的一些练习。例如,下面是一个使用循环的 `uses_only` 版本。 + +```py +def uses_only(word, available): + for letter in word: + if letter not in available: + return False + return True +``` + +`uses_only` 检查 `word` 中的所有字母是否都在 `available` 中。使用集合,我们可以像这样重写它。 + +```py +def uses_only(word, available): + return set(word) <= set(available) +``` + +如果 `word` 中的字母是 `available` 中字母的子集,那么意味着 `word` 只使用了 `available` 中的字母。 + +## 18.2\. Counters + +`Counter` 类似于集合,但如果一个元素出现多次,`Counter` 会记录该元素出现的次数。如果你熟悉数学中的“多重集”概念,那么 `Counter` 就是表示多重集的自然方式。 + +`Counter` 类定义在一个名为 `collections` 的模块中,因此你需要导入该模块。然后,你可以像使用函数一样使用类对象,并将字符串、列表或其他类型的序列作为参数传递。 + +```py +from collections import Counter + +counter = Counter('banana') +counter +``` + +```py +Counter({'a': 3, 'n': 2, 'b': 1}) +``` + +```py +from collections import Counter + +t = (1, 1, 1, 2, 2, 3) +counter = Counter(t) +counter +``` + +```py +Counter({1: 3, 2: 2, 3: 1}) +``` + +`Counter` 对象类似于字典,它将每个键映射到该键出现的次数。与字典一样,键必须是可哈希的。 + +与字典不同,`Counter` 对象在访问不存在的元素时不会引发异常。相反,它会返回 `0`。 + +```py +counter['d'] +``` + +```py +0 +``` + +我们可以使用 `Counter` 对象来解决第十章的一个练习,该练习要求编写一个函数,接受两个单词并检查它们是否是字母异位词——即,一个单词的字母是否可以重新排列成另一个单词。 + +这是使用 `Counter` 对象的一个解决方案。 + +```py +def is_anagram(word1, word2): + return Counter(word1) == Counter(word2) +``` + +如果两个单词是字母异位词,它们包含相同的字母和相同的出现次数,因此它们的 `Counter` 对象是等价的。 + +`Counter` 提供了一个名为 `most_common` 的方法,它返回一个值-频率对的列表,按出现频率从高到低排序。 + +```py +counter.most_common() +``` + +```py +[(1, 3), (2, 2), (3, 1)] +``` + +它们还提供了方法和运算符来执行类似集合的操作,包括加法、减法、并集和交集。例如,`+` 运算符可以将两个 `Counter` 对象合并,创建一个新的 `Counter`,其中包含两个对象的键以及计数的和。 + +我们可以通过将 `'bans'` 中的字母制作成 `Counter`,并将其添加到 `'banana'` 中的字母来进行测试。 + +```py +counter2 = Counter('bans') +counter + counter2 +``` + +```py +Counter({1: 3, 2: 2, 3: 1, 'b': 1, 'a': 1, 'n': 1, 's': 1}) +``` + +你将有机会在本章末的练习中探索其他 `Counter` 操作。 + +## 18.3\. defaultdict + +`collections` 模块还提供了 `defaultdict`,它类似于字典,但如果访问一个不存在的键,它会自动生成一个新值。 + +创建 `defaultdict` 时,你提供一个函数,用于创建新值。创建对象的函数有时被称为**工厂函数**。内置的用于创建列表、集合等类型的函数可以作为工厂函数使用。 + +例如,下面是一个创建新 `list` 的 `defaultdict`。 + +```py +from collections import defaultdict + +d = defaultdict(list) +d +``` + +```py +defaultdict(list, {}) +``` + +请注意,参数是 `list`,它是一个类对象,而不是 `list()`,后者是一个函数调用,用来创建一个新列表。工厂函数只有在我们访问一个不存在的键时才会被调用。 + +```py +t = d['new key'] +t +``` + +```py +[] +``` + +新的列表,我们称之为`t`,也被添加到了字典中。因此,如果我们修改`t`,变动也会出现在`d`中: + +```py +t.append('new value') +d['new key'] +``` + +```py +['new value'] +``` + +如果你正在创建一个包含列表的字典,通常可以使用`defaultdict`编写更简洁的代码。 + +在第十一章的一个练习中,我创建了一个字典,将已排序的字母字符串映射到可以用这些字母拼写的单词列表。例如,字符串 `'opst'` 映射到列表 `['opts', 'post', 'pots', 'spot', 'stop', 'tops']`。这是原始代码。 + +```py +def all_anagrams(filename): + d = {} + for line in open(filename): + word = line.strip().lower() + t = signature(word) + if t not in d: + d[t] = [word] + else: + d[t].append(word) + return d +``` + +这是一个使用 `defaultdict` 的更简洁版本。 + +```py +def all_anagrams(filename): + d = defaultdict(list) + for line in open(filename): + word = line.strip().lower() + t = signature(word) + d[t].append(word) + return d +``` + +在章节末尾的练习中,你将有机会练习使用`defaultdict`对象。 + +```py +from collections import defaultdict + +d = defaultdict(list) +key = ('into', 'the') +d[key].append('woods') +d[key] +``` + +```py +['woods'] +``` + +## 18.4\. 条件表达式 + +条件语句通常用于选择两个值中的一个,例如这样: + +```py +if x > 0: + y = math.log(x) +else: + y = float('nan') +``` + +该语句检查 `x` 是否为正数。如果是,它会计算其对数。如果不是,`math.log` 会引发一个 ValueError。为了避免程序中断,我们生成一个 `NaN`,这是一个表示“非数字”的特殊浮点值。 + +我们可以通过**条件表达式**更简洁地编写这个语句。 + +```py +y = math.log(x) if x > 0 else float('nan') +``` + +你几乎可以像读英语一样读这行:“`y` 等于 log-`x`,如果 `x` 大于 0;否则它等于 `NaN`”。 + +递归函数有时可以通过条件表达式简洁地写出来。例如,这是一个带有条件*语句*的 `factorial` 版本。 + +```py +def factorial(n): + if n == 0: + return 1 + else: + return n * factorial(n-1) +``` + +这是一个带有条件*表达式*的版本。 + +```py +def factorial(n): + return 1 if n == 0 else n * factorial(n-1) +``` + +条件表达式的另一个用途是处理可选参数。例如,这是一个类定义,包含一个使用条件语句来检查带有默认值的参数的 `__init__` 方法。 + +```py +class Kangaroo: + def __init__(self, name, contents=None): + self.name = name + if contents is None: + contents = [] + self.contents = contents +``` + +这是一个使用条件表达式的版本。 + +```py +def __init__(self, name, contents=None): + self.name = name + self.contents = [] if contents is None else contents +``` + +一般来说,如果两个分支都包含单一的表达式且没有语句,可以用条件表达式替代条件语句。 + +## 18.5\. 列表推导式 + +在前几章中,我们已经看到一些例子,我们从一个空列表开始,并通过 `append` 方法逐个添加元素。例如,假设我们有一个包含电影标题的字符串,我们想要将所有单词的大写字母进行转换。 + +```py +title = 'monty python and the holy grail' +``` + +我们可以将其拆分成一个字符串列表,遍历这些字符串,进行大写转换,并将它们追加到一个列表中。 + +```py +t = [] +for word in title.split(): + t.append(word.capitalize()) + +' '.join(t) +``` + +```py +'Monty Python And The Holy Grail' +``` + +我们可以通过**列表推导式**更简洁地做同样的事情: + +```py +t = [word.capitalize() for word in title.split()] + +' '.join(t) +``` + +```py +'Monty Python And The Holy Grail' +``` + +方括号操作符表示我们正在构建一个新列表。括号内的表达式指定了列表的元素,`for` 子句指示我们正在循环遍历的序列。 + +列表推导式的语法可能看起来很奇怪,因为循环变量—在这个例子中是 `word`—出现在表达式中,而我们还没有看到它的定义。但你会习惯的。 + +另一个例子是,在第九章中,我们使用这个循环从文件中读取单词并将它们追加到列表中。 + +```py +word_list = [] + +for line in open('words.txt'): + word = line.strip() + word_list.append(word) +``` + +下面是我们如何将其写成列表推导式的方式。 + +```py +word_list = [line.strip() for line in open('words.txt')] +``` + +列表推导式也可以包含一个`if`子句,用来决定哪些元素会被包含在列表中。例如,这里是我们在第十章中使用的一个`for`循环,用于生成`word_list`中所有回文单词的列表。 + +```py +palindromes = [] + +for word in word_list: + if is_palindrome(word): + palindromes.append(word) +``` + +下面是我们如何用列表推导式做同样的事情。 + +```py +palindromes = [word for word in word_list if is_palindrome(word)] +``` + +当列表推导式作为函数的参数时,我们通常可以省略括号。例如,假设我们想要将\(1 / 2^n\)的值加总,其中\(n\)从 0 到 9。我们可以像这样使用列表推导式。 + +```py +sum([1/2**n for n in range(10)]) +``` + +```py +1.998046875 +``` + +或者我们可以像这样省略括号。 + +```py +sum(1/2**n for n in range(10)) +``` + +```py +1.998046875 +``` + +在这个例子中,参数严格来说是一个**生成器表达式**,而不是列表推导式,它实际上并没有创建一个列表。但除此之外,行为是一样的。 + +列表推导式和生成器表达式简洁且易于阅读,至少对于简单的表达式是如此。它们通常比等效的`for`循环更快,有时甚至快得多。所以,如果你生气我没有早点提到它们,我理解。 + +但为了我的辩护,列表推导式更难调试,因为你不能在循环内部放置`print`语句。我建议你仅在计算足够简单、你很可能第一次就能写对的情况下使用它们。或者考虑先编写并调试一个`for`循环,再将其转换为列表推导式。 + +## 18.6\. `any`和`all` + +Python 提供了一个内置函数`any`,它接受一个布尔值序列,并在其中任何一个值为`True`时返回`True`。 + +```py +any([False, False, True]) +``` + +```py +True +``` + +`any`通常与生成器表达式一起使用。 + +```py +any(letter == 't' for letter in 'monty') +``` + +```py +True +``` + +这个例子并不是很有用,因为它与`in`运算符做的事情相同。但我们可以使用`any`来为第七章中的一些练习写出简洁的解法。例如,我们可以像这样编写`uses_none`。 + +```py +def uses_none(word, forbidden): + """Checks whether a word avoids forbidden letters.""" + return not any(letter in forbidden for letter in word) +``` + +这个函数循环遍历`word`中的字母,检查其中是否有字母在`forbidden`中。使用`any`和生成器表达式的结合是高效的,因为一旦找到了`True`值,它就会立即停止,而不必遍历整个序列。 + +Python 提供了另一个内置函数`all`,它会在序列中的每个元素都为`True`时返回`True`。我们可以使用它来编写`uses_all`的简洁版本。 + +```py +def uses_all(word, required): + """Check whether a word uses all required letters.""" + return all(letter in word for letter in required) +``` + +使用`any`和`all`表达式可以简洁、高效且易于阅读。 + +## 18.7\. 命名元组 + +`collections`模块提供了一个名为`namedtuple`的函数,可以用来创建简单的类。例如,第十六章中的`Point`对象只有两个属性,`x`和`y`。以下是我们如何定义它的。 + +```py +class Point: + """Represents a point in 2-D space.""" + + def __init__(self, x, y): + self.x = x + self.y = y + + def __str__(self): + return f'({self.x}, {self.y})' +``` + +这段代码传达了少量信息却包含了很多代码。`namedtuple`提供了一种更简洁的方式来定义像这样的类。 + +```py +from collections import namedtuple + +Point = namedtuple('Point', ['x', 'y']) +``` + +第一个参数是你想创建的类的名称,第二个参数是`Point`对象应该拥有的属性列表。结果是一个类对象,这就是为什么它被赋值给一个首字母大写的变量名。 + +使用`namedtuple`创建的类提供了一个`__init__`方法,用于将值分配给属性,还有一个`__str__`方法,用于以可读的形式显示对象。所以我们可以像这样创建并显示一个`Point`对象。 + +```py +p = Point(1, 2) +p +``` + +```py +Point(x=1, y=2) +``` + +`Point`还提供了一个`__eq__`方法,用于检查两个`Point`对象是否相等——也就是说,它们的属性是否相同。 + +```py +p == Point(1, 2) +``` + +```py +True +``` + +你可以通过名称或索引访问命名元组的元素。 + +```py +p.x, p.y +``` + +```py +(1, 2) +``` + +```py +p[0], p[1] +``` + +```py +(1, 2) +``` + +你也可以将命名元组当作元组来使用,如下所示的赋值。 + +```py +x, y = p +x, y +``` + +```py +(1, 2) +``` + +但`namedtuple`对象是不可变的。属性初始化后,它们不能被更改。 + +```py +p[0] = 3 +``` + +```py +TypeError: 'Point' object does not support item assignment +``` + +```py +p.x = 3 +``` + +```py +AttributeError: can't set attribute +``` + +`namedtuple`提供了一种快速定义简单类的方法。缺点是简单类有时并不总是保持简单。你可能会决定稍后为命名元组添加方法。在这种情况下,你可以定义一个新类,从命名元组继承。 + +```py +class Pointier(Point): + """This class inherits from Point""" +``` + +或者到那时你可以切换到常规的类定义。 + +## 18.8\. 打包关键字参数 + +在第十一章中,我们写了一个函数,将它的参数打包成一个元组。 + +```py +def mean(*args): + return sum(args) / len(args) +``` + +你可以用任意数量的参数调用这个函数。 + +```py +mean(1, 2, 3) +``` + +```py +2.0 +``` + +但`*`运算符并不会打包关键字参数。因此,带有关键字参数调用此函数会导致错误。 + +```py +mean(1, 2, start=3) +``` + +```py +TypeError: mean() got an unexpected keyword argument 'start' +``` + +要打包关键字参数,我们可以使用`**`运算符: + +```py +def mean(*args, **kwargs): + print(kwargs) + return sum(args) / len(args) +``` + +关键字打包参数可以使用任何名称,但`kwargs`是常见的选择。结果是一个字典,它将关键字映射到对应的值。 + +```py +mean(1, 2, start=3) +``` + +```py +{'start': 3} +``` + +```py +1.5 +``` + +在这个例子中,`kwargs`的值被打印出来,但除此之外没有任何效果。 + +但`**`运算符也可以在参数列表中使用,用来解包字典。例如,这是一个`mean`的版本,它打包收到的任何关键字参数,然后将其解包为`sum`的关键字参数。 + +```py +def mean(*args, **kwargs): + return sum(args, **kwargs) / len(args) +``` + +现在,如果我们以`start`作为关键字参数调用`mean`,它会传递给`sum`,并作为求和的起始点。在下面的例子中,`start=3`在计算平均值之前将`3`加到总和中,所以总和是`6`,结果是`3`。 + +```py +mean(1, 2, start=3) +``` + +```py +3.0 +``` + +作为另一个例子,如果我们有一个包含`x`和`y`键的字典,我们可以使用解包运算符来创建一个`Point`对象。 + +```py +d = dict(x=1, y=2) +Point(**d) +``` + +```py +Point(x=1, y=2) +``` + +如果没有解包运算符,`d`将被视为单个位置参数,因此它被赋值给`x`,我们会得到一个`TypeError`,因为没有第二个参数可以赋值给`y`。 + +```py +d = dict(x=1, y=2) +Point(d) +``` + +```py +TypeError: Point.__new__() missing 1 required positional argument: 'y' +``` + +当你处理具有大量关键字参数的函数时,通常创建并传递指定常用选项的字典是很有用的。 + +```py +def pack_and_print(**kwargs): + print(kwargs) + +pack_and_print(a=1, b=2) +``` + +```py +{'a': 1, 'b': 2} +``` + +## 18.9\. 调试 + +在前面的章节中,我们使用`doctest`来测试函数。例如,这里有一个名为`add`的函数,它接受两个数字并返回它们的和。它包含一个`doctest`,检查`2 + 2`是否等于`4`。 + +```py +def add(a, b): + '''Add two numbers. + + >>> add(2, 2) + 4 + ''' + return a + b +``` + +这个函数接受一个函数对象并运行它的`doctests`。 + +```py +from doctest import run_docstring_examples + +def run_doctests(func): + run_docstring_examples(func, globals(), name=func.__name__) +``` + +所以我们可以像这样测试`add`函数。 + +```py +run_doctests(add) +``` + +没有输出,这意味着所有的测试都通过了。 + +Python 提供了另一种用于运行自动化测试的工具,称为`unittest`。它的使用稍微复杂一些,但这里有一个例子。 + +```py +from unittest import TestCase + +class TestExample(TestCase): + + def test_add(self): + result = add(2, 2) + self.assertEqual(result, 4) +``` + +首先,我们导入`TestCase`,这是`unittest`模块中的一个类。为了使用它,我们必须定义一个继承自`TestCase`的新类,并提供至少一个测试方法。测试方法的名称必须以`test`开头,并应表明它测试的是哪个函数。 + +在这个例子中,`test_add`通过调用`add`函数、保存结果,并调用`assertEqual`来测试`add`函数。`assertEqual`继承自`TestCase`,它接受两个参数并检查它们是否相等。 + +为了运行这个测试方法,我们必须运行`unittest`中的一个名为`main`的函数,并提供几个关键字参数。以下函数展示了详细信息——如果您有兴趣,可以向虚拟助手询问它是如何工作的。 + +```py +import unittest + +def run_unittest(): + unittest.main(argv=[''], verbosity=0, exit=False) +``` + +`run_unittest`不接受`TestExample`作为参数,而是查找继承自`TestCase`的类。然后,它查找以`test`开头的方法并运行它们。这个过程叫做**测试发现**。 + +下面是我们调用`run_unittest`时发生的情况。 + +```py +run_unittest() +``` + +```py +---------------------------------------------------------------------- +Ran 1 test in 0.000s + +OK +``` + +`unittest.main`报告它运行的测试数量和结果。在这种情况下,`OK`表示测试通过。 + +为了查看测试失败时发生了什么,我们将向`TestExample`添加一个错误的测试方法。 + +```py +%%add_method_to TestExample + + def test_add_broken(self): + result = add(2, 2) + self.assertEqual(result, 100) +``` + +下面是我们运行测试时发生的情况。 + +```py +run_unittest() +``` + +```py +====================================================================== +FAIL: test_add_broken (__main__.TestExample) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tmp/ipykernel_1109857/3833266738.py", line 3, in test_add_broken + self.assertEqual(result, 100) +AssertionError: 4 != 100 + +---------------------------------------------------------------------- +Ran 2 tests in 0.000s + +FAILED (failures=1) +``` + +报告包括失败的测试方法和显示失败位置的错误信息。总结部分表明有两个测试被运行,其中一个失败了。 + +在下面的练习中,我将建议一些提示,您可以用它们向虚拟助手询问关于`unittest`的更多信息。 + +## 18.10\. 术语表 + +**工厂:** 用于创建对象的函数,通常作为参数传递给其他函数。 + +**条件表达式:** 使用条件语句来选择两个值中的一个的表达式。 + +**列表推导式:** 一种简洁的方式来遍历序列并创建一个列表。 + +**生成器表达式:** 类似于列表推导式,但它不创建列表。 + +**测试发现:** 一种用于查找和运行测试的过程。 + +## 18.11\. 练习 + +```py +# This cell tells Jupyter to provide detailed debugging information +# when a runtime error occurs. Run it before working on the exercises. + +%xmode Verbose +``` + +### 18.11.1\. 向虚拟助手提问 + +本章有一些话题可能您会想了解。 + ++ “Python 的 set 类有哪些方法和操作符?” + ++ “Python 的 Counter 类有哪些方法和操作符?” + ++ “Python 的列表推导式和生成器表达式有什么区别?” + ++ “什么时候应该使用 Python 的`namedtuple`而不是定义一个新类?” + ++ “打包和解包关键字参数有什么用途?” + ++ “`unittest`是如何进行测试发现的?” + ++ “除了`assertEqual`,`unittest.TestCase`中最常用的方法有哪些?” + ++ “`doctest`和`unittest`的优缺点是什么?” + +对于以下练习,考虑请求虚拟助手的帮助,但如同往常一样,请记得测试结果。 + +### 18.11.2\. 练习 + +第七章中的一个练习要求编写一个名为`uses_none`的函数,它接受一个单词和一串禁用字母,如果单词中不使用任何禁用字母,则返回`True`。以下是一个解决方案。 + +```py +def uses_none(word, forbidden): + for letter in word.lower(): + if letter in forbidden.lower(): + return False + return True +``` + +编写这个函数的版本,使用`set`操作代替`for`循环。提示:询问虚拟助手,“如何计算 Python 集合的交集?” + +### 18.11.3\. 练习 + +拼字游戏是一种棋盘游戏,目标是使用字母瓦片拼写单词。例如,如果我们有字母瓦片`T`、`A`、`B`、`L`、`E`,我们可以拼出`BELT`和`LATE`,但是我们无法拼出`BEET`,因为我们没有两个`E`。 + +编写一个函数,接受一个字母字符串和一个单词,检查这些字母是否能拼出该单词,考虑每个字母出现的次数。 + +### 18.11.4\. 练习 + +在第十七章中的一个练习中,我对`has_straightflush`的解决方案使用了以下方法,它将`PokerHand`分成一个包含四手牌的列表,每手牌都包含相同花色的卡牌。 + +```py + def partition(self): + """Make a list of four hands, each containing only one suit.""" + hands = [] + for i in range(4): + hands.append(PokerHand()) + + for card in self.cards: + hands[card.suit].add_card(card) + + return hands +``` + +编写这个函数的简化版本,使用`defaultdict`。 + +### 18.11.5\. 练习 + +这是来自第十一章的一个计算斐波那契数的函数。 + +```py +def fibonacci(n): + if n == 0: + return 0 + + if n == 1: + return 1 + + return fibonacci(n-1) + fibonacci(n-2) +``` + +编写这个函数的版本,使用单个返回语句,使用两个条件表达式,其中一个嵌套在另一个内部。 + +### 18.11.6\. 练习 + +以下是一个递归计算二项式系数的函数。 + +```py +def binomial_coeff(n, k): + """Compute the binomial coefficient "n choose k". + + n: number of trials + k: number of successes + + returns: int + """ + if k == 0: + return 1 + + if n == 0: + return 0 + + return binomial_coeff(n-1, k) + binomial_coeff(n-1, k-1) +``` + +使用嵌套条件表达式重写函数主体。 + +这个函数的效率不高,因为它会不断计算相同的值。通过如第十章所述的记忆化方法,使其更高效。 + +```py +binomial_coeff(10, 4) # should be 210 +``` + +```py +210 +``` + +### 18.11.7\. 练习 + +这是来自第十七章中`Deck`类的`__str__`方法。 + +```py +%%add_method_to Deck + + def __str__(self): + res = [] + for card in self.cards: + res.append(str(card)) + return '\n'.join(res) +``` + +使用列表推导或生成器表达式编写这个方法的更简洁版本。 + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可证:[MIT 许可证](https://mit-license.org/) + +文本许可证:[知识共享署名-非商业性使用-相同方式共享 4.0 国际](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_21.md b/translations/zh/tnkpy3e_21.md new file mode 100644 index 0000000..24ac7d3 --- /dev/null +++ b/translations/zh/tnkpy3e_21.md @@ -0,0 +1,57 @@ +# 19. 最后的思考 + +> 原文:[`allendowney.github.io/ThinkPython/chap19.html`](https://allendowney.github.io/ThinkPython/chap19.html) + +学习编程并不容易,但如果你已经走到了这一步,你已经打下了一个良好的基础。现在,我有一些建议,可以帮助你继续学习并应用所学的知识。 + +本书旨在为编程提供一个通用的入门介绍,因此我们没有专注于具体应用。根据你的兴趣,使用你新学到的技能可以应用到任何领域。 + +如果你对数据科学感兴趣,我有三本书你可能会喜欢: + ++ *Think Stats: Exploratory Data Analysis*,O'Reilly Media,2014 年。 + ++ *Think Bayes: Bayesian Statistics in Python*,O'Reilly Media,2021 年。 + ++ *Think DSP: Digital Signal Processing in Python*,O'Reilly Media,2016 年。 + +如果你对物理建模和复杂系统感兴趣,你可能会喜欢: + ++ *Modeling and Simulation in Python: An Introduction for Scientists and Engineers*,No Starch Press,2023 年。 + ++ *Think Complexity: Complexity Science and Computational Modeling*,O'Reilly Media,2018 年。 + +这些书籍使用了 NumPy、SciPy、pandas 以及其他用于数据科学和科学计算的 Python 库。 + +本书试图在编程的通用原则和 Python 的细节之间找到平衡。因此,它并没有涵盖 Python 语言的所有特性。关于 Python 的更多内容以及使用它的良好建议,我推荐由 Luciano Ramalho 编写的*Fluent Python: Clear, Concise, and Effective Programming*,第二版,O'Reilly Media,2022 年。 + +在学习编程之后,一个常见的下一步是学习数据结构和算法。我目前正在进行相关工作,名为*Data Structures and Information Retrieval in Python*。该书的免费电子版可以在 Green Tea Press 网站上获得:[`greenteapress.com`](https://greenteapress.com)。 + +当你处理更复杂的程序时,你将会遇到新的挑战。你可能会觉得回顾本书中关于调试的章节很有帮助。特别是,记住第十二章中的调试六个 R 法则:阅读、运行、反思、橡皮鸭调试、撤退和休息。 + +本书建议了一些调试工具,包括`print`和`repr`函数,第十一章中的`structshape`函数,以及第十四章中的内建函数`isinstance`、`hasattr`和`vars`。 + +它还建议了一些用于测试程序的工具,包括`assert`语句、`doctest`模块和`unittest`模块。在程序中加入测试是预防和发现错误、节省调试时间的最佳方法之一。 + +但最好的调试方式是你根本不需要调试。如果你按照第六章中描述的增量开发过程进行开发,并且在过程中不断进行测试,你将会犯更少的错误,并且在出错时能更快找到它们。另外,记得第四章中提到的封装和泛化,尤其是在你在 Jupyter notebooks 中开发代码时特别有用。 + +在本书中,我提到了一些使用虚拟助手帮助你学习、编程和调试的方法。我希望这些工具对你有帮助。 + +除了像 ChatGPT 这样的虚拟助手,你可能还想使用像 Copilot 这样的工具,在你输入代码时自动完成。我最初没有推荐使用这些工具,因为它们对于初学者来说可能会让人感到不知所措。但现在你可以尝试一下。 + +有效使用 AI 工具需要一些实验和反思,以找到适合自己的工作方式。如果你觉得从 ChatGPT 复制代码到 Jupyter 很麻烦,你可能会更喜欢像 Copilot 这样的工具。但你用来编写提示和解释回应的认知工作,可能和工具生成的代码一样有价值,就像橡胶鸭调试一样。 + +随着你编程经验的积累,你可能想要探索其他开发环境。我认为 Jupyter notebook 是一个不错的起点,但它相对较新,并不像传统的集成开发环境(IDE)那样广泛使用。对于 Python,最流行的 IDE 包括 PyCharm 和 Spyder,还有 Thonny,通常推荐给初学者。其他 IDE,如 Visual Studio Code 和 Eclipse,也支持其他编程语言。或者,作为一种更简单的选择,你可以使用任何你喜欢的文本编辑器来编写 Python 程序。 + +在你继续编程之旅的过程中,你不必孤单前行!如果你住在城市里或附近,很可能有一个 Python 用户组可以加入。这些小组通常对初学者非常友好,所以不要害怕。如果你所在的地方没有这样的群体,你也许可以远程参与活动。另外,留意本地区的 Python 会议。 + +提高编程技能的最好方法之一就是学习另一种语言。如果你对统计学和数据科学感兴趣,你可能想学习 R。但是我特别推荐学习像 Racket 或 Elixir 这样的函数式编程语言。函数式编程需要一种不同的思维方式,这改变了你对程序的思考方式。 + +祝你好运! + +[Think Python: 第 3 版](https://allendowney.github.io/ThinkPython/index.html) + +版权所有 2024 [Allen B. Downey](https://allendowney.com) + +代码许可证:[MIT 许可证](https://mit-license.org/) + +文本许可证:[知识共享署名-非商业性使用-相同方式共享 4.0 国际](https://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/translations/zh/tnkpy3e_22.md b/translations/zh/tnkpy3e_22.md new file mode 100644 index 0000000..ecbcabf --- /dev/null +++ b/translations/zh/tnkpy3e_22.md @@ -0,0 +1,81 @@ +# 空白笔记本 + +> 原文:[`allendowney.github.io/ThinkPython/blank.html`](https://allendowney.github.io/ThinkPython/blank.html) + +对于每一章节,我创建了一个“空白”笔记本,保留了原始文本,但大部分代码已删除。这些笔记本非常适合进行跟随练习,学习者可以在其中填写空白部分。 + +**第一章:编程作为思维方式** + ++ [点击这里在 Colab 上运行第一章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap01.ipynb) + +**第二章:变量和语句** + ++ [点击这里在 Colab 上运行第二章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap02.ipynb) + +**第三章:函数** + ++ [点击这里在 Colab 上运行第三章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap03.ipynb) + +**第四章:函数和接口** + ++ [点击这里在 Colab 上运行第四章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap04.ipynb) + +**第五章:条件语句和递归** + ++ [点击这里在 Colab 上运行第五章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap05.ipynb) + +**第六章:返回值** + ++ [点击这里在 Colab 上运行第六章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap06.ipynb) + +**第七章:迭代和搜索** + ++ [点击这里在 Colab 上运行第七章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap07.ipynb) + +**第八章:字符串和正则表达式** + ++ [点击这里在 Colab 上运行第八章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap08.ipynb) + +**第九章:列表** + ++ [点击这里在 Colab 上运行第九章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap09.ipynb) + +**第十章:字典** + ++ [点击这里在 Colab 上运行第十章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap10.ipynb) + +**第十一章:元组** + ++ [点击这里在 Colab 上运行第十一章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap11.ipynb) + +**第十二章:文本分析与生成** + ++ [点击这里在 Colab 上运行第十二章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap12.ipynb) + +**第十三章:文件和数据库** + ++ [点击这里在 Colab 上运行第十三章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap13.ipynb) + +**第十四章:类和函数** + ++ [点击这里在 Colab 上运行第十四章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap14.ipynb) + +**第十五章:类和方法** + ++ [点击这里在 Colab 上运行第十五章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap15.ipynb) + +**第十六章:类和对象** + ++ [点击这里在 Colab 上运行第十六章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap16.ipynb) + +**第十七章:继承** + ++ [点击这里在 Colab 上运行第十七章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap17.ipynb) + +**第十八章:Python 扩展** + ++ [点击这里在 Colab 上运行第十八章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap18.ipynb) + +**第十九章:最终思考** + ++ [点击这里在 Colab 上运行第十九章](https://colab.research.google.com/github/AllenDowney/ThinkPython/blob/v3/blank/chap19.ipynb)