Python 编程思维第三版(一)

来源:allendowney.github.io/ThinkPython/

译者:飞龙

协议:CC BY-NC-SA 4.0

Think Python

原文:allendowney.github.io/ThinkPython/

Think Python 是一本为没有编程经验的人(或者曾经尝试过但觉得困难的人)介绍 Python 的书。你可以在 Bookshop.orgAmazon 购买第三版的纸质书和电子书。

这是本书在 Green Tea Press 的主页

第三版的主要变化有:

  • 本书现在完全使用 Jupyter 笔记本格式,你可以在同一个地方阅读文本、运行代码并完成练习。通过下面的链接,你可以在 Colab 上运行这些笔记本,所以你无需安装任何东西即可开始。

  • 这本书的内容经过了大量修订,部分章节顺序有所调整。现在有更多的练习,我认为许多练习都更好。

  • 每一章的结尾都有一些建议,推荐使用像 ChatGPT 和 Colab AI 这样的工具来深入学习并帮助完成练习。

这些笔记本

第一章:将编程作为一种思维方式

第二章:变量与语句

第三章:函数

第四章:函数与接口

第五章:条件语句与递归

第六章:返回值

第七章:迭代与搜索

第八章:字符串与正则表达式

第九章:列表

第十章:字典

第十一章:元组

第十二章:文本分析与生成

第十三章:文件与数据库

第十四章:类与函数

第十五章:类与方法

第十六章:类与对象

第十七章:继承

第十八章:Python 扩展

第十九章:总结思考

教师资源

如果你正在使用这本书进行教学,以下是一些你可能会觉得有用的资源。

  • 你可以从这个 GitHub 仓库下载包含解答的笔记本。

  • 每章的测验和整本书的总结性测验可以根据要求提供。

  • 使用 Jupyter 进行教学与学习 是一本在线书籍,提供了在课堂上有效使用 Jupyter 的建议。你可以在这里阅读这本书

  • 在课堂上使用笔记本的最佳方式之一是现场编程,教师编写代码,学生在自己的笔记本中跟随。要了解现场编程——以及关于教学编程的其他很多宝贵建议——我推荐由 The Carpentries 提供的教师培训,你可以在这里阅读

  • 对于每一章,我创建了一个包含原文的“空白”笔记本,但大部分代码已被删除。这些笔记本对于进行跟随练习非常有用,学习者可以在其中填写空白。空白笔记本的链接在这里

前言

原文:allendowney.github.io/ThinkPython/chap00.html

这本书适合谁?

如果你想学习编程,你来对地方了。Python 是初学者最好的编程语言之一,它也是目前最具需求的技能之一。

你也来得正是时候,因为现在学习编程可能比以往任何时候都要容易。借助像 ChatGPT 这样的虚拟助手,你不必孤单学习。在本书中,我将建议你如何使用这些工具加速学习。

本书主要面向从未编程过的人和那些有其他编程语言经验的人。如果你已经有了丰富的 Python 经验,可能会觉得前几章的进度太慢。

学习编程的一个挑战是你必须学习种语言:一种是编程语言本身,另一种是我们用来讨论程序的词汇。如果你只学习编程语言,当你需要解释错误信息、阅读文档、与他人交流或者使用虚拟助手时,你很可能会遇到问题。如果你已经做了一些编程,但没有学会这第二种语言,希望你能从本书中受益。

本书目标

在写这本书时,我尽力小心处理词汇。当一个术语首次出现时,我会对其进行定义。在每一章的末尾,还有一个术语表,回顾了本章中出现的术语。

我还尽量简明扼要。读书所需的思维努力越少,你就越能集中精力编程。

但是,你不能仅仅通过读书来学习编程——你必须实践。因此,本书在每一章的末尾都包括了练习,帮助你巩固所学内容。

如果你认真阅读并坚持完成练习,你会取得进展。但我要提醒你——学习编程并不容易,即使对于经验丰富的程序员来说,它也可能是令人沮丧的。随着学习的深入,我会建议一些策略,帮助你编写正确的程序并修复错误的程序。

导读

本书的每一章都是建立在前一章的基础上的,因此你应该按顺序阅读,并在继续之前花时间完成练习。

前六章介绍了基本的元素,如算术、条件语句和循环。它们还介绍了编程中最重要的概念——函数,以及使用函数的一种强大方法——递归。

第七章和第八章介绍了字符串——它们可以表示字母、单词和句子——以及与之相关的算法。

第 9 到第十二章介绍了 Python 的核心数据结构——列表、字典和元组——这些是编写高效程序的强大工具。第十二章介绍了分析文本和随机生成新文本的算法。像这样的算法是大型语言模型(LLM)的核心,因此本章将让你了解像 ChatGPT 这样的工具是如何工作的。

第十三章讲解了如何将数据存储到长期存储中——文件和数据库。作为练习,你可以编写一个程序,搜索文件系统并查找重复文件。

第 14 到第十七章介绍了面向对象编程(OOP),这是一种组织程序和它们处理的数据的方式。许多 Python 库是以面向对象的风格编写的,因此这些章节将帮助你理解它们的设计——并定义你自己的对象。

本书的目标不是覆盖整个 Python 语言。而是专注于语言的一个子集,这个子集能够以最少的概念提供最大的能力。尽管如此,Python 仍然有许多功能,可以帮助你高效地解决常见问题。第十八章介绍了其中的一些功能。

最后,第十九章分享了我离别时的想法,并给出了继续编程旅程的建议。

第三版有什么新内容?

本版中最大的变化是由两项新技术推动的——Jupyter 笔记本和虚拟助手。

本书的每一章都是一个 Jupyter 笔记本,它是一个包含普通文本和代码的文档。对我而言,这使得编写代码、测试代码并保持与文本一致变得更加容易。对你而言,这意味着你可以在一个地方运行代码、修改代码并完成练习。使用笔记本的说明在第一章中。

另一个大变化是,我增加了有关如何使用像 ChatGPT 这样的虚拟助手的建议,并利用它们加速学习。当本书的上一版在 2016 年出版时,这些工具的前身远不如现在有用,而且大多数人对此一无所知。如今,它们已成为软件工程的标准工具,我认为它们将成为学习编程——以及学习其他许多内容——的变革性工具。

书中的其他变化源于我对第二版的遗憾。

第一个遗憾是我没有强调软件测试。这在 2016 年时就已经是一个遗憾的遗漏,但随着虚拟助手的出现,自动化测试变得更加重要。因此,这一版介绍了 Python 最广泛使用的测试工具——doctestunittest,并包含了几个练习,你可以在其中练习使用这些工具。

我的另一个遗憾是第二版的练习不够均衡——有些比其他的更有趣,而有些则过于难。转向 Jupyter 笔记本帮助我开发并测试了一个更具吸引力和更有效的练习顺序。

在这次修订中,主题的顺序几乎没有变化,但我重新安排了一些章节,并将两个较短的章节合并为一个。此外,我扩展了字符串的内容,加入了正则表达式。

有些章节使用了海龟图形。在之前的版本中,我使用了 Python 的 turtle模块,但遗憾的是它在 Jupyter 笔记本中无法工作。所以我用一个新的海龟模块替换了它,应该更易于使用。

最后,我重写了大量的文本,澄清了需要澄清的地方,并删减了那些我可以更简洁表达的部分。

我对这个新版本感到非常自豪——希望你会喜欢!

入门指南

对于大多数编程语言,包括 Python,你可以使用许多工具来编写和运行程序。这些工具被称为集成开发环境(IDEs)。一般来说,IDE 有两种类型:

  • 一些 IDE 与包含代码的文件一起工作,因此它们提供了编辑和运行这些文件的工具。

  • 其他 IDE 主要与笔记本一起使用,笔记本是包含文本和代码的文档。

对于初学者,我推荐从像 Jupyter 这样的笔记本开发环境开始。

本书的笔记本可以从allendowney.github.io/ThinkPython上的在线仓库获得。

有两种使用方式:

  • 你可以下载笔记本并在自己的计算机上运行。在这种情况下,你需要安装 Python 和 Jupyter,这并不难,但如果你想学习 Python,花费大量时间安装软件可能会让人感到沮丧。

  • 另一种选择是通过 Colab 运行笔记本,Colab 是一个在网页浏览器中运行的 Jupyter 环境,因此你无需安装任何东西。Colab 由 Google 运营,且免费使用。

如果你刚开始学习,我强烈建议你从 Colab 开始。

教师资源

如果你是用本书进行教学,以下是一些你可能会觉得有用的资源。

  • 你可以在allendowney.github.io/ThinkPython找到带有习题解答的笔记本,并附有以下额外资源的链接。

  • 每章的测验和整本书的总结性测验可以根据请求提供。

  • Jupyter 的教学与学习是一本在线书籍,提供了在课堂上有效使用 Jupyter 的建议。你可以在jupyter4edu.github.io/jupyter-edu-book阅读这本书。

  • 使用笔记本的最佳方式之一是现场编码,教师编写代码,学生在自己的笔记本中跟随。要了解现场编码并获取关于编程教学的其他建议,我推荐 The Carpentries 提供的讲师培训,网址是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 版

版权所有 2024 Allen B. Downey

代码许可:MIT 许可证

文本许可:创用 CC 姓名标示-非商业性使用-相同方式共享 4.0 国际版

1. 编程作为一种思维方式

原文:allendowney.github.io/ThinkPython/chap01.html

本书的第一个目标是教你如何使用 Python 编程。但是,学习编程意味着学习一种新的思维方式,因此本书的第二个目标是帮助你像计算机科学家一样思考。这种思维方式结合了数学、工程和自然科学的一些最佳特征。像数学家一样,计算机科学家使用形式化语言来表示思想——特别是表示计算。像工程师一样,他们设计事物,将组件组装成系统,并在替代方案之间进行权衡。像科学家一样,他们观察复杂系统的行为,提出假设,并测试预测。

我们将从编程的最基本元素开始,逐步深入。在本章中,我们将了解 Python 如何表示数字、字母和单词。你还将学会执行算术运算。

你还将开始学习编程的词汇,包括运算符、表达式、值和类型等术语。这个词汇非常重要——你需要它来理解本书的其他内容,与其他程序员沟通,并使用和理解虚拟助手。

1.1. 算术运算符

算术运算符是表示算术计算的符号。例如,加号+表示加法运算。

30 + 12 
42 

减号-是执行减法的运算符。

43 - 1 
42 

星号*表示乘法运算。

6 * 7 
42 

斜杠/表示除法运算:

84 / 2 
42.0 

注意,除法的结果是42.0而不是42。这是因为 Python 中有两种类型的数字:

  • 整数,表示没有分数或小数部分的数字,以及

  • 浮点数,表示整数和带有小数点的数字。

如果你对两个整数进行加法、减法或乘法运算,结果是一个整数。但是如果你对两个整数进行除法运算,结果是一个浮动小数。Python 提供了另一个运算符//,用于执行整数除法。整数除法的结果始终是一个整数。

84 // 2 
42 

整数除法也被称为“地板除法”,因为它总是向下舍入(朝向“地板”)。

85 // 2 
42 

最后,运算符**执行指数运算;也就是说,它将一个数字提升到一个幂:

7 ** 2 
49 

在某些其他语言中,插入符号^用于表示指数运算,但在 Python 中,它是一个位运算符,称为 XOR。如果你不熟悉位运算符,结果可能会让你感到意外:

7 ^ 2 
5 

我不会在本书中介绍位运算符,但你可以在wiki.python.org/moin/BitwiseOperators阅读相关内容。

1.2. 表达式

运算符和数字的集合叫做表达式。一个表达式可以包含任意数量的运算符和数字。例如,下面是一个包含两个运算符的表达式。

6 + 6 ** 2 
42 

请注意,指数运算优先于加法。Python 遵循你在数学课上学到的运算顺序:指数运算优先于乘法和除法,而乘法和除法又优先于加法和减法。

在下面的例子中,乘法发生在加法之前。

12 + 5 * 6 
42 

如果你希望加法先发生,可以使用括号。

(12 + 5) * 6 
102 

每个表达式都有一个。例如,表达式 6 * 7 的值是 42

1.3. 算术函数

除了算术运算符,Python 还提供了一些函数,可以与数字一起使用。例如,round 函数接受一个浮动点数字,并将其四舍五入到最接近的整数。

round(42.4) 
42 
round(42.6) 
43 

abs 函数计算一个数字的绝对值。对于正数,绝对值就是数字本身。

abs(42) 
42 

对于负数,绝对值是正数。

abs(-42) 
42 

当我们使用像这样的函数时,我们说我们在调用该函数。调用函数的表达式叫做函数调用

当你调用函数时,括号是必须的。如果不加括号,会得到错误信息。

abs 42 
 Cell In[17], line 1
    abs 42
        ^
SyntaxError: invalid syntax 

你可以忽略这条消息的第一行,它现在不包含任何我们需要理解的信息。第二行是包含错误的代码,下面有一个插入符号 (^),指示发现错误的位置。

最后一行表明这是一个语法错误,意味着表达式的结构有问题。在这个例子中,问题是函数调用需要括号。

让我们看看如果你省略括号以及值会发生什么。

abs 
<function abs(x, /)> 

函数名本身就是一个合法的表达式,具有一个值。当它被显示时,值表示 abs 是一个函数,并包含一些稍后我会解释的附加信息。

1.4. 字符串

除了数字,Python 还可以表示字母的序列,这些序列被称为字符串,因为字母像珠子一样串在一起。要写一个字符串,我们可以将字母序列放在直引号内。

'Hello' 
'Hello' 

使用双引号也是合法的。

"world" 
'world' 

双引号使得写包含撇号的字符串变得容易,因为撇号和直引号是相同的符号。

"it's a small " 
"it's a small " 

字符串也可以包含空格、标点符号和数字。

'Well, ' 
'Well, ' 

+ 运算符可以与字符串一起使用;它将两个字符串连接成一个字符串,这个操作称为连接

'Well, ' + "it's a small " + 'world.' 
"Well, it's a small world." 

* 运算符也可以与字符串一起使用;它可以制作字符串的多个副本并将它们连接起来。

'Spam, ' * 4 
'Spam, Spam, Spam, Spam, ' 

其他的算术运算符不能与字符串一起使用。

Python 提供了一个名为 len 的函数,用于计算字符串的长度。

len('Spam') 
4 

注意,len 计算引号之间的字母数,但不包括引号本身。

当你创建一个字符串时,请确保使用直引号。反引号,也称为反引号,会导致语法错误。

`Hello` 
 Cell In[26], line 1
    `Hello`
    ^
SyntaxError: invalid syntax 

智能引号,也称为卷曲引号,也是非法的。

1.5. 值和类型

到目前为止,我们已经看到了三种类型的值:

  • 2 是一个整数,

  • 42.0 是一个浮点数,而

  • 'Hello' 是一个字符串。

一种值被称为类型。每个值都有一个类型 - 或者我们有时说它“属于”一个类型。

Python 提供了一个名为 type 的函数,可以告诉你任何值的类型。整数的类型是 int

type(2) 
int 

浮点数的类型是 float

type(42.0) 
float 

字符串的类型是 str

type('Hello, World!') 
str 

类型 intfloatstr 可以被用作函数。例如,int 可以接受一个浮点数并将其转换为整数(总是向下取整)。

int(42.9) 
42 

float 可以将整数转换为浮点数值。

float(42) 
42.0 

现在,这里有一些可能让人困惑的东西。如果你把一串数字放在引号中会得到什么呢?

'126' 
'126' 

它看起来像一个数字,但实际上它是一个字符串。

type('126') 
str 

如果你尝试像数字一样使用它,可能会得到一个错误。

'126' / 3 
TypeError: unsupported operand type(s) for /: 'str' and 'int' 

这个例子会生成一个 TypeError,这意味着表达式中的值(称为操作数)具有错误的类型。错误消息表明 / 运算符不支持这些值的类型,它们分别为 strint

如果你有一个包含数字的字符串,你可以使用 int 将其转换为整数。

int('126') / 3 
42.0 

如果你有一个包含数字和小数点的字符串,你可以使用 float 将其转换为浮点数。

float('12.6') 
12.6 

当你写一个大整数时,你可能会尝试在数字组之间使用逗号,例如 1,000,000。这在 Python 中是一个合法的表达式,但结果不是一个整数。

1,000,000 
(1, 0, 0) 

Python 解释 1,000,000 为一个逗号分隔的整数序列。我们稍后会学到更多关于这种序列的知识。

你可以使用下划线使大数更易于阅读。

1_000_000 
1000000 

1.6. 形式语言和自然语言

自然语言 是人们说的语言,如英语、西班牙语和法语。它们不是由人类设计的;它们是自然演变而来的。

形式语言 是由人类设计用于特定应用的语言。例如,数学家使用的符号是一种形式语言,非常擅长表示数字和符号之间的关系。类似地,编程语言是被设计用来表达计算的形式语言。

尽管形式语言和自然语言有一些共同点,但它们之间存在重要的区别:

  • 歧义:自然语言充满了歧义,人们通过使用上下文线索和其他信息来处理它。形式语言旨在几乎或完全无歧义,这意味着任何程序具有完全确定的意义,无论上下文如何。

  • 冗余性:为了弥补歧义并减少误解,自然语言使用冗余。因此,它们通常很冗长。正式语言不那么冗余,更加简洁。

  • 文字的字面性:自然语言充满成语和隐喻。正式语言确切地表达其意思。

因为我们都是用自然语言长大的,有时很难适应正式语言。正式语言比自然语言更为密集,因此阅读起来需要更多时间。此外,结构也很重要,所以不总是从上到下、从左到右阅读最佳。最后,细节至关重要。在自然语言中,你可以忽略拼写和标点的小错误,但在正式语言中,这可能造成很大差异。

1.7. 调试

程序员会犯错误。出于一些奇怪的原因,编程错误被称为bug,追踪它们的过程称为debugging

编程,尤其是调试,有时会激发强烈的情绪。如果你在解决一个困难的 bug,你可能会感到愤怒、悲伤或尴尬。

准备好应对这些反应可能有助于你应对它们。一种方法是把计算机看作是一个具有某些优势(如速度和精度)和特定弱点(如缺乏同理心和无法把握全局图景)的员工。

你的工作是成为一名优秀的经理:找到利用优势和减少弱点的方法。找到利用你的情绪参与问题解决的方法,同时不让你的反应影响你有效工作的能力。

学习调试可能很令人沮丧,但这是一种有价值的技能,对编程之外的许多活动都很有用。在每章的末尾,都会有一个像这样的部分,提供关于调试的建议。希望它们有所帮助!

1.8. 术语表

算术运算符: 符号,如+*,表示加法或乘法等算术运算。

整数: 一种表示没有分数或小数部分的数字的类型。

浮点数: 一种表示整数和带小数部分的数字的类型。

整数除法: 一个运算符,//,用于将两个数字相除并向下取整至整数。

表达式: 变量、值和运算符的组合。

值: 整数、浮点数或字符串 - 或者我们稍后将看到的其他类型的值。

函数: 执行某些有用操作的一系列语句的命名序列。函数可能需要参数,也可能不需要,并且可能产生结果,也可能不产生。

函数调用: 一个表达式或表达式的一部分,运行一个函数。它由函数名后跟括号中的参数列表组成。

语法错误: 程序中的错误,使其无法解析 - 因此也无法运行。

字符串: 一种表示字符序列的类型。

concatenation: 将两个字符串连接在一起。

type: 一类值。我们到目前为止见过的类型有整数(类型 int)、浮点数(类型 float)和字符串(类型 str)。

operand: 操作符作用的值之一。

natural language: 人们说的自然演变而来的任何语言。

formal language: 人们为特定目的设计的任何语言,例如表示数学思想或计算机程序。所有编程语言都是形式语言。

bug: 程序中的错误。

debugging: 查找和修正错误的过程。

1.9. 练习

# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.

%xmode Verbose 
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 会做什么?答案是它有时会向上舍入,有时会向下舍入。试试这些例子,看看你能不能弄清楚它遵循什么规则。

round(42.5) 
42 
round(43.5) 
44 

如果你感兴趣,可以问虚拟助手:“如果一个数字以 0.5 结尾,Python 会向上还是向下舍入?”

1.9.3. 练习

当你学习新特性时,应该尝试自己动手并故意犯错。这样,你会学到错误信息的含义,当你再次看到它们时就能理解它们。这比在后期偶然犯错要好。

  1. 你可以使用负号来表示负数,例如 -2。如果你在一个数字前面加上加号会发生什么?那 2++2 呢?

  2. 如果你有两个值中间没有运算符,比如4 2,会发生什么?

  3. 如果你调用一个函数像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 秒是多少秒?

  2. 10 公里是多少英里?提示:1 英里等于 1.61 公里。

  3. 如果你在 42 分钟 42 秒内跑完 10 公里,你的平均配速是多少(单位:每英秒)?

  4. 你的平均配速是多少(单位:分钟和秒每英里)?

  5. 你的平均速度是多少(单位:英里每小时)?

如果你已经了解了变量,你可以在这个练习中使用它们。如果你不懂,也可以做这个练习,然后我们会在下一章介绍它们。

Think Python: 第三版

版权所有 2024 Allen B. Downey

代码许可:MIT 许可

文本许可:知识共享署名-非商业性使用-相同方式共享 4.0 国际

2. 变量与语句

原文:allendowney.github.io/ThinkPython/chap02.html

在上一章中,我们使用运算符编写了执行算术计算的表达式。

在本章中,你将学习关于变量和语句、import语句以及print函数的知识。我还将介绍更多我们用来讨论程序的词汇,包括“参数”和“模块”。

2.1. 变量

变量是指向某个值的名称。要创建一个变量,我们可以像这样写一个赋值语句

n = 17 

一个赋值语句有三个部分:左边是变量名,等号操作符=,右边是表达式。在这个示例中,表达式是一个整数。在以下示例中,表达式是一个浮动小数。

pi = 3.141592653589793 

在以下示例中,表达式是一个字符串。

message = 'And now for something completely different' 

当你执行赋值语句时,没有输出。Python 会创建变量并赋予它一个值,但赋值语句没有可见的效果。然而,在创建变量后,你可以将其作为表达式使用。因此我们可以这样显示message的值:

message 
'And now for something completely different' 

你还可以将变量用作包含算术运算符的表达式的一部分。

n + 25 
42 
2 * pi 
6.283185307179586 

你还可以在调用函数时使用变量。

round(pi) 
3 
len(message) 
42 

2.2. 状态图

在纸面上表示变量的常见方式是写下变量名,并画一个箭头指向它的值。

[外链图片转存中…(img-DYho23UJ-1748168063762)]

这种图形被称为状态图,因为它展示了每个变量的状态(可以把它看作是变量的“心理状态”)。我们将在全书中使用状态图来表示 Python 如何存储变量及其值的模型。

2.3. 变量名

变量名可以任意长。它们可以包含字母和数字,但不能以数字开头。使用大写字母是合法的,但通常约定变量名使用小写字母。

变量名中唯一可以出现的标点符号是下划线字符_。它通常用于多个单词的名字中,例如your_nameairspeed_of_unladen_swallow

如果给变量一个非法的名称,就会得到语法错误。million!是非法的,因为它包含了标点符号。

million!  =  1000000 
 Cell In[12], line 1
    million!  =  1000000
           ^
SyntaxError: invalid syntax 

76trombones是非法的,因为它以数字开头。

76trombones = 'big parade' 
 Cell In[13], line 1
  76trombones = 'big parade'
     ^
SyntaxError: invalid decimal literal 

class也是非法的,但可能不太明显为什么。

class = 'Self-Defence Against Fresh Fruit' 
 Cell In[14], line 1
    class = 'Self-Defence Against Fresh Fruit'
          ^
SyntaxError: invalid syntax 

事实证明,class是一个关键字,是用来指定程序结构的特殊词汇。关键字不能用作变量名。

这是 Python 关键字的完整列表:

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 模块。

import math 

模块是一个包含变量和函数的集合。数学模块提供了一个叫做pi的变量,包含了数学常数(\pi)的值。我们可以像这样显示它的值。

math.pi 
3.141592653589793 

要在模块中使用变量,你必须在模块名称和变量名称之间使用点操作符.)。

数学模块还包含函数。例如,sqrt 计算平方根。

math.sqrt(25) 
5.0 

pow 将一个数字提升为第二个数字的幂。

math.pow(5, 2) 
25.0 

到目前为止,我们已经看到两种将数字提升为幂的方法:我们可以使用 math.pow 函数或指数运算符 **。两者都可以,但运算符的使用频率比函数更高。

2.5. 表达式和语句

到目前为止,我们已经看到几种类型的表达式。一个表达式可以是一个单独的值,比如整数、浮点数或字符串。它也可以是一个包含值和运算符的集合。它还可以包括变量名和函数调用。这是一个包含这些元素的表达式。

19 + n + round(math.pi) * 2 
42 

我们也见过几种类型的语句。语句是一个有作用但没有值的代码单元。例如,一个赋值语句创建一个变量并赋予它一个值,但语句本身没有值。

n = 17 

同样,导入语句也有一个作用——它导入一个模块,以便我们可以使用它包含的变量和函数——但它没有可见的效果。

import math 

计算表达式的值称为求值。执行语句称为执行

2.6. print 函数

当你求值一个表达式时,结果会被显示出来。

n + 1 
18 

但是,如果你计算多个表达式,只有最后一个表达式的值会被显示。

n + 2
n + 3 
20 

要显示多个值,你可以使用 print 函数。

print(n+2)
print(n+3) 
19
20 

它同样适用于浮点数和字符串。

print('The value of pi is approximately')
print(math.pi) 
The value of pi is approximately
3.141592653589793 

你还可以使用由逗号分隔的表达式序列。

print('The value of pi is approximately', math.pi) 
The value of pi is approximately 3.141592653589793 

注意,print 函数会在值之间添加空格。

2.7. 参数

当你调用一个函数时,括号中的表达式被称为参数。通常我会解释为什么,但是在这种情况下,术语的技术含义几乎与词汇的常见含义无关,所以我就不尝试了。

到目前为止,我们看到的一些函数只接受一个参数,像 int

int('101') 
101 

有些需要两个参数,像 math.pow

math.pow(5, 2) 
25.0 

有些可以接受额外的可选参数。例如,int 可以接受一个第二个参数,指定数字的基数。

int('101', 2) 
5 

二进制中的数字序列101表示十进制中的数字 5。

round还可以接受一个可选的第二个参数,表示四舍五入的位数。

round(math.pi, 3) 
3.142 

一些函数可以接受任意数量的参数,比如print

print('Any', 'number', 'of', 'arguments') 
Any number of arguments 

如果你调用一个函数并提供了太多的参数,那也是一个TypeError

float('123.0', 2) 
TypeError: float expected at most 1 argument, got 2 

如果你提供了太少的参数,那也是一个TypeError

math.pow(2) 
TypeError: pow expected 2 arguments, got 1 

如果你提供了一个类型函数无法处理的参数,那也是一个TypeError

math.sqrt('123') 
TypeError: must be real number, not str 

在开始时进行这种检查可能会让人烦恼,但它有助于你发现和修正错误。

2.8. 注释

随着程序变得越来越大和复杂,它们变得更难阅读。正式的编程语言是密集的,通常很难看一段代码就明白它在做什么以及为什么这么做。

因此,最好在程序中添加注释,用自然语言解释程序在做什么。这些注释被称为注释,以#符号开头。

# number of seconds in 42:42
seconds = 42 * 60 + 42 

在这种情况下,注释会单独出现在一行上。你也可以将注释放在一行的末尾:

miles = 10 / 1.61     # 10 kilometers in miles 

#到行末的所有内容都会被忽略——它对程序的执行没有影响。

注释在记录代码中不明显的特性时最有用。可以合理假设读者能够弄明白代码做了什么;更有用的是解释为什么代码这么做。

这个注释与代码重复,毫无用处:

v = 8     # assign 8 to v 

这个注释包含了代码中没有的信息:

v = 8     # velocity in miles per hour 

良好的变量名可以减少注释的需要,但过长的名字可能会让复杂的表达式难以阅读,因此需要做出取舍。

2.9. 调试

程序中可能发生三种类型的错误:语法错误、运行时错误和语义错误。区分它们很有用,这样可以更快地定位问题。

  • 语法错误:“语法”是指程序的结构以及关于该结构的规则。如果程序中的任何地方存在语法错误,Python 不会运行程序,而是立即显示一条错误信息。

  • 运行时错误:如果程序中没有语法错误,它就可以开始运行。但如果发生错误,Python 会显示一条错误信息并停止运行。这种错误被称为运行时错误,也叫做异常,因为它表示发生了某些异常情况。

  • 语义错误:第三种类型的错误是“语义”错误,即与含义相关的错误。如果程序中存在语义错误,它会运行,但不会生成错误信息,而且它不会按你预期的方式工作。识别语义错误可能很棘手,因为它需要你通过查看程序的输出,反向推理出程序在做什么。

如我们所见,非法的变量名是语法错误。

million!  =  1000000 
 Cell In[40], line 1
    million!  =  1000000
           ^
SyntaxError: invalid syntax 

如果你使用了不支持的操作符类型,这就是一个运行时错误。

'126' / 3 
TypeError: unsupported operand type(s) for /: 'str' and 'int' 

最后,这里有一个语义错误的例子。假设我们想要计算13的平均值,但我们忽略了操作顺序,写成了这样:

1 + 3 / 2 
2.5 

当此表达式被求值时,它不会产生错误信息,因此没有语法错误或运行时错误。但结果不是13的平均值,因此程序不正确。这是一个语义错误,因为程序运行了,但没有达到预期的效果。

2.10. 术语表

变量: 一个代表值的名称。

赋值语句: 给变量赋值的语句。

状态图: 一组变量及其引用值的图形表示。

关键字: 用来指定程序结构的特殊词语。

导入语句: 读取模块文件,使我们可以使用其中的变量和函数的语句。

模块: 一个包含 Python 代码的文件,包括函数定义,有时还包括其他语句。

点操作符: 用于通过指定模块名称后跟点和函数名来访问另一个模块中的函数的操作符

求值: 执行表达式中的操作以计算值。

语句: 一行或多行代码,表示一个命令或操作。

执行: 运行一个语句并按照它的指示操作。

参数: 在调用函数时提供给函数的值。

注释: 程序中包含的文本,提供关于程序的信息,但对程序执行没有影响。

运行时错误: 导致程序显示错误信息并退出的错误。

异常: 在程序运行时检测到的错误。

语义错误: 一个错误,导致程序执行不正确,但不会显示错误信息。

2.11. 练习

# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.

%xmode Verbose 
Exception reporting mode: Verbose 

2.11.1. 向虚拟助手提问

再次鼓励你使用虚拟助手来了解本章中的任何主题。

如果你对我列出的任何关键字感到好奇,你可以问:“为什么 class 是一个关键字?”或“为什么变量名不能是关键字?”

你可能注意到,intfloatstr不是 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.cosmath.sin计算(x)的正弦和余弦,以及它们平方的和。

结果应该接近 1。它可能不是精确的 1,因为浮点运算并不完全准确——它只是近似正确的。

第三部分。 除了pi,在math模块中定义的另一个变量是e,它代表自然对数的底数,数学符号表示为(e)。如果你不熟悉这个值,可以问虚拟助手“math.e是什么?”现在让我们通过三种方法计算(e²):

  • 使用math.e和指数运算符(**)。

  • 使用math.powmath.e的值提高到2的幂。

  • 使用math.exp,它接受一个参数(x),并计算(e^x)。

你可能注意到,最后一个结果与其他两个略有不同。看看你能不能找出哪个是正确的。

《Think Python: 第三版》

版权 2024 Allen B. Downey

代码许可证:MIT 许可证

文本许可证:知识共享署名-非商业性使用-相同方式共享 4.0 国际版

3. 函数

原文:allendowney.github.io/ThinkPython/chap03.html

在上一章中,我们使用了 Python 提供的几个函数,比如 intfloat,以及 math 模块提供的一些函数,如 sqrtpow。在这一章中,你将学习如何创建自己的函数并运行它们。我们还将展示一个函数如何调用另一个函数。作为示例,我们将展示《蒙提·派森》歌曲的歌词。这些搞笑的例子展示了一个重要特性——编写自己函数的能力是编程的基础。

本章还介绍了一个新的语句——for 循环,它用于重复计算。

3.1. 定义新函数

函数定义指定了一个新函数的名称以及在调用该函数时运行的语句序列。下面是一个例子:

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。任何合法的变量名也是合法的函数名。

函数名后面的空括号表示该函数不接受任何参数。

函数定义的第一行叫做 头部,其余部分称为 函数体。头部必须以冒号结束,函数体必须缩进。按惯例,缩进通常使用四个空格。这个函数的体部分包含两个打印语句;通常,函数体可以包含任意数量的语句。

定义一个函数会创建一个 函数对象,我们可以像这样显示它。

print_lyrics 
<function __main__.print_lyrics()> 

输出表明 print_lyrics 是一个不接受任何参数的函数。__main__ 是包含 print_lyrics 的模块名。

现在我们已经定义了一个函数,我们可以像调用内建函数一样调用它。

print_lyrics() 
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day. 

当函数运行时,它会执行函数体中的语句,这些语句会显示《伐木工歌》的前两行。

3.2. 参数

我们看到的一些函数需要参数;例如,当你调用 abs 时,你传递一个数字作为参数。一些函数需要多个参数;例如,math.pow 需要两个参数,一个是底数,另一个是指数。

这是一个接收参数的函数定义。

def print_twice(string):
    print(string)
    print(string) 

括号中的变量名是一个 参数。当调用函数时,参数会被赋予实参的值。例如,我们可以这样调用 print_twice

print_twice('Dennis Moore, ') 
Dennis Moore, 
Dennis Moore, 

运行这个函数的效果与将参数赋值给参数变量,然后执行函数体相同,如下所示。

string = 'Dennis Moore, '
print(string)
print(string) 
Dennis Moore, 
Dennis Moore, 

你也可以使用一个变量作为参数。

line = 'Dennis Moore, '
print_twice(line) 
Dennis Moore, 
Dennis Moore, 

在这个例子中,line 的值被赋给了参数 string

3.3. 调用函数

一旦定义了一个函数,你就可以在另一个函数中使用它。为了演示,我们将编写打印《Spam 歌》歌词的函数(www.songfacts.com/lyrics/monty-python/the-spam-song)。

Spam,Spam,Spam,Spam,

Spam,Spam,Spam,Spam,

Spam,Spam,

(可爱的 Spam,神奇的 Spam!)

Spam,Spam,

我们从以下函数开始,它接受两个参数。

def repeat(word, n):
    print(word * n) 

我们可以使用这个函数来打印歌曲的第一行,像这样。

spam = 'Spam, '
repeat(spam, 4) 
Spam, Spam, Spam, Spam, 

为了显示前两行,我们可以定义一个新的函数,使用repeat

def first_two_lines():
    repeat(spam, 4)
    repeat(spam, 4) 

然后像这样调用它。

first_two_lines() 
Spam, Spam, Spam, Spam, 
Spam, Spam, Spam, Spam, 

为了显示最后三行,我们可以定义另一个函数,这个函数同样使用repeat

def last_three_lines():
    repeat(spam, 2)
    print('(Lovely Spam, Wonderful Spam!)')
    repeat(spam, 2) 
last_three_lines() 
Spam, Spam, 
(Lovely Spam, Wonderful Spam!)
Spam, Spam, 

最后,我们可以通过一个函数将所有内容组合起来,打印出整首诗。

def print_verse():
    first_two_lines()
    last_three_lines() 
print_verse() 
Spam, Spam, Spam, Spam, 
Spam, Spam, Spam, Spam, 
Spam, Spam, 
(Lovely Spam, Wonderful Spam!)
Spam, Spam, 

当我们运行print_verse时,它调用了first_two_lines,而first_two_lines又调用了repeatrepeat则调用了print。这涉及了很多函数。

当然,我们本可以用更少的函数做同样的事情,但这个示例的重点是展示函数如何协同工作。

3.4. 重复

如果我们想显示多于一段的歌词,可以使用for语句。下面是一个简单的示例。

for i in range(2):
    print(i) 
0
1 

第一行是以冒号结尾的头部。第二行是主体,需要缩进。

头部以关键字for开始,后面跟着一个名为i的新变量和另一个关键字in。它使用range函数创建一个包含两个值的序列,这两个值分别是01。在 Python 中,当我们开始计数时,通常是从0开始的。

for语句运行时,它将range中的第一个值赋给i,然后在主体中运行print函数,显示0

当程序执行到主体末尾时,它会回到头部,这就是为什么这个语句被称为循环。第二次进入循环时,它将range中的下一个值赋给i并显示出来。然后,由于这是range中的最后一个值,循环结束。

这是我们如何使用for循环打印歌曲的两段歌词。

for i in range(2):
    print("Verse", i)
    print_verse()
    print() 
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的参数,该参数必须是整数,并显示给定数量的诗句。

def print_n_verses(n):
    for i in range(n):
        print_verse()
        print() 

在这个例子中,我们没有在循环主体中使用i,但头部仍然需要有一个变量名。

3.5. 变量和参数是局部的

当你在函数内部创建一个变量时,它是局部的,意味着它只在函数内部存在。例如,下面的函数接受两个参数,将它们连接起来并打印结果两次。

def cat_twice(part1, part2):
    cat = part1 + part2
    print_twice(cat) 

这是一个使用它的示例:

line1 = 'Always look on the '
line2 = 'bright side of life.'
cat_twice(line1, line2) 
Always look on the bright side of life.
Always look on the bright side of life. 

cat_twice运行时,它会创建一个名为cat的局部变量,而该变量在函数结束时被销毁。如果我们尝试显示它,就会得到一个NameError

print(cat) 
NameError: name 'cat' is not defined 

在函数外部,cat是未定义的。

参数也是局部的。例如,在cat_twice外部,没有part1part2这样的东西。

3.6. 堆栈图

为了跟踪哪些变量可以在哪些地方使用,有时画一个堆栈图会很有用。像状态图一样,堆栈图展示了每个变量的值,但它们还展示了每个变量所属的函数。

每个函数都由一个框架表示。框架是一个外面写着函数名称、里面包含函数参数和局部变量的框。

这是上一个例子的堆栈图。

_images/02b6ddc296c3c51396cc7c1a916aa9f4ea1bc5ed61b9fe10d6ec63e9b928fc68.png

这些框架按照堆栈的顺序排列,表示哪个函数调用了哪个函数,依此类推。从底部开始,printprint_twice调用,print_twicecat_twice调用,cat_twice__main__调用——这是最上层框架的一个特殊名称。当你在任何函数外部创建一个变量时,它属于__main__

print的框架中,问号表示我们不知道参数的名称。如果你感到好奇,可以问虚拟助手:“Python 的 print 函数的参数是什么?”

3.7. 追踪栈

当函数中发生运行时错误时,Python 会显示正在运行的函数的名称、调用它的函数的名称,依此类推,直到堆栈的顶部。为了看到一个例子,我将定义一个包含错误的print_twice版本——它试图打印cat,这是另一个函数中的局部变量。

def print_twice(string):
    print(cat)            # NameError
    print(cat) 

现在让我们来看一下运行cat_twice时会发生什么。

# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs, including a traceback.

%xmode Verbose 
Exception reporting mode: Verbose 
cat_twice(line1, line2) 
---------------------------------------------------------------------------
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. 练习

# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.

%xmode Verbose 
Exception reporting mode: Verbose 

3.11.1. 向虚拟助手提问

函数或for循环中的语句按照约定缩进四个空格。但并非所有人都同意这一约定。如果你对这一伟大的争论的历史感到好奇,可以让虚拟助手“告诉我关于 Python 中的空格和制表符”。

虚拟助手在编写小函数方面非常擅长。

  1. 请让你喜欢的虚拟助手“编写一个名为 repeat 的函数,它接收一个字符串和一个整数,并将该字符串打印指定的次数。”

  2. 如果结果使用了for循环,你可以问:“能不能不用for循环?”

  3. 从本章中任选一个其他函数,并请虚拟助手编写它。挑战在于准确描述函数,以便得到你想要的结果。使用你在本书中学到的词汇。

虚拟助手在调试函数方面也非常擅长。

  1. 询问 VA,这个print_twice版本有什么问题。

    def print_twice(string):
        print(cat)
        print(cat) 
    

如果您在以下任何练习中遇到困难,请考虑向 VA 寻求帮助。

3.11.2. 练习

编写一个名为print_right的函数,它以名为text的字符串作为参数,并打印字符串,使得字符串的最后一个字母位于显示的第 40 列。

提示:使用len函数、字符串连接运算符(+)和字符串重复运算符(*)。

这里有一个示例展示它应该如何工作。

print_right("Monty")
print_right("Python's")
print_right("Flying Circus") 
 Monty
                                Python's
                           Flying Circus 

3.11.3. 练习

编写一个名为triangle的函数,它接受一个字符串和一个整数,并绘制一个具有给定高度的金字塔,由字符串的副本组成。这里有一个使用字符串'L'的 5 级金字塔的示例。

triangle('L', 5) 
L
LL
LLL
LLLL
LLLLL 

3.11.4. 练习

编写一个名为rectangle的函数,它接受一个字符串和两个整数,并绘制一个具有给定宽度和高度的矩形,由字符串的副本组成。这里有一个宽度为5,高度为4的矩形的示例,由字符串'H'组成。

rectangle('H', 5, 4) 
HHHHH
HHHHH
HHHHH
HHHHH 

3.11.5. 练习

歌曲“99 瓶啤酒”以这首诗歌开始:

墙上有 99 瓶啤酒

99 瓶啤酒

拿一个下来,传递它

墙上有 98 瓶啤酒

然后第二节是一样的,只是从 98 瓶开始,以 97 结束。歌曲会继续——很长时间——直到没有啤酒为止。

编写一个名为bottle_verse的函数,它以一个数字作为参数,并显示以给定数量的瓶子开头的诗句。

提示:考虑从能够打印诗歌的第一、第二或最后一行的函数开始,然后使用它来编写bottle_verse

使用这个函数调用来显示第一节。

bottle_verse(99) 
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函数。

for n in range(99, 0, -1):
    bottle_verse(n)
    print() 

Think Python: 3rd Edition

版权所有 2024 年 Allen B. Downey

代码许可:MIT 许可证

文本许可证:知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

4. 函数与接口

原文:allendowney.github.io/ThinkPython/chap04.html

本章介绍了一个名为jupyturtle的模块,它允许你通过给一只虚拟的海龟下指令来创建简单的图形。我们将使用这个模块编写绘制正方形、多边形和圆形的函数,并演示接口设计,这是一种设计能协同工作的函数的方式。

4.1. jupyturtle 模块

要使用jupyturtle模块,我们可以这样导入它。

import jupyturtle 

现在我们可以使用模块中定义的函数,如make_turtleforward

jupyturtle.make_turtle()
jupyturtle.forward(100) 

make_turtle创建一个画布,这是屏幕上可以绘图的区域,并且创建一只海龟,海龟通过一个圆形的壳和一个三角形的头来表示。圆形表示海龟的位置,三角形表示它面朝的方向。

forward指令让海龟沿着它面朝的方向移动指定的距离,并在此过程中绘制一条线段。这个距离使用的是任意单位,实际的大小取决于你的计算机屏幕。

我们将多次使用jupyturtle模块中定义的函数,所以如果我们每次都不必写模块名,那就更方便了。如果我们像这样导入模块,这样做是可能的。

from jupyturtle import make_turtle, forward 

这个版本的导入语句从jupyturtle模块导入了make_turtleforward,这样我们就可以像这样调用它们。

make_turtle()
forward(100) 

jupyturtle还提供了另外两个函数,我们将使用它们,分别是leftright。我们将这样导入它们。

from jupyturtle import left, right 

left指令使海龟向左转动。它接受一个参数,表示转动的角度,单位是度。例如,我们可以这样使海龟左转 90 度。

make_turtle()
forward(50)
left(90)
forward(50) 

这个程序使海龟先向东移动然后向北移动,留下了两条线段。在继续之前,试试看能否修改之前的程序,绘制一个正方形。

4.2. 绘制一个正方形

这是绘制正方形的一种方式。

make_turtle()

forward(50)
left(90)

forward(50)
left(90)

forward(50)
left(90)

forward(50)
left(90) 

因为这个程序会重复执行相同的一对语句四次,我们可以通过for循环来更简洁地实现同样的效果。

make_turtle()
for i in range(4):
    forward(50)
    left(90) 

4.3. 封装与泛化

让我们将上一节的绘制正方形的代码放到一个名为square的函数里。

def square():
    for i in range(4):
        forward(50)
        left(90) 

现在我们可以这样调用这个函数。

make_turtle()
square() 

将一段代码封装到一个函数中叫做封装。封装的好处之一是它为代码附上了一个名称,这可以作为一种文档说明。另一个好处是,如果你要重复使用这段代码,调用函数比复制粘贴函数体要简洁得多!

在当前版本中,正方形的大小始终是50。如果我们想绘制不同大小的正方形,可以将边长作为参数传入。

def square(length):
    for i in range(4):
        forward(length)
        left(90) 

现在我们可以绘制不同大小的正方形了。

make_turtle()
square(30)
square(60) 

向函数添加一个参数叫做泛化,因为它使得函数变得更加通用:在之前的版本中,正方形的大小总是一样的;而在这个版本中,它可以是任意大小。

如果我们添加另一个参数,我们可以使它更通用。以下函数绘制具有给定边数的规则多边形。

def polygon(n, length):
    angle = 360 / n
    for i in range(n):
        forward(length)
        left(angle) 

在一个具有n条边的规则多边形中,相邻边之间的角度是360 / n度。

以下示例绘制一个具有 7 条边、边长为 30 的多边形。

make_turtle()
polygon(7, 30) 

当一个函数有很多数值型参数时,很容易忘记它们是什么,或者它们应该按什么顺序排列。一个好主意是,在参数列表中包含参数的名称。

make_turtle()
polygon(n=7, length=30) 

这些有时被称为“命名参数”,因为它们包括了参数名称。但在 Python 中,它们更常被称为关键字参数(不要与 Python 中的保留字如fordef混淆)。

这种赋值运算符=的使用提醒我们参数和参数列表的工作方式——当你调用一个函数时,实参会被赋值给形参。

4.4. 近似圆形

现在假设我们要画一个圆。我们可以通过画一个边数非常多的多边形来近似画圆,这样每一条边足够小,几乎看不见。这里有一个函数,使用polygon绘制一个具有30条边的多边形,近似一个圆。

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 倍。

make_turtle(delay=0.02)
circle(30) 

这个解决方案的一个局限性是n是一个常量,这意味着对于非常大的圆,边太长了,而对于小圆,我们浪费时间绘制非常短的边。一个选择是通过将n作为参数来泛化这个函数。但现在我们暂时保持简单。

4.5. 重构

现在让我们写一个更通用的circle版本,叫做arc,它接受第二个参数angle,并绘制一个跨度为给定角度的圆弧。例如,如果angle360度,它绘制一个完整的圆。如果angle180度,它绘制一个半圆。

为了编写circle,我们能够重用polygon,因为多边形的边数多时是圆的一个良好近似。但我们不能用polygon来编写arc

相反,我们将创建polygon的更通用版本,叫做polyline

def polyline(n, length, angle):
    for i in range(n):
        forward(length)
        left(angle) 

polyline接受三个参数:要绘制的线段数n,线段长度length,以及它们之间的角度angle

现在,我们可以重写polygon来使用polyline

def polygon(n, length):
    angle = 360.0 / n
    polyline(n, length, angle) 

我们可以使用polyline来绘制arc

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

def circle(radius):
    arc(radius,  360) 

为了检查这些函数是否按预期工作,我们将用它们画出像蜗牛一样的图形。使用delay=0时,海龟运行得尽可能快。

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

请注意,polyline中的angle值与arc中的angle值不同。参数是局部的,这意味着你可以在不同的函数中使用相同的参数名;它在每个函数中都是一个不同的变量,并且可能指向不同的值。

4.7. 开发计划

开发计划是编写程序的过程。我们在这一章中使用的过程是“封装与泛化”。这个过程的步骤如下:

  1. 首先编写一个没有函数定义的小程序。

  2. 一旦你让程序正常工作,找出其中的一个连贯部分,将其封装成一个函数并为其命名。

  3. 通过添加适当的参数来泛化函数。

  4. 重复步骤 1 到 3,直到你有一组有效的函数。

  5. 寻找通过重构改进程序的机会。例如,如果你在多个地方有相似的代码,考虑将它提取到一个适当的通用函数中。

这个过程有一些缺点——稍后我们会看到一些替代方法——但是如果你事先不知道如何将程序分解成函数,它是有用的。这个方法让你在编写过程中逐步设计。

函数的设计有两个部分:

  • 接口是指函数的使用方式,包括它的名称、它接受的参数以及它应该做什么。

  • 实现是指函数如何完成其预定的任务。

例如,这是我们编写的第一个版本的circle,它使用了polygon

def circle(radius):
    circumference = 2 * math.pi * radius
    n = 30
    length = circumference / n
    polygon(n, length) 

这是使用arc的重构版本。

def circle(radius):
    arc(radius,  360) 

这两个函数有相同的接口——它们接受相同的参数并做相同的事情——但它们的实现不同。

4.8. 文档字符串

文档字符串是函数开头的字符串,用于解释接口(“doc”是“documentation”的缩写)。下面是一个例子:

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. 练习

# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.

%xmode Verbose 
Exception reporting mode: Verbose 

对于以下练习,可能有一些额外的海龟函数你可能想要使用。

  • penup将海龟的虚拟笔抬起,这样它在移动时不会留下轨迹。

  • pendown将笔放下。

以下函数使用penuppendown来移动海龟而不留下轨迹。

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,绘制一个具有平行边的四边形。然后重写rectanglerhombus,使其使用parallelogram

4.11.4. 练习

编写一组适当通用的函数,能够绘制像这样的形状。

[外链图片转存中…(img-4KR03gKn-1748168063765)]

提示:编写一个名为triangle的函数,绘制一个三角形段,然后编写一个名为draw_pie的函数,使用triangle

4.11.5. 练习

编写一组适当通用的函数,能够绘制像这样的花朵。

提示:使用arc编写一个名为petal的函数,绘制一片花瓣。

4.11.6. 请虚拟助手帮忙

Python 中有几个像jupyturtle这样的模块,我们在本章中使用的模块是为本书定制的。所以如果你请虚拟助手帮忙,它可能不知道使用哪个模块。但如果你给它一些示例,它应该能够弄明白。例如,试试这个提示,看看它能否写出一个绘制螺旋的函数:

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 版

版权所有 2024 Allen B. Downey

代码许可:MIT License

文字许可:Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International

5. 条件语句与递归

原文:allendowney.github.io/ThinkPython/chap05.html

本章的主要主题是if语句,根据程序的状态执行不同的代码。通过if语句,我们将能够探索计算机科学中的一个强大概念——递归

但我们将从三个新特性开始:取模运算符、布尔表达式和逻辑运算符。

5.1. 整数除法与取模

回想一下,整数除法运算符//将两个数字相除并向下舍入为整数。例如,假设一部电影的放映时间是 105 分钟,你可能想知道这是多少小时。常规除法返回一个浮动数值:

minutes = 105
minutes / 60 
1.75 

但是我们通常不写带小数点的小时数。整数除法返回整数小时数,并向下舍入:

minutes = 105
hours = minutes // 60
hours 
1 

要得到余数,你可以用分钟形式减去一小时:

remainder = minutes - hours * 60
remainder 
45 

或者你可以使用取模运算符%,它将两个数字相除并返回余数。

remainder = minutes % 60
remainder 
45 

取模运算符比看起来更有用。例如,它可以检查一个数字是否能被另一个数字整除——如果x % y为零,那么x能被y整除。

同时,它还可以提取数字的最右边一位或几位。例如,x % 10返回x的最右一位数字(以十进制表示)。类似地,x % 100返回最后两位数字。

x = 123
x % 10 
3 
x % 100 
23 

最后,取模运算符可以进行“钟表算术”。例如,如果一个事件从上午 11 点开始并持续三小时,我们可以使用取模运算符来计算它结束的时间。

start = 11
duration = 3
end = (start + duration) % 12
end 
2 

该事件将于下午 2 点结束。

5.2. 布尔表达式

布尔表达式是一个值为真或假的表达式。例如,下面的表达式使用了等于运算符==,它比较两个值,如果它们相等则返回True,否则返回False

5 == 5 
True 
5 == 7 
False 

一个常见的错误是使用单个等号(=)而不是双等号(==)。记住,=是将值赋给变量,而==是比较两个值。

x = 5
y = 7 
x == y 
False 

TrueFalse是属于bool类型的特殊值;它们不是字符串:

type(True) 
bool 
type(False) 
bool 

==运算符是关系运算符之一;其他的有:

x != y               # x is not equal to y 
True 
x > y                # x is greater than y 
False 
x < y               # x is less than to y 
True 
x >= y               # x is greater than or equal to y 
False 
x <= y               # x is less than or equal to y 
True 

5.3. 逻辑运算符

要将布尔值组合成表达式,我们可以使用逻辑运算符。最常见的有andornot。这些运算符的意义与它们在英语中的含义相似。例如,下面的表达式的值为True,当且仅当x大于0 并且小于10

x > 0 and x < 10 
True 

如果任一或两个条件为真,下面的表达式的值为True,即如果数字能被 2 3 整除:

x % 2 == 0 or x % 3 == 0 
False 

最后,not运算符否定一个布尔表达式,因此如果x > yFalse,下面的表达式将是True

not x > y 
True 

严格来说,逻辑运算符的操作数应该是布尔表达式,但 Python 并不严格。任何非零数字都会被解释为True

42 and True 
True 

这种灵活性可能很有用,但它有一些细节可能让人困惑。你可能会想避免使用它。

5.4. if 语句

为了编写有用的程序,我们几乎总是需要能够检查条件,并根据条件改变程序的行为。条件语句赋予我们这个能力。最简单的形式是if语句:

if x > 0:
    print('x is positive') 
x is positive 

if是 Python 的关键字。if语句的结构与函数定义相同:一个头部,后跟一个缩进的语句或语句序列,称为

if后面的布尔表达式称为条件。如果条件为真,缩进块中的语句会执行。如果条件为假,则不执行。

在块中可以包含任意数量的语句,但必须至少包含一个。有时,创建一个什么也不做的块是有用的——通常是作为你还未编写代码的占位符。在这种情况下,你可以使用pass语句,它什么也不做。

if x < 0:
    pass          # TODO: need to handle negative values! 

注释中的TODO字样是一个约定,提醒你稍后需要做某事。

5.5. else子句

一个if语句可以有第二部分,称为else子句。语法如下:

if x % 2 == 0:
    print('x is even')
else:
    print('x is odd') 
x is odd 

如果条件为真,则执行第一个缩进的语句;否则,执行第二个缩进的语句。

在这个例子中,如果x是偶数,那么x除以2的余数是0,所以条件为真,程序显示x is even。如果x是奇数,则余数是1,条件为假,程序显示x is odd

由于条件必须为真或假,最终只有一个分支会被执行。分支被称为分支

5.6. 链式条件语句

有时可能存在多于两个的可能性,需要更多的分支。表达这种计算的一种方式是链式条件语句,它包含一个elif子句。

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') 
x is less than y 

elif是“else if”的缩写。elif子句的数量没有限制。如果有else子句,它必须位于最后,但不必存在。

每个条件都会按顺序检查。如果第一个条件为假,则检查下一个,以此类推。如果其中一个条件为真,则执行相应的分支,if语句结束。即使有多个条件为真,也只有第一个为真的分支会执行。

5.7. 嵌套条件语句

一个条件语句也可以嵌套在另一个条件语句中。我们可以像这样重新编写前一节中的例子:

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') 
x is less than y 

外部的if语句包含了两个分支。第一个分支包含一个简单的语句。第二个分支包含另一个if语句,它有自己的两个分支。那两个分支都是简单语句,尽管它们也可以是条件语句。

尽管语句的缩进使得结构变得清晰,嵌套条件语句可能仍然难以阅读。我建议你尽量避免使用它们。

逻辑运算符通常提供了一种简化嵌套条件语句的方法。这里是一个包含嵌套条件的例子。

if 0 < x:
    if x < 10:
        print('x is a positive single-digit number.') 
x is a positive single-digit number. 

只有当我们通过了两个条件判断,print语句才会执行,因此我们可以通过and运算符获得相同的效果。

if 0 < x and x < 10:
    print('x is a positive single-digit number.') 
x is a positive single-digit number. 

对于这种情况,Python 提供了一种更简洁的选项:

if 0 < x < 10:
    print('x is a positive single-digit number.') 
x is a positive single-digit number. 

5.8. 递归

一个函数调用自身是合法的。虽然它为什么是个好事可能不那么显而易见,但事实证明,这是程序能够做的最神奇的事情之一。这里有一个例子。

def countdown(n):
    if n <= 0:
        print('Blastoff!')
    else:
        print(n)
        countdown(n-1) 

如果n为 0 或负数,countdown输出单词“Blastoff!”否则,它输出n,然后调用自身,传递n-1作为参数。

这是当我们以参数3调用此函数时发生的情况。

countdown(3) 
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=1countdown返回。

得到n=2countdown返回。

得到n=3countdown返回。

一个调用自身的函数是递归的。作为另一个例子,我们可以写一个函数,打印字符串n次。

def print_n_times(string, n):
    if n > 0:
        print(string)
        print_n_times(string, n-1) 

如果n为正数,print_n_times会显示string的值,然后调用自身,传递stringn-1作为参数。

如果n0或负数,条件为假,print_n_times什么也不做。

下面是它的工作原理。

print_n_times('Spam ', 4) 
Spam 
Spam 
Spam 
Spam 

对于像这样的简单例子,可能使用for循环会更容易。但稍后我们会看到一些使用for循环很难编写而递归容易编写的例子,所以早点开始学习递归是有益的。

5.9. 递归函数的栈图

这是一个栈图,展示了当我们用n=3调用countdown时创建的框架。

[外链图片转存中…(img-IJeYs7HY-1748168063766)]

四个countdown框架的参数n值各不相同。栈底部,即n=0的地方,称为基准情况。它不再做递归调用,因此没有更多的框架。

5.10. 无限递归

如果递归永远无法到达基准情况,它将不断进行递归调用,程序也永远不会结束。这被称为无限递归,通常来说,这种情况是不推荐的。下面是一个包含无限递归的最小函数。

def recurse():
    recurse() 

每当recurse被调用时,它会调用自己,这样就会创建另一个栈帧。在 Python 中,栈上同时存在的栈帧数量是有限制的。如果程序超出了这个限制,就会导致运行时错误。

recurse() 
---------------------------------------------------------------------------
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,它会暂停程序并等待用户输入。当用户按下ReturnEnter键时,程序恢复执行,input会返回用户输入的内容作为字符串。

text = input() 

在获取用户输入之前,你可能想要显示一个提示,告诉用户应该输入什么。input可以接受一个提示作为参数:

name = input('What...is your name?\n')
name 
What...is your name?
It is Arthur, King of the Britons 
'It is Arthur, King of the Britons' 

提示末尾的序列\n表示换行符,它是一个特殊字符,导致换行——这样用户的输入就会显示在提示的下方。

如果你期望用户输入一个整数,可以使用int函数将返回值转换为int

prompt = 'What...is the airspeed velocity of an unladen swallow?\n'
speed = input(prompt)
speed 
What...is the airspeed velocity of an unladen swallow?
What do you mean: an African or European swallow? 
'What do you mean: an African or European swallow?' 

但如果用户输入了非整数的内容,你将得到一个运行时错误。

int(speed) 
ValueError: invalid literal for int() with base 10: 'What do you mean: an African or European swallow?' 

我们将在后面学习如何处理这种类型的错误。

5.12. 调试

当出现语法错误或运行时错误时,错误消息包含了大量信息,但可能会让人感到不知所措。通常最有用的部分是:

  • 错误的类型是什么,以及

  • 错误发生的位置。

语法错误通常很容易找到,但也有一些陷阱。与空格和制表符相关的错误可能会很棘手,因为它们是不可见的,而我们习惯于忽略它们。

x = 5
 y = 6 
 Cell In[49], line 2
    y = 6
    ^
IndentationError: unexpected indent 

在这个例子中,问题在于第二行缩进了一个空格。但是错误信息指向了y,这很具有误导性。错误消息指示问题被发现的位置,但实际的错误可能出现在代码的更早部分。

运行时错误也有类似情况。例如,假设你尝试将一个比率转换为分贝,如下所示:

import math
numerator = 9
denominator = 10
ratio = numerator // denominator
decibels = 10 * math.log10(ratio) 
---------------------------------------------------------------------------
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. 词汇表

递归: 调用当前正在执行的函数的过程。

取模运算符: 一个运算符%,用于整数,并返回一个数字除以另一个数字后的余数。

布尔表达式: 其值为TrueFalse的表达式。

关系运算符: 用于比较操作数的运算符:==!=><>=<=

逻辑运算符: 用于组合布尔表达式的运算符,包括andornot

条件语句: 根据某些条件控制执行流程的语句。

条件: 条件语句中的布尔表达式,决定执行哪个分支。

代码块: 一个或多个缩进的语句,表示它们是另一个语句的一部分。

分支: 条件语句中的一个替代执行语句序列。

链式条件: 具有一系列替代分支的条件语句。

嵌套条件: 出现在另一个条件语句分支中的条件语句。

递归: 调用自身的函数就是递归的。

基本情况: 递归函数中的一个条件分支,不进行递归调用。

无限递归: 没有基本情况或永远达不到基本情况的递归。最终,无限递归会导致运行时错误。

换行符: 在字符串的两个部分之间创建换行的字符。

5.14. 练习

# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.

%xmode Verbose 
Exception reporting mode: Verbose 

5.14.1. 向虚拟助手提问

  • 向虚拟助手询问:“取模运算符有什么用途?”

  • Python 提供了运算符来计算逻辑操作andornot,但它没有计算排他性or操作的运算符,通常写作xor。向助手询问:“什么是逻辑xor操作,我如何在 Python 中计算它?”

在本章中,我们看到了两种写三分支if语句的方法,使用链式条件或嵌套条件。你可以使用虚拟助手将它们相互转换。例如,问虚拟助手:“将这条语句转换为链式条件。”

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') 
x is less than y 

向虚拟助手询问:“用一个条件重写这条语句。”

if 0 < x:
    if x < 10:
        print('x is a positive single-digit number.') 
x is a positive single-digit number. 

看看虚拟助手是否能简化这个不必要的复杂性。

if not x <= 0 and not x >= 10:
    print('x is a positive single-digit number.') 
x is a positive single-digit number. 

这是一个尝试递归的函数,它以 2 为步长倒数。

def countdown_by_two(n):
    if n == 0:
        print('Blastoff!')
    else:
        print(n)
        countdown_by_two(n-2) 

看起来它能正常工作。

countdown_by_two(6) 
6
4
2
Blastoff! 

但它有一个错误。询问虚拟助手问题出在哪里,以及如何修复它。将它提供的解决方案粘贴回来并进行测试。

5.14.2. 练习

time模块提供了一个名为time的函数,它返回自“Unix 纪元”(1970 年 1 月 1 日 00:00:00 UTC 协调世界时)以来的秒数。

from time import time

now = time()
now 
1716394001.8466134 

使用整数除法和取模运算符来计算自 1970 年 1 月 1 日以来的天数,并且计算当前的时、分、秒。

你可以在docs.python.org/3/library/time.html上阅读更多关于time模块的信息。

5.14.3. 练习

如果你给定了三根棍子,你可能无法将它们排列成三角形。例如,如果其中一根棍子长 12 英寸,而其他两根棍子长 1 英寸,你就无法让短棍在中间相遇。对于任何三条边,都有一个测试来判断是否可以形成三角形:

如果三条边中的任何一条大于其他两条边的和,那么就不能形成三角形。否则,可以形成三角形。(如果两条边的和等于第三条边,它们就形成了所谓的“退化”三角形。)

编写一个名为is_triangle的函数,接受三个整数作为参数,并根据是否能够从给定长度的棍子中形成三角形,打印“是”或“否”。提示:使用链式条件。

5.14.4. 练习

以下程序的输出是什么?绘制一个堆栈图,展示程序打印结果时的状态。

def recurse(n, s):
    if n == 0:
        print(s)
    else:
        recurse(n-1, n+s)

recurse(3, 0) 
6 

5.14.5. 练习

以下练习使用了第四章中描述的jupyturtle模块。

阅读以下函数,看看你能否弄清楚它的作用。然后运行它,看看你是否理解正确。调整lengthanglefactor的值,观察它们对结果的影响。如果你不确定自己理解其工作原理,可以尝试问一个虚拟助手。

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的科赫曲线。

  2. 向左转 60 度。

  3. 画一个长度为x/3的科赫曲线。

  4. 向右转 120 度。

  5. 画一个长度为x/3的科赫曲线。

  6. 向左转 60 度。

  7. 画一个长度为x/3的科赫曲线。

例外情况是如果x小于5——在这种情况下,你可以直接画一条长度为x的直线。

编写一个名为koch的函数,接受x作为参数,并绘制给定长度的科赫曲线。

结果应如下所示:

make_turtle(delay=0)
koch(120) 

5.14.7. 练习

虚拟助理知道jupyturtle模块中的功能,但这些功能有许多版本,名称不同,因此助理可能不知道你在谈论哪个版本。

解决这个问题的方法是,在提问之前,你可以提供额外的信息。例如,你可以以“这是一个使用jupyturtle模块的程序”开头,然后粘贴本章节中的一个示例。之后,助理应该能够生成使用此模块的代码。

例如,向助理询问绘制谢尔宾斯基三角形的程序。你得到的代码应该是一个很好的起点,但你可能需要进行一些调试。如果第一次尝试不起作用,你可以告诉助理发生了什么,并请求帮助,或者自行调试。

这是结果的大致样子,尽管你得到的版本可能会有所不同。

make_turtle(delay=0, height=200)

draw_sierpinski(100, 3) 

《Think Python:第三版》

版权 2024 Allen B. Downey

代码许可:MIT 许可证

文本许可:知识共享署名-非商业性使用-相同方式共享 4.0 国际

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值