Python 编程:从入门到实践(第2版)· 语法基础部分
本地教材转换稿,来源:
F:\学习资料\Python编程:从入门到实践(第2版).pdf。
仅保留第一部分(第 1-11 章)的正文文字,原书插图与版式未保留;
用途:为「Python 算法与蓝桥杯」科目补齐 Python 基础语法(函数、推导式、内置函数等)。
<!-- p42 -->
<!-- p43 -->
第 1 章 起步
在本章中,你将运行自己的第一个程序——
hello_world.py。为此,你首先需要检查自己的计算机是否安装
了较新版本的Python;如果没有,就要安装它。你还要安装一个
文本编辑器,用于编写和运行Python程序。你输入Python代码
时,这个文本编辑器能够识别它们并突出显示不同的部分,让你
能够轻松地了解代码的结构。
<!-- p44 -->
1.1 搭建编程环境
在不同的操作系统中,Python存在细微的差别,因此有几点你需要牢
记在心。本节将确保你的系统正确安装Python。
1.1.1 P y t h o n 版本
每种编程语言都会随着新概念和新技术的推出而不断发展,Python开
发者也在一直致力于丰富和强化其功能。本书编写期间的最新版本为
Python 3.7,但只要你安装了Python 3.6或更高的版本,就能运行本
书中的所有代码。在本节中,你将核实系统是否安装了Python,以及
是否需要安装更新的版本。附录A提供了详尽的指南,指导你在各种主
流操作系统中安装最新版本的Python。
有些较老的Python项目依然使用Python 2,但你应该使用Python 3。
如果你的系统安装了Python 2,很可能是为了支持系统需要的一些旧
程序。你应保留它,并安装更新的版本以便学习本书。
1.1.2 运行 P y t h o n 代码片段
Python自带一个在终端窗口中运行的解释器,让你无须保存并运行整
个程序就能尝试运行Python代码片段。
本书将以如下方式列出代码片段:
❶ >>> print("Hello Python interpreter!")
Hello Python interpreter!
提示符>>> 表明正在使用终端窗口,而加粗的文本表示需要你输入之
后按回车键来执行的代码。本书的大多数示例是独立的小程序,你将
在编辑器中执行它们,因为大多数代码也是这样编写出来的。然而,
为高效地演示一些基本概念,需要在Python终端会话中执行一系列代
码片段。只要代码清单中包含三个右尖括号(如 ❶ 所示),就意味着
<!-- p45 -->
代码是在终端会话中执行的,而输出也是来自终端会话的。稍后将演
示如何在Python解释器中编写代码。
此外,你还要安装一款文本编辑器,并使用它来完成学习编程的标准
操作——编写一个简单的Hello World程序。长期以来,编程界都认为
刚接触一门新语言时,如果首先使用它来编写一个在屏幕上显示消息
“Hello world!”的程序,将给你带来好运。这种程序虽然简单,却
有其用途:如果它能够在你的系统上正确运行,那么你编写的任何
Python程序也都将正确运行。
1 . 1 . 3 S u b l i m e T e x t 简 介
Sublime Text是一款简单的文本编辑器,可以在任何现代操作系统中
安装。你几乎能直接在Sublime Text中执行所有程序。在Sublime
Text中执行程序时,代码将在其内嵌的终端会话中运行,让你能够轻
松地看到输出。
Sublime Text是一款适合初学者的编辑器,但很多专业编程人员也在
使用它。在学习Python的过程中熟练掌握Sublime Text之后,可继续
使用它来编写复杂的大型项目。Sublime Text的许可条件非常宽松,
可以一直免费使用,但如果你喜欢它并想长期使用,其开发者会要求
你购买许可证。
附录B介绍了其他几种文本编辑器,如果你想知道还有哪些编辑器可供
使用,现在就应该读一读。如果你想马上动手编程,可先使用Sublime
Text,等有了一些编程经验后再考虑使用其他编辑器。本章稍后将引
导你在当前使用的操作系统中安装Sublime Text。
<!-- p46 -->
1 . 2 在 不 同 操 作 系 统 中 搭 建 P y t h o n 编 程
环 境
Python是一种跨平台的编程语言,这意味着它能够运行在所有主流操
作系统中。在所有安装了Python的现代计算机上,都能够运行你编写
的任何Python程序。然而,在不同的操作系统中,安装Python的方法
存在细微的差别。
在本节中,你将学习如何在自己的系统中安装Python。首先要检查系
统是否安装了较新的Python版本,如果没有,就进行安装;然后是安
装Sublime Text。在不同的操作系统中搭建Python编程环境时,只有
这两步存在差别。
接下来,你将运行Hello World程序,并排除各种故障。我将详细介绍
如何在各种操作系统中完成这些任务,让你能够搭建一个对初学者友
好的Python编程环境。
1 . 2 . 1 在 W i n d o w s 系 统 中 搭 建 P y t h o n 编 程 环 境
Windows系统并非都默认安装了Python,因此你可能需要安装它,再安
装Sublime Text。
- 安 装 P y t h o n
首先,检查你的系统是否安装了Python。为此,在“开始”菜单
中输入command 并按回车以打开一个命令窗口;也可以按住
Shift键并右击桌面,选择“在此处打开命令窗口” 。在终端窗
口中输入python (全部小写)并按回车:如果出现了Python提
示符(>>> ),就说明系统安装了Python;如果出现一条错误消
息,指出python 是无法识别的命令,就说明没有安装Python。
如果出现后一种情况或者安装的Python版本低于3.6,就需要下载
Windows Python安装程序。为此,请访问Python官方网站主页。
将鼠标指向Download链接,你将看到一个用于下载最新版本
Python的按钮。单击该按钮,这将根据你的系统自动下载正确的
<!-- p47 -->
安装程序。下载安装程序后,运行它。请务必选中复选框Add
Python(版本号)to PATH(例如图1-1),这让你能够更轻松地
配置系统。
图 1 - 1 确 保 选 中 复 选 框 A d d P y t h o n ( 版 本 号 ) t o P A T H
- 在 终 端 会 话 中 运 行 P y t h o n
打开一个命令窗口,并在其中执行命令python 。如果出现了
Python提示符(>>> ),就说明Windows找到了你刚安装的
Python版本。
C:\> python
Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC
v.1916 64 bit
(AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more
information.
>>
注 意 如果没有看到类似的输出,请参阅附录A中更详尽的
安装说明。
<!-- p48 -->
在Python会话中执行下面的命令,并确认看到了输出“Hello
Python interpreter!”。
>> print("Hello Python interpreter!")
Hello Python interpreter!
>>
每当要运行Python代码片段时,都请打开一个命令窗口并启动
Python终端会话。要关闭该终端会话,可按Ctrl + Z、再按回车
键,也可执行命令exit() 。
- 安 装 S u b l i m e T e x t
要下载Sublime Text安装程序,可访问Sublime Text网站主页,
单击Download链接,并查找Windows安装程序。下载安装程序后运
行它,并接受所有的默认设置。
在Windows 10系统中,可如此打开PowerShell窗口。——编者注
1 . 2 . 2 在 m a c O S 系 统 中 搭 建 P y t h o n 编 程 环 境
大多数macOS系统默认安装了Python。确定安装了Python后,你还需安
装Sublime Text,并确保其配置正确无误。
- 检 查 是 否 安 装 了 P y t h o n 3
在文件夹Applications/Utilities中,选择Terminal,打开一个
终端窗口;也可以按Command + 空格键,再输入terminal 并按
回车。为确定是否安装了Python,请执行命令python (请注
意,其中的p 是小写的),这也将在终端窗口中启动Python,让
你能够输入Python命令。输出类似于下面这样,它指出了安装的
Python版本;最后的>>> 是提示符,让你能够输入Python命令。
$ python
Python 2.7.15 (default, Aug 17 2018, 22:39:05)
[GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)] on
<!-- p49 -->
darwin
Type "help", "copyright", "credits", or "license" for more
information.
>>
上述输出表明,当前计算机默认使用的Python版本为Python
2.7.15。看到上述输出后,如果要退出Python并返回到终端窗
口,可按Ctrl + D或执行命令exit() 。
要检查系统是否安装了Python 3,可尝试执行命令python3 。
可能会出现一条错误消息,这意味着没有安装任何Python 3版
本。如果输出指出安装了Python 3.6或更高的版本,可以直接跳
过下一小节。如果系统没有安装Python 3,就需要手动安装它。
注意,请将本书中所有的命令python 都替换为命令python3
,这样才能使用Python 3(而不是Python 2)。Python 2和
Python 3的差别非常大,如果你使用Python 2来运行本书的代
码,肯定会遇到麻烦。
如果系统默认安装的是低于Python 3.6的版本,请按下一小节的
说明安装最新版本。
- 安 装 最 新 的 P y t h o n 版 本
要下载Python安装程序,可访问Python网站主页。将鼠标指向
Download链接,你将看到一个用于下载最新版本Python的按钮。
单击该按钮,这将根据你的系统自动下载正确的安装程序。下载
安装程序后,运行它。
运行安装程序后,在终端提示符下执行如下命令:
$ python3 --version
Python 3.7.2
输出应该类似于上面这样。如果确实如此,就可以开始尝试使用
Python了,但请务必将本书中的每个命令python 都替换为
<!-- p50 -->
python3 。
- 在 终 端 会 话 中 运 行 P y t h o n 代 码
现在可以打开终端窗口并执行命令python3 ,再尝试运行
Python代码片段。请在终端会话中输入如下代码行并按回车:
>> print("Hello Python interpreter!")
Hello Python interpreter!
>>
消息将直接输出到当前终端窗口中。别忘了,要关闭Python解释
器,可按Ctrl + D或执行命令exit() 。
- 安 装 S u b l i m e T e x t
要安装编辑器Sublime Text,需要下载安装程序。为此,可访问
Sublime Text网站主页,单击链接Download,并查找macOS安装程
序。下载安装程序后运行它,再将Sublime Text图标拖放到文件
夹Applications中。
1 . 2 . 3 在 L i n u x 系 统 中 搭 建 P y t h o n 编 程 环 境
Linux系统是为编程而设计的,因此大多数Linux计算机默认安装了
Python。编写和维护Linux的人认为,你很可能会使用这种系统进行编
程,他们也鼓励你这样做。因此,要在这种系统中编程,你几乎不用
安装什么软件,只需要修改一些设置。
- 检 查 P y t h o n 版 本
在你的系统中运行应用程序Terminal(如果你使用的是Ubuntu,
可按Ctrl + Alt + T),打开一个终端窗口。为确定安装的是哪
个Python版本,请执行命令python3 (请注意,其中的p 是小
写的)。如果安装了Python,这个命令将启动Python解释器。输
<!-- p51 -->
出类似于下面这样,它指出了安装的Python版本;最后的>>> 是
提示符,让你能够输入Python命令。
$ python3
Python 3.7.2 (default, Dec 27 2018, 04:01:51)
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more
information.
>>
上述输出表明,当前计算机默认使用的Python版本为Python
3.7.2。看到上述输出后,如果要退出Python并返回到终端窗口,
可按Ctrl + D或执行命令exit() 。务必将本书中的每个命令
python 都替换为python3 。
要运行本书的代码,必须使用Python 3.6或更高的版本。如果系
统安装的是低于Python 3.6的版本,请参阅附录A,了解如何安装
最新版。
- 在 终 端 会 话 中 运 行 P y t h o n 代 码
现在可打开终端窗口并执行命令python3 ,再尝试运行Python
代码片段。检查Python版本时,你就这样做过。下面再次这样
做,然后在终端会话中输入如下代码并按回车:
>> print("Hello Python interpreter!")
Hello Python interpreter!
>>
消息将直接打印到当前终端窗口中。别忘了,要关闭Python解释
器,可按Ctrl + D或执行命令exit() 。
- 安 装 S u b l i m e T e x t
在Linux系统中,可通过Ubuntu Software Center来安装Sublime
Text。为此,单击菜单中的Ubuntu Software图标并查找Sublime
<!-- p52 -->
Text,再通过单击来安装它。
<!-- p53 -->
1 . 3 运 行 H e l l o W o r l d 程 序
安装较新版本的Python和Sublime Text后,就可以编写并运行你的第
一个Python程序了。这样做之前,需要设置Sublime Text,确保它使
用系统中正确的Python版本。然后,就可以编写并运行Hello World程
序了。
1 . 3 . 1 配 置 S u b l i m e T e x t 以 使 用 正 确 的 P y t h o n 版 本
如果在你的系统中执行命令python 时启动的是Python 3,就无须做
任何配置,直接跳到下一节即可。如果需要执行命令python3 来启
动Python,就需要配置Sublime Text,使其使用正确的Python版本来
运行你编写的程序。
为此,单击Sublime Text图标以启动它,也可在搜索栏中输入Sublime
Text来找到它再启动。选择菜单Tools ▶ Build System ▶ New
Build System,新建一个配置文件。删除该文件中的所有内容,再输
入如下内容:
Python3
.sublime-build
{
"cmd": ["python3", "-u", "$file"],
}
这段代码让Sublime Text使用命令python3 来运行Python程序。将
这个文件保存到Sublime Text默认打开的文件夹中,并将其命名为
Python3.sublime-build。
1 . 3 . 2 运 行 程 序 h e l l o _ w o r l d . p y
<!-- p54 -->
编写第一个程序前,在系统中创建一个名为python_work的文件夹,用
于存储你开发的项目。文件名和文件夹名称最好使用小写字母,并使
用下划线代替空格,因为Python采用了这些命名约定。
启动Sublime Text,再选择菜单File ▶ Save As将Sublime Text创建
的空文件存储到文件夹python_work中,并将其命名为
hello_world.py。文件扩展名.py告诉Sublime Text,文件中的代码是
使用Python编写的,这能让它知道如何运行这个程序,并以有帮助的
方式突出其中的代码。
保存这个文件后,在其中输入如下代码行:
hello_world.py
print("Hello Python world!")
在你的系统中,如果能使用命令python 来启动Python 3,可以选择
菜单Tools ▶ Build或按Ctrl + B(在macOS系统中为Command + B)
来运行程序。如果需要像前一节那样配置Sublime Text,请选择菜单
Tools ▶ Build System ▶ Python 3来运行这个程序。从此以后,你
就可以选择菜单Tools ▶ Build或按Ctrl + B(或Command + B)来运
行程序了。
在Sublime Text的底部,将出现一个终端窗口,其中包含如下输出:
Hello Python world!
[Finished in 0.1s]
如果看不到上述输出,可能是因为这个程序出了点问题。请检查你输
入的每个字符。是否不小心将print 的首字母大写了?是否遗漏了引
号或圆括号?编程语言的语法非常严格,只要不满足要求,就会报
错。如果你无法运行这个程序,请参阅下一节的建议。
<!-- p55 -->
1.4 解决安装问题
如果无法运行程序hello_world.py,可尝试如下几个解决方法,这些
通用方法适用于所有编程问题。
程序存在严重错误时,Python将显示traceback,即错误报告。
Python会仔细研究文件,试图找出其中的问题。trackback可能会
提供线索,让你知道是什么问题让程序无法运行。
离开计算机,先休息一会儿再尝试。别忘了,语法在编程中非常
重要,即便是少一个冒号、引号不匹配或括号不匹配,都可能导
致程序无法正确运行。请再次阅读本章的相关内容,并重新审视
你编写的代码,看看能否找出错误。
推倒重来。你也许不需要卸载任何软件,但删除文件
hello_world.py并重新创建它也许是合理的选择。
让别人在你的计算机或其他计算机上按本章的步骤重做一遍,并
仔细观察。你可能遗漏了一小步,而别人刚好没有遗漏。
请懂Python的人帮忙。当你有这样的想法时,可能发现在你认识
的人当中就有人使用Python。
本章的安装说明在本书主页上:ituring.cn/book/2784。对你来
说,在线版也许更合适,因为可以复制并粘贴其中的代码。
到网上寻求帮助。附录C提供了很多在线资源,如论坛或在线聊天
网站,你可以在这些地方请求解决过相同问题的人提供解决方
案。
不要担心这会打扰经验丰富的程序员。每个程序员都遇到过问题,大
多数程序员很乐意帮助你正确地设置系统。只要能清晰地说明你要做
什么、尝试了哪些方法及其结果,就很可能有人能够帮到你。正如前
言中指出的,Python社区对初学者非常友好。
任何现代计算机都能够运行Python。前期的问题可能令人沮丧,但很
值得你花时间去解决。能够运行hello_world.py后,你就可以开始学
习Python了,而且编程工作会更有趣,也更令人愉快。
<!-- p56 -->
1 . 5 从 终 端 运 行 P y t h o n 程 序
你编写的大多数程序将直接在文本编辑器中运行,但有时候从终端运
行程序很有用。例如,你可能想直接运行既有的程序。
在任何安装了Python的系统上都可以这样做,前提是你知道如何进入
程序文件所在的目录。为尝试这样做,请确保将文件hello_world.py
存储到了桌面的文件夹python_work中。
1 . 5 . 1 在 W i n d o w s 系 统 中 从 终 端 运 行 P y t h o n 程 序
在命令窗口中,可以使用终端命令cd (表示change directory,即
切 换 目 录 )在文件系统中导航。使用命令dir (表示directory,即
目 录 )可以显示当前目录中的所有文件。
为运行程序hello_world.py,请打开一个新的终端窗口,并执行下面
的命令:
❶ C:\> cd Desktop\python_work
❷ C:\Desktop\python_work> dir
hello_world.py
❸ C:\Desktop\python_work> python hello_world.py
Hello Python world!
这里使用了命令cd 来切换到文件夹Desktop\python_work(见 ❶ )。
接下来,使用命令dir 来确认这个文件夹中包含文件
hello_world.py(见 ❷ )。最后,使用命令python
hello_world.py 来运行这个文件(见 ❸ )。
大多数程序可直接从编辑器运行,但待解决的问题比较复杂时,你编
写的程序可能需要从终端运行。
<!-- p57 -->
1 . 5 . 2 在 L i n u x 和 m a c O S 系 统 中 从 终 端 运 行 P y t h o n 程
序
在Linux和macOS系统中,从终端运行Python程序的方式相同。在终端
会话中,可以使用终端命令cd (表示change directory,即切 换 目
录 )在文件系统中导航。使用命令ls (表示list,即列 表 )可以
显示当前目录中所有未隐藏的文件。
为运行程序hello_world.py,请打开一个新的终端窗口,并执行下面
的命令:
❶ ~$ cd Desktop/python_work/
❷ ~/Desktop/python_work$ ls
hello_world.py
❸ ~/Desktop/python_work$ python hello_world.py
Hello Python world!
这里使用了命令cd 来切换到文件夹Desktop/python_work(见 ❶ )。
接下来,使用命令ls 来确认这个文件夹中包含文件
hello_world.py(见 ❷ )。最后,使用命令python
hello_world.py 来运行这个文件(见 ❸ )。
就这么简单。要运行Python程序,只需使用命令python (或
python3 )即可。
动 手 试 一 试
本章的练习都是探索性的,但从第2章开始将要求你用那一章学到
的知识来解决问题。
练 习 1 - 1 : p y t h o n . o r g 浏览Python主页,寻找你感兴趣的主
题。你对Python越熟悉,这个网站对你来说就越有用。
练 习 1 - 2 : 输 入 错 误 打开你刚创建的文件hello_world.py,在
代码中添加一个输入错误,再运行这个程序。输入错误会引发错
<!-- p58 -->
误吗?你能理解显示的错误消息吗?你能添加一个不会导致错误
的输入错误吗?你凭什么认为它不会导致错误?
练 习 1 - 3 : 无 穷 的 技 艺 如果你有无穷多种编程技艺,你打算开
发什么样的程序呢?你就要开始学习编程了。如果心中有目标,
就能立即将新学到的技能付诸应用,现在正是草拟目标的大好时
机。将想法记录下来是个不错的习惯,这样每当需要开始新项目
时,都可参考它们。现在请花点时间描绘三个你想创建的程序。
<!-- p59 -->
1.6 小结
在本章中,你大致了解了Python,并在自己的系统中安装了Python。
你还安装了一个文本编辑器,以简化Python代码的编写工作。你学习
了如何在终端会话中运行Python代码片段,并运行了第一个程序——
hello_world.py。你还大致了解了如何解决安装问题。
在下一章,你将学习如何在Python程序中使用各种数据和变量。
<!-- p60 -->
<!-- p61 -->
第 2 章 变量和简单数据类型
在本章中,你将学习可在Python程序中使用的各种
数据,还将学习如何在程序中使用变量来表示这些数据。
<!-- p62 -->
2 . 1 运 行 h e l l o _ w o r l d . p y 时 发 生 的 情 况
运行hello_world.py时,Python都做了些什么呢?下面来深入研究一
下。实际上,即便是运行简单的程序,Python所做的工作也相当多:
hello_world.py
print("Hello Python world!")
运行上述代码时,你将看到如下输出:
Hello Python world!
运行文件hello_world.py时,末尾的.py指出这是一个Python程序,因
此编辑器将使用P y t h o n 解 释 器 来运行它。Python解释器读取整个程
序,确定其中每个单词的含义。例如,看到后面跟着圆括号的单词
print 时,解释器就将圆括号中的内容打印到屏幕。
编写程序时,编辑器会以各种方式突出程序的不同部分。例如,它知
道print() 是一个函数的名称,因此将其显示为某种颜色;它知
道"Hello Python world!" 不是Python代码,因此将其显示为另
一种颜色。这种功能称为语 法 高 亮 ,在你刚开始编写程序时很有帮
助。
<!-- p63 -->
2.2 变量
下面来尝试在hello_world.py中使用一个变量。在这个文件开头添加
一行代码,并对第二行代码进行修改,如下所示:
hello_world.py
message = "Hello Python world!"
print(message)
运行这个程序,看看结果如何。你会发现,输出与以前相同:
Hello Python world!
我们添加了一个名为message 的变 量 。每个变量都指向一个值
——与该变量相关联的信息。在这里,指向的值为文本"Hello
Python world!" 。
添加变量导致Python解释器需要做更多工作。处理第一行代码时,它
将变量message 与文本"Hello Python world!" 关联起来;处
理第二行代码时,它将与变量message 关联的值打印到屏幕。
下面来进一步扩展这个程序:修改hello_world.py,使其再打印一条
消息。为此,在hello_world.py中添加一个空行,再添加下面两行代
码:
message = "Hello Python world!"
print(message)
message = "Hello Python Crash Course world!"
print(message)
<!-- p64 -->
现在如果运行这个程序,将看到两行输出:
Hello Python world!
Hello Python Crash Course world!
在程序中可随时修改变量的值,而Python将始终记录变量的最新值。
2.2.1 变量的命名和使用
在Python中使用变量时,需要遵守一些规则和指南。违反这些规则将
引发错误,而指南旨在让你编写的代码更容易阅读和理解。请务必牢
记下述有关变量的规则。
变量名只能包含字母、数字和下划线。变量名能以字母或下划线
打头,但不能以数字打头。例如,可将变量命名为message_1
,但不能将其命名为1_message 。
变量名不能包含空格,但能使用下划线来分隔其中的单词。例
如,变量名greeting_message 可行,但变量名greeting
message 会引发错误。
不要将Python关键字和函数名用作变量名,即不要使用Python保
留用于特殊用途的单词,如print (请参见附录A.4)。
变量名应既简短又具有描述性。例如,name 比n 好,
student_name 比s_n 好,name_length 比
length_of_persons_name 好。
慎用小写字母l 和大写字母O ,因为它们可能被人错看成数字1
和0 。
要创建良好的变量名,需要经过一定的实践,在程序复杂而有趣时尤
其如此。随着编写的程序越来越多,并开始阅读别人编写的代码,你
将越来越善于创建有意义的变量名。
注 意 就目前而言,应使用小写的Python变量名。虽然在变量
名中使用大写字母不会导致错误,但是大写字母在变量名中有特
殊含义,这将在本书后面讨论。
<!-- p65 -->
2.2.2 使用变量时避免命名错误
程序员都会犯错,而且大多数程序员每天都会犯错。虽然优秀的程序
员也会犯错,但他们也知道如何高效地消除错误。下面来看一种你可
能犯的错误,并学习如何消除它。
我们将有意地编写一些引发错误的代码。请输入下面的代码,包括其
中拼写不正确、以粗体显示的单词mesage :
message = "Hello Python Crash Course reader!"
print(mesage)
程序存在错误时,Python解释器将竭尽所能地帮助你找出问题所在。
程序无法成功运行时,解释器将提供一个traceback。traceback是一
条记录,指出了解释器尝试运行代码时,在什么地方陷入了困境。下
面是你不小心错误地拼写了变量名时,Python解释器提供的
traceback:
Traceback (most recent call last):
❶ File "hello_world.py", line 2, in <module>
❷ print(mesage)
❸ NameError: name 'mesage' is not defined
解释器指出,文件hello_world.py的第二行存在错误(见 ❶ )。它列
出了这行代码,旨在帮助你快速找出错误(见 ❷ ),还指出了它发现
的是什么样的错误(见 ❸ )。在这里,解释器发现了一个名 称 错 误 ,
并报告打印的变量mesage 未定义:Python无法识别你提供的变量
名。名称错误通常意味着两种情况:要么是使用变量前忘记给它赋
值,要么是输入变量名时拼写不正确。
在这个示例中,第二行的变量名message 遗漏了字母s。Python解释
器不会对代码做拼写检查,但要求变量名的拼写一致。例如,如果在
代码的另一个地方也将message 错误地拼写成了mesage ,结果将
如何呢?
<!-- p66 -->
mesage = "Hello Python Crash Course reader!"
print(mesage)
在这种情况下,程序将成功运行!
Hello Python Crash Course reader!
编程语言要求严格,但并不关心拼写是否正确。因此,创建变量名和
编写代码时,无须考虑英语中的拼写和语法规则。
很多编程错误都简单,只是在程序的某一行输错了一个字符。为找出
这种错误而花费很长时间的大有人在。很多程序员天资聪颖、经验丰
富,却为找出这种细微的错误花费数小时。你可能觉得这很好笑,但
别忘了,在你的编程生涯中,经常会有同样的遭遇。
2.2.3 变量是标签
变量常被描述为可用于存储值的盒子。在你刚接触变量时,这种定义
可能很有帮助,但它并没有准确描述Python内部表示变量的方式。一
种好得多的定义是,变量是可以赋给值的标签,也可以说变量指向特
定的值。
刚学习编程时,这种差别可能意义不大,但越早知道越好。你迟早会
遇到变量的行为出乎意料的情形,此时如果对变量的工作原理有准确
的认识,将有助于搞清楚代码是如何运行的。
注 意 要理解新的编程概念,最佳的方式是尝试在程序中使用
它们。如果你在完成本书的练习时陷入了困境,请尝试做点其他
的事情。如果这样做后依然无法摆脱困境,请复习相关内容。如
果这样做后情况依然如故,请参阅附录C的建议。
动 手 试 一 试
<!-- p67 -->
在完成下面的每个练习时,都编写一个独立的程序。保存每个程
序时,使用符合标准Python约定的文件名:使用小写字母和下划
线,如simple_message.py和simple_messages.py。
练 习 2 - 1 : 简 单 消 息 将一条消息赋给变量,并将其打印出来。
练 习 2 - 2 : 多 条 简 单 消 息 将一条消息赋给变量,并将其打印出
来;再将变量的值修改为一条新消息,并将其打印出来。
<!-- p68 -->
2.3 字符串
大多数程序定义并收集某种数据,然后使用它们来做些有意义的事
情。有鉴于此,对数据进行分类大有裨益。我们将介绍的第一种数据
类型是字符串。字符串虽然看似简单,但能够以很多不同的方式使
用。
字 符 串 就是一系列字符。在Python中,用引号括起的都是字符串,
其中的引号可以是单引号,也可以是双引号,如下所示:
"This is a string."
'This is also a string.'
这种灵活性让你能够在字符串中包含引号和撇号:
'I told my friend, "Python is my favorite language!"'
"The language 'Python' is named after Monty Python, not the
snake."
"One of Python's strengths is its diverse and supportive
community."
下面来看一些使用字符串的方式。
2 . 3 . 1 使 用 方 法 修 改 字 符 串 的 大 小 写
对于字符串,可执行的最简单的操作之一是修改其中单词的大小写。
请看下面的代码,并尝试判断其作用:
name.py
name = "ada lovelace"
print(name.title())
<!-- p69 -->
将这个文件保存为name.py,再运行它。你将看到如下输出:
Ada Lovelace
在这个示例中,变量name 指向小写的字符串"ada lovelace" 。
在函数调用print() 中,方法title() 出现在这个变量的后面。方
法 是Python可对数据执行的操作。在name.title() 中,name 后
面的句点(. )让Python对变量name 执行方法title() 指定的操
作。每个方法后面都跟着一对圆括号,这是因为方法通常需要额外的
信息来完成其工作。这种信息是在圆括号内提供的。函数title()
不需要额外的信息,因此它后面的圆括号是空的。
方法title() 以首字母大写的方式显示每个单词,即将每个单词的
首字母都改为大写。这很有用,因为你经常需要将名字视为信息。例
如,你可能希望程序将值Ada 、ADA 和ada 视为同一个名字,并将
它们都显示为Ada 。
还有其他几个很有用的大小写处理方法。例如,要将字符串改为全部
大写或全部小写,可以像下面这样做:
name = "Ada Lovelace"
print(name.upper())
print(name.lower())
这些代码的输出如下:
ADA LOVELACE
ada lovelace
存储数据时,方法lower() 很有用。很多时候,你无法依靠用户来
提供正确的大小写,因此需要将字符串先转换为小写,再存储它们。
以后需要显示这些信息时,再将其转换为最合适的大小写方式。
<!-- p70 -->
2.3.2 在字符串中使用变量
在有些情况下,你可能想在字符串中使用变量的值。例如,你可能想
使用两个变量分别表示名和姓,然后合并这两个值以显示姓名:
full_name.py
first_name = "ada"
last_name = "lovelace"
❶ full_name = f"{first_name} {last_name}"
print(full_name)
要在字符串中插入变量的值,可在前引号前加上字母f (见 ❶ ),再
将要插入的变量放在花括号内。这样,当Python显示字符串时,将把
每个变量都替换为其值。
这种字符串名为f 字 符 串 。f是format(设置格式)的简写,因为
Python通过把花括号内的变量替换为其值来设置字符串的格式。上述
代码的输出如下:
ada lovelace
使用f字符串可完成很多任务,如利用与变量关联的信息来创建完整的
消息,如下所示:
first_name = "ada"
last_name = "lovelace"
full_name = f"{first_name} {last_name}"
❶ print(f"Hello, {full_name.title()}!")
在这里,一个问候用户的句子中使用了完整的姓名(见 ❶ ),并使用
方法title() 来将姓名设置为合适的格式。这些代码显示一条格式
良好的简单问候语:
<!-- p71 -->
Hello, Ada Lovelace!
还可以使用f字符串来创建消息,再把整条消息赋给变量:
first_name = "ada"
last_name = "lovelace"
full_name = f"{first_name} {last_name}"
❶ message = f"Hello, {full_name.title()}!"
❷ print(message)
上述代码也显示消息Hello, Ada Lovelace! ,但将这条消息赋给
了一个变量(见 ❶ ),这让最后的函数调用print() 变得简单得多
(见 ❷ )。
注 意 f字符串是Python 3.6引入的。如果你使用的是Python
3.5或更早的版本,需要使用format() 方法,而非这种f语法。
要使用方法format() ,可在圆括号内列出要在字符串中使用的
变量。对于每个变量,都通过一对花括号来引用。这样将按顺序
将这些花括号替换为圆括号内列出的变量的值,如下所示:
full_name = "{} {}".format(first_name, last_name)
2 . 3 . 3 使 用 制 表 符 或 换 行 符 来 添 加 空 白
在编程中,空 白 泛指任何非打印字符,如空格、制表符和换行符。
你可以使用空白来组织输出,让用户阅读起来更容易。
要在字符串中添加制表符,可使用字符组合\t ,如下述代码的 ❶ 处所
示:
>> print("Python")
Python
❶ >>> print("\tPython")
<!-- p72 -->
Python
要在字符串中添加换行符,可使用字符组合\n :
>> print("Languages:\nPython\nC\nJavaScript")
Languages:
Python
C
JavaScript
还可在同一个字符串中同时包含制表符和换行符。字符串"\n\t" 让
Python换到下一行,并在下一行开头添加一个制表符。下面的示例演
示了如何使用一个单行字符串来生成四行输出:
>> print("Languages:\n\tPython\n\tC\n\tJavaScript")
Languages:
Python
C
JavaScript
在接下来的两章中,你将使用为数不多的几行代码来生成很多行输
出,届时制表符和换行符将提供极大的帮助。
2.3.4 删除空白
在程序中,额外的空白可能令人迷惑。对程序员来说,'python'
和'python ' 看起来几乎没什么两样,但对程序来说,它们却是两
个不同的字符串。Python能够发现'python ' 中额外的空白,并认
为它意义重大——除非你告诉它不是这样的。
空白很重要,因为你经常需要比较两个字符串是否相同。一个重要的
示例是,在用户登录网站时检查其用户名。不过在非常简单的情形
下,额外的空格也可能令人迷惑。所幸,在Python中删除用户输入数
据中的多余空白易如反掌。
<!-- p73 -->
Python能够找出字符串开头和末尾多余的空白。要确保字符串末尾没
有空白,可使用方法rstrip() 。
❶ >>> favorite_language = 'python '
❷ >>> favorite_language
'python '
❸ >>> favorite_language.rstrip()
'python'
❹ >>> favorite_language
'python '
与变量favorite_language 相关联的字符串末尾有多余的空白
(见 ❶ )。你在终端会话中向Python询问这个变量的值时,可看到末
尾的空格(见 ❷ )。对变量favorite_language 调用方法
rstrip() 后(见 ❸ ),这个多余的空格被删除了。然而,这种删除
只是暂时的,接下来再次询问favorite_language 的值时,你会
发现这个字符串与输入时一样,依然包含多余的空白(见 ❹ )。
要永久删除这个字符串中的空白,必须将删除操作的结果关联到变
量:
>> favorite_language = 'python '
❶ >>> favorite_language = favorite_language.rstrip()
>> favorite_language
'python'
为删除这个字符串中的空白,要将其末尾的空白剔除,再将结果关联
到原来的变量(见 ❶ )。在编程中,经常需要修改变量的值,再将新
值关联到原来的变量。这就是变量的值可能随程序的运行或用户输入
数据而发生变化的原因所在。
你还可以剔除字符串开头的空白,或者同时剔除字符串两边的空白。
为此,可分别使用方法lstrip() 和strip() :
<!-- p74 -->
❶ >>> favorite_language = ' python '
❷ >>> favorite_language.rstrip()
' python'
❸ >>> favorite_language.lstrip()
'python '
❹ >>> favorite_language.strip()
'python'
在这个示例中,我们首先创建了一个开头和末尾都有空白的字符串
(见 ❶ )。接下来,分别删除末尾(见 ❷ )、开头(见 ❸ )和两边(见
❹ )的空白。尝试使用这些剥除函数有助于你熟悉字符串操作。在实
际程序中,这些剥除函数最常用于在存储用户输入前对其进行清理。
2.3.5 使用字符串时避免语法错误
语 法 错 误 是一种你时不时会遇到的错误。程序中包含非法的Python
代码时,就会导致语法错误。例如,在用单引号括起的字符串中,如
果包含撇号,就将导致错误。这是因为这会导致Python将第一个单引
号和撇号之间的内容视为一个字符串,进而将余下的文本视为Python
代码,从而引发错误。
下面演示了如何正确地使用单引号和双引号。请将该程序保存为
apostrophe.py,再运行它:
apostrophe.py
message = "One of Python's strengths is its diverse community."
print(message)
撇号位于两个双引号之间,因此Python解释器能够正确地理解这个字
符串:
One of Python's strengths is its diverse community.
<!-- p75 -->
然而,如果使用单引号,Python将无法正确地确定字符串的结束位
置:
message = 'One of Python's strengths is its diverse community.'
print(message)
你将看到如下输出:
File "apostrophe.py", line 1
message = 'One of Python's strengths is its diverse
community.'
^ ❶
SyntaxError: invalid syntax
从上述输出可知,错误发生在第二个单引号后面(见 ❶ )。这种语法
错误表明,在解释器看来,其中的有些内容不是有效的Python代码。
错误的原因各种各样,我将指出一些常见的原因。学习编写Python代
码时,你可能会经常遇到语法错误。语法错误也是最不具体的错误类
型,因此可能难以找出并修复。受困于非常棘手的错误时,请参阅附
录C提供的建议。
注 意 编写程序时,编辑器的语法高亮功能可帮助你快速找出
某些语法错误。看到Python代码以普通句子的颜色显示,或者普
通句子以Python代码的颜色显示时,就可能意味着文件中存在引
号不匹配的情况。
动 手 试 一 试
完成下面的每个练习时,都编写一个独立的程序,并将其保存为
名称类似于name_cases.py的文件。如果遇到了困难,请休息一会
儿或参阅附录C提供的建议。
练 习 2 - 3 : 个 性 化 消 息 用变量表示一个人的名字,并向其显示
一条消息。显示的消息应非常简单,下面是一个例子。
<!-- p76 -->
Hello Eric, would you like to learn some Python today?
练 习 2 - 4 : 调 整 名 字 的 大 小 写 用变量表示一个人的名字,再以
小写、大写和首字母大写的方式显示这个人名。
练 习 2 - 5 : 名 言 找一句你钦佩的名人说的名言,将其姓名和名
言打印出来。输出应类似于下面这样(包括引号)。
Albert Einstein once said, “A person who never made
a mistake never tried anything new.”
练 习 2 - 6 : 名 言 2 重复练习2-5,但用变量famous_person 表
示名人的姓名,再创建要显示的消息并将其赋给变量message
,然后打印这条消息。
练 习 2 - 7 : 剔 除 人 名 中 的 空 白 用变量表示一个人的名字,并在
其开头和末尾都包含一些空白字符。务必至少使用字符组合"\t"
和"\n" 各一次。
打印这个人名,显示其开头和末尾的空白。然后,分别使用剔除
函数lstrip() 、rstrip() 和strip() 对人名进行处理,并
将结果打印出来。
<!-- p77 -->
2.4 数
在编程中,经常使用数来记录得分、表示可视化数据、存储Web应用信
息,等等。Python能根据数的用法以不同的方式处理它们。鉴于整数
使用起来最简单,下面就先来看看Python是如何管理它们的。
2.4.1 整数
在Python中,可对整数执行加(+ )减(- )乘(* )除(/ )运
算。
>> 2 + 3
>> 3 - 2
>> 2 * 3
>> 3 / 2
1.5
在终端会话中,Python直接返回运算结果。Python使用两个乘号表示
乘方运算:
>> 3 ** 2
>> 3 ** 3
>> 10 ** 6
1000000
Python还支持运算次序,因此可在同一个表达式中使用多种运算。还
可以使用圆括号来修改运算次序,让Python按你指定的次序执行运
算,如下所示:
<!-- p78 -->
>> 2 + 3*4
>> (2 + 3) * 4
在这些示例中,空格不影响Python计算表达式的方式。它们的存在旨
在让你在阅读代码时能迅速确定先执行哪些运算。
2.4.2 浮点数
Python将所有带小数点的数称为浮 点 数 。大多数编程语言使用了这
个术语,它指出了这样一个事实:小数点可出现在数的任何位置。每
种编程语言都必须细心设计,以妥善地处理浮点数,确保不管小数点
出现在什么位置,数的行为都是正常的。
从很大程度上说,使用浮点数时无须考虑其行为。你只需输入要使用
的数,Python通常会按你期望的方式处理它们:
>> 0.1 + 0.1
0.2
>> 0.2 + 0.2
0.4
>> 2 * 0.1
0.2
>> 2 * 0.2
0.4
但需要注意的是,结果包含的小数位数可能是不确定的:
>> 0.2 + 0.1
0.30000000000000004
>> 3 * 0.1
0.30000000000000004
所有语言都存在这种问题,没有什么可担心的。Python会尽力找到一
种精确表示结果的方法,但鉴于计算机内部表示数的方式,这在有些
<!-- p79 -->
情况下很难。就现在而言,暂时忽略多余的小数位数即可。在第二部
分的项目中,你将在需要时学习处理多余小数位的方式。
2.4.3 整数和浮点数
将任意两个数相除时,结果总是浮点数,即便这两个数都是整数且能
整除:
>> 4/2
2.0
在其他任何运算中,如果一个操作数是整数,另一个操作数是浮点
数,结果也总是浮点数:
>> 1 + 2.0
3.0
>> 2 * 3.0
6.0
>> 3.0 ** 2
9.0
无论是哪种运算,只要有操作数是浮点数,Python默认得到的总是浮
点数,即便结果原本为整数也是如此。
2.4.4 数中的下划线
书写很大的数时,可使用下划线将其中的数字分组,使其更清晰易
读:
>> universe_age = 14_000_000_000
当你打印这种使用下划线定义的数时,Python不会打印其中的下划
线:
<!-- p80 -->
>> print(universe_age)
14000000000
这是因为存储这种数时,Python会忽略其中的下划线。将数字分组
时,即便不是将每三位分成一组,也不会影响最终的值。在Python看
来,1000 与1_000 没什么不同,1_000 与10_00 也没什么不同。
这种表示法适用于整数和浮点数,但只有Python 3.6和更高的版本支
持。
2.4.5 同时给多个变量赋值
可在一行代码中给多个变量赋值,这有助于缩短程序并提高其可读
性。这种做法最常用于将一系列数赋给一组变量。
例如,下面演示了如何将变量x 、y 和z 都初始化为零:
>> x, y, z = 0, 0, 0
这样做时,需要用逗号将变量名分开;对于要赋给变量的值,也需同
样处理。Python将按顺序将每个值赋给对应的变量。只要变量和值的
个数相同,Python就能正确地将它们关联起来。
2.4.6 常量
常 量 类似于变量,但其值在程序的整个生命周期内保持不变。
Python没有内置的常量类型,但Python程序员会使用全大写来指出应
将某个变量视为常量,其值应始终不变:
MAX_CONNECTIONS = 5000
在代码中,要指出应将特定的变量视为常量,可将其字母全部大写。
<!-- p81 -->
动 手 试 一 试
练 习 2 - 8 : 数 字 8 编写四个表达式,分别使用加法、减法、乘
法和除法运算,但结果都是数字8。为使用函数调用print() 来
显示结果,务必将这些表达式用圆括号括起来。也就是说,你应
该编写四行类似于下面的代码:
print(5+3)
输出应为四行,其中每行都只包含数字8。
练 习 2 - 9 : 最 喜 欢 的 数 用一个变量来表示你最喜欢的数,再使
用这个变量创建一条消息,指出你最喜欢的数是什么,然后将这
条消息打印出来。
<!-- p82 -->
2.5 注释
在大多数编程语言中,注释是一项很有用的功能。本书前面编写的程
序中都只包含Python代码,但随着程序越来越大、越来越复杂,就应
在其中添加说明,对你解决问题的方法进行大致的阐述。注 释 让你
能够使用自然语言在程序中添加说明。
2.5.1 如何编写注释
在Python中,注释用井号(# )标识。井号后面的内容都会被Python
解释器忽略,如下所示:
comment.py
向大家问好。
print("Hello Python people!")
Python解释器将忽略第一行,只执行第二行。
Hello Python people!
2.5.2 该编写什么样的注释
编写注释的主要目的是阐述代码要做什么,以及是如何做的。在开发
项目期间,你对各个部分如何协同工作了如指掌,但过段时间后,有
些细节你可能不记得了。当然,你总是可以通过研究代码来确定各个
部分的工作原理,但通过编写注释以清晰的自然语言对解决方案进行
概述,可节省很多时间。
要成为专业程序员或与其他程序员合作,就必须编写有意义的注释。
当前,大多数软件是合作编写的,编写者可能是同一家公司的多名员
工,也可能是众多致力于同一个开源项目的人员。训练有素的程序员
<!-- p83 -->
都希望代码中包含注释,因此你最好从现在开始就在程序中添加描述
性注释。作为新手,最值得养成的习惯之一就是在代码中编写清晰、
简洁的注释。
如果不确定是否要编写注释,就问问自己:在找到合理的解决方案之
前,考虑了多个解决方案吗?如果答案是肯定的,就编写注释对你的
解决方案进行说明吧。相比回过头去再添加注释,删除多余的注释要
容易得多。从现在开始,本书的示例都将使用注释来阐述代码的工作
原理。
动 手 试 一 试
练 习 2 - 1 0 : 添 加 注 释 选择你编写的两个程序,在每个程序中
至少添加一条注释。如果程序太简单,实在没有什么需要说明
的,就在程序文件开头加上你的姓名和当前日期,再用一句话阐
述程序的功能。
<!-- p84 -->
2.6 P y t h o n 之禅
经验丰富的程序员倡导尽可能避繁就简。Python社区的理念都包含在
Tim Peters撰写的“Python之禅”中。要获悉这些有关编写优秀
Python代码的指导原则,只需在解释器中执行命令import this 。
这里不打算赘述整个“Python之禅”,而只与大家分享其中的几条原
则,让你明白为何它们对Python新手来说至关重要。
>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Python程序员笃信代码可以编写得漂亮而优雅。编程是要解决问题
的,设计良好、高效而漂亮的解决方案都会让程序员心生敬意。随着
你对Python的认识越来越深入,并使用它来编写越来越多的代码,有
一天也许会有人站在你后面惊呼:“哇,代码编写得真是漂亮!”
Simple is better than complex.
如果有两个解决方案,一个简单、一个复杂,但都行之有效,就选择
简单的解决方案吧。这样,你编写的代码将更容易维护,你或他人以
后改进这些代码时也会更容易。
Complex is better than complicated.
现实是复杂的,有时候可能没有简单的解决方案。在这种情况下,就
选择最简单可行的解决方案吧。
Readability counts.
<!-- p85 -->
即便是复杂的代码,也要让它易于理解。开发的项目涉及复杂代码
时,一定要为这些代码编写有益的注释。
There should be one-- and preferably only one --obvious way to do
it.
如果让两名Python程序员去解决同一个问题,他们提供的解决方案应
大致相同。这并不是说编程没有创意空间,而是恰恰相反!然而,大
部分编程工作是使用常见解决方案来解决简单的小问题,但这些小问
题都包含在更庞大、更有创意空间的项目中。在你的程序中,各种具
体细节对其他Python程序员来说都应易于理解。
Now is better than never.
你可以用余生来学习Python和编程的纷繁难懂之处,但这样你什么项
目都完不成。不要企图编写完美无缺的代码,而是要先编写行之有效
的代码,再决定是对其做进一步改进,还是转而去编写新代码。
等你进入下一章,开始研究更复杂的主题时,务必牢记这种简约而清
晰的理念。如此,经验丰富的程序员定将对你编写的代码心生敬意,
进而乐意向你提供反馈,并与你合作开发有趣的项目。
动 手 试 一 试
练 习 2 - 1 1 : P y t h o n 之 禅 在Python终端会话中执行命令import
this ,并粗略地浏览一下其他的指导原则。
<!-- p86 -->
2.7 小结
在本章中,你学习了:如何使用变量;如何创建描述性变量名以及如
何消除名称错误和语法错误;字符串是什么,以及如何使用小写、大
写和首字母大写方式显示字符串;使用空白来显示整洁的输出,以及
如何剔除字符串中多余的空白;如何使用整数和浮点数;一些使用数
值数据的方式。你还学习了如何编写说明性注释,让代码对你和其他
人来说更容易理解。最后,你了解了让代码尽可能简单的理念。
在第3章,你将学习如何在被称为列 表 的变量中存储一系列信息,以
及如何通过遍历列表来操作其中的信息。
<!-- p87 -->
第 3 章 列表简介
在本章和下一章中,你将学习列表是什么以及如何
使用列表元素。列表让你能够在一个地方存储成组的信息,其中
可以只包含几个元素,也可以包含数百万个元素。列表是新手可
直接使用的最强大的Python功能之一,它融合了众多重要的编程
概念。
<!-- p88 -->
3.1 列表是什么
列 表 由一系列按特定顺序排列的元素组成。你可以创建包含字母表
中所有字母、数字0~9或所有家庭成员姓名的列表;也可以将任何东
西加入列表中,其中的元素之间可以没有任何关系。列表通常包含多
个元素,因此给列表指定一个表示复数的名称(如letters 、
digits 或names )是个不错的主意。
在Python中,用方括号([] )表示列表,并用逗号分隔其中的元
素。下面是一个简单的列表示例,其中包含几种自行车:
bicycles.py
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles)
如果让Python将列表打印出来,Python将打印列表的内部表示,包括
方括号:
['trek', 'cannondale', 'redline', 'specialized']
鉴于这不是你要让用户看到的输出,下面来学习如何访问列表元素。
3.1.1 访问列表元素
列表是有序集合,因此要访问列表的任意元素,只需将该元素的位置
(索 引 )告诉Python即可。要访问列表元素,可指出列表的名称,
再指出元素的索引,并将后者放在方括号内。
例如,下面的代码从列表bicycles 中提取第一款自行车:
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
❶ print(bicycles[0])
<!-- p89 -->
❶ 处演示了访问列表元素的语法。当你请求获取列表元素时,Python
只返回该元素,而不包括方括号:
trek
这正是你要让用户看到的结果——整洁、干净的输出。
你还可以对任意列表元素调用第2章介绍的字符串方法。例如,可使用
方法title() 让元素'trek' 的格式更整洁:
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles[0].title())
这个示例的输出与前一个示例相同,只是首字母T是大写的。
3.1.2 索引从0而不是1开始
在Python中,第一个列表元素的索引为0,而不是1。多数编程语言是
如此规定的,这与列表操作的底层实现相关。如果结果出乎意料,请
看看你是否犯了简单的差一错误。
第二个列表元素的索引为1。根据这种简单的计数方式,要访问列表的
任何元素,都可将其位置减1,并将结果作为索引。例如,要访问第四
个列表元素,可使用索引3。
下面的代码访问索引1 和索引3 处的自行车:
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles[1])
print(bicycles[3])
<!-- p90 -->
这些代码返回列表中的第二个和第四个元素:
cannondale
specialized
Python为访问最后一个列表元素提供了一种特殊语法。通过将索引指
定为-1 ,可让Python返回最后一个列表元素:
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles[-1])
这段代码返回'specialized' 。这种语法很有用,因为你经常需要
在不知道列表长度的情况下访问最后的元素。这种约定也适用于其他
负数索引。例如,索引-2 返回倒数第二个列表元素,索引-3 返回倒
数第三个列表元素,依此类推。
3.1.3 使用列表中的各个值
你可以像使用其他变量一样使用列表中的各个值。例如,可以使用f字
符串根据列表中的值来创建消息。
下面尝试从列表中提取第一款自行车,并使用这个值创建一条消息:
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
❶ message = f"My first bicycle was a {bicycles[0].title()}."
print(message)
我们使用bicycles[0] 的值生成了一个句子,并将其赋给变量
message (见 ❶ )。输出是一个简单的句子,其中包含列表中的第一
款自行车:
My first bicycle was a Trek.
<!-- p91 -->
动 手 试 一 试
请尝试编写一些简短的程序来完成下面的练习,以获得一些使用
Python列表的第一手经验。你可能需要为每章创建一个文件夹,
以整洁有序的方式存储为完成各章的练习而编写的程序。
练 习 3 - 1 : 姓 名 将一些朋友的姓名存储在一个列表中,并将其
命名为names 。依次访问该列表中的每个元素,从而将每个朋友
的姓名打印出来。
练 习 3 - 2 : 问 候 语 继续使用练习3-1中的列表,但不打印每个
朋友的姓名,而为每人打印一条消息。每条消息都包含相同的问
候语,但抬头为相应朋友的姓名。
练 习 3 - 3 : 自 己 的 列 表 想想你喜欢的通勤方式,如骑摩托车或
开汽车,并创建一个包含多种通勤方式的列表。根据该列表打印
一系列有关这些通勤方式的宣言,下面是一个例子。
I would like to own a Honda motorcycle.
<!-- p92 -->
3 . 2 修 改 、 添 加 和 删 除 元 素
你创建的大多数列表将是动态的,这意味着列表创建后,将随着程序
的运行增删元素。例如,你创建一个游戏,要求玩家射杀从天而降的
外星人。为此,可在开始时将一些外星人存储在列表中,然后每当有
外星人被射杀时,都将其从列表中删除,而每次有新的外星人出现在
屏幕上时,都将其添加到列表中。在整个游戏运行期间,外星人列表
的长度将不断变化。
3.2.1 修改列表元素
修改列表元素的语法与访问列表元素的语法类似。要修改列表元素,
可指定列表名和要修改的元素的索引,再指定该元素的新值。
例如,假设有一个摩托车列表,其中的第一个元素为'honda' ,如
何修改它的值呢?
motorcycles.py
❶ motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)
❷ motorcycles[0] = 'ducati'
print(motorcycles)
首先定义一个摩托车列表,其中的第一个元素为'honda' (见 ❶ )。
接下来,将第一个元素的值改为'ducati' (见 ❷ )。输出表明,第
一个元素的值确实变了,但其他列表元素的值没变:
['honda', 'yamaha', 'suzuki']
['ducati', 'yamaha', 'suzuki']
你可以修改任意列表元素的值,而不仅仅是第一个元素的值。
<!-- p93 -->
3.2.2 在列表中添加元素
你可能出于众多原因要在列表中添加新元素。例如,你可能希望游戏
中出现新的外星人、添加可视化数据或给网站添加新注册的用户。
Python提供了多种在既有列表中添加新数据的方式。
- 在 列 表 末 尾 添 加 元 素
在列表中添加新元素时,最简单的方式是将元素附 加
(append)到列表。给列表附加元素时,它将添加到列表末尾。
继续使用前一个示例中的列表,在其末尾添加新元素'ducati'
:
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)
❶ motorcycles.append('ducati')
print(motorcycles)
方法append() 将元素'ducati' 添加到列表末尾(见 ❶ ),而
不影响列表中的其他所有元素:
['honda', 'yamaha', 'suzuki']
['honda', 'yamaha', 'suzuki', 'ducati']
方法append() 让动态地创建列表易如反掌。例如,你可以先创
建一个空列表,再使用一系列函数调用append() 来添加元素。
下面来创建一个空列表,再在其中添加元素'honda'
、'yamaha' 和'suzuki' :
motorcycles = []
motorcycles.append('honda')
motorcycles.append('yamaha')
motorcycles.append('suzuki')
print(motorcycles)
<!-- p94 -->
最终的列表与前述示例中的列表完全相同:
['honda', 'yamaha', 'suzuki']
这种创建列表的方式极其常见,因为经常要等程序运行后,你才
知道用户要在程序中存储哪些数据。为控制用户,可首先创建一
个空列表,用于存储用户将要输入的值,然后将用户提供的每个
新值附加到列表中。
- 在 列 表 中 插 入 元 素
使用方法insert() 可在列表的任何位置添加新元素。为此,你
需要指定新元素的索引和值。
motorcycles = ['honda', 'yamaha', 'suzuki']
❶ motorcycles.insert(0, 'ducati')
print(motorcycles)
在这个示例中,值'ducati' 被插入到了列表开头(见 ❶ )。方
法insert() 在索引0 处添加空间,并将值'ducati' 存储到
这个地方。这种操作将列表中既有的每个元素都右移一个位置:
['ducati', 'honda', 'yamaha', 'suzuki']
3.2.3 从列表中删除元素
你经常需要从列表中删除一个或多个元素。例如,玩家将空中的一个
外星人射杀后,你很可能要将其从存活的外星人列表中删除;当用户
<!-- p95 -->
在你创建的Web应用中注销账户时,你就需要将该用户从活动用户列表
中删除。你可以根据位置或值来删除列表中的元素。
- 使 用 del 语 句 删 除 元 素
如果知道要删除的元素在列表中的位置,可使用del 语句。
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)
❶ del motorcycles[0]
print(motorcycles)
❶ 处的代码使用del 删除了列表motorcycles 中的第一个元
素'honda' :
['honda', 'yamaha', 'suzuki']
['yamaha', 'suzuki']
使用del 可删除任意位置处的列表元素,条件是知道其索引。例
如,下面演示了如何删除前述列表中的第二个元素'yamaha' :
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)
del motorcycles[1]
print(motorcycles)
下面的输出表明,已经将第二款摩托车从列表中删除了:
['honda', 'yamaha', 'suzuki']
['honda', 'suzuki']
<!-- p96 -->
在这两个示例中,使用del 语句将值从列表中删除后,你就无法
再访问它了。
- 使 用 方 法 pop() 删 除 元 素
有时候,你要将元素从列表中删除,并接着使用它的值。例如,
你可能需要获取刚被射杀的外星人的
坐标和
坐标,以便在相
应的位置显示爆炸效果;在Web应用程序中,你可能要将用户从活
跃成员列表中删除,并将其加入到非活跃成员列表中。
方法pop() 删除列表末尾的元素,并让你能够接着使用它。术语
弹 出 (pop)源自这样的类比:列表就像一个栈,而删除列表末
尾的元素相当于弹出栈顶元素。
下面来从列表motorcycles 中弹出一款摩托车:
❶ motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)
❷ popped_motorcycle = motorcycles.pop()
❸ print(motorcycles)
❹ print(popped_motorcycle)
首先定义并打印列表motorcycles (见 ❶ )。接下来,从这个
列表中弹出一个值,并将其赋给变量popped_motorcycle 中
(见 ❷ )。然后打印这个列表,以核实从中删除了一个值(见
❸ )。最后打印弹出的值,以证明我们依然能够访问被删除的值
(见 ❹ )。
输出表明,列表末尾的值'suzuki' 已删除,它现在被赋给了变
量popped_motorcycle :
['honda', 'yamaha', 'suzuki']
['honda', 'yamaha']
suzuki
<!-- p97 -->
方法pop() 有什么用处呢?假设列表中的摩托车是按购买时间存
储的,就可使用方法pop() 打印一条消息,指出最后购买的是哪
款摩托车:
motorcycles = ['honda', 'yamaha', 'suzuki']
last_owned = motorcycles.pop()
print(f"The last motorcycle I owned was a
{last_owned.title()}.")
输出是一个简单的句子,指出了最新购买的是哪款摩托车:
The last motorcycle I owned was a Suzuki.
- 弹 出 列 表 中 任 何 位 置 处 的 元 素
实际上,可以使用pop() 来删除列表中任意位置的元素,只需在
圆括号中指定要删除元素的索引即可。
motorcycles = ['honda', 'yamaha', 'suzuki']
❶ first_owned = motorcycles.pop(0)
❷ print(f"The first motorcycle I owned was a
{first_owned.title()}.")
首先弹出列表中的第一款摩托车(见 ❶ ),然后打印一条有关这
辆摩托车的消息(见 ❷ )。输出是一个简单的句子,描述了我购
买的第一辆摩托车:
The first motorcycle I owned was a Honda.
<!-- p98 -->
别忘了,每当你使用pop() 时,被弹出的元素就不再在列表中
了。
如果你不确定该使用del 语句还是pop() 方法,下面是一个简
单的判断标准:如果你要从列表中删除一个元素,且不再以任何
方式使用它,就使用del 语句;如果你要在删除元素后还能继续
使用它,就使用方法pop() 。
- 根 据 值 删 除 元 素
有时候,你不知道要从列表中删除的值所处的位置。如果只知道
要删除的元素的值,可使用方法remove() 。
例如,假设要从列表motorcycles 中删除值'ducati' 。
motorcycles = ['honda', 'yamaha', 'suzuki', 'ducati']
print(motorcycles)
❶ motorcycles.remove('ducati')
print(motorcycles)
❶ 处的代码让Python确定'ducati' 出现在列表的什么地方,并
将该元素删除:
['honda', 'yamaha', 'suzuki', 'ducati']
['honda', 'yamaha', 'suzuki']
使用remove() 从列表中删除元素时,也可接着使用它的值。下
面删除值'ducati' 并打印一条消息,指出要将其从列表中删除
的原因:
❶ motorcycles = ['honda', 'yamaha', 'suzuki', 'ducati']
print(motorcycles)
❷ too_expensive = 'ducati'
<!-- p99 -->
❸ motorcycles.remove(too_expensive)
print(motorcycles)
❹ print(f"\nA {too_expensive.title()} is too expensive for
me.")
定义列表(见 ❶ )后,将值'ducati' 赋给变量
too_expensive (见 ❷ )。接下来,使用这个变量来告诉
Python将哪个值从列表中删除(见 ❸ )。最后,值'ducati' 已
经从列表中删除,但可通过变量too_expensive 来访问它(见
❹ )。这让我们能够打印一条消息,指出将'ducati' 从列表
motorcycles 中删除的原因:
['honda', 'yamaha', 'suzuki', 'ducati']
['honda', 'yamaha', 'suzuki']
A Ducati is too expensive for me.
注 意 方法remove() 只删除第一个指定的值。如果要删
除的值可能在列表中出现多次,就需要使用循环来确保将每
个值都删除。这将在第7章中介绍。
动 手 试 一 试
下面的练习比第2章的练习要复杂些,但让你有机会以前面介
绍过的各种方式使用列表。
练 习 3 - 4 : 嘉 宾 名 单 如果你可以邀请任何人一起共进晚餐
(无论是在世的还是故去的),你会邀请哪些人?请创建一
个列表,其中包含至少三个你想邀请的人,然后使用这个列
表打印消息,邀请这些人来与你共进晚餐。
练 习 3 - 5 : 修 改 嘉 宾 名 单 你刚得知有位嘉宾无法赴约,因
此需要另外邀请一位嘉宾。
<!-- p100 -->
以完成练习3-4时编写的程序为基础,在程序末尾添加一
条print 语句,指出哪位嘉宾无法赴约。
修改嘉宾名单,将无法赴约的嘉宾的姓名替换为新邀请
的嘉宾的姓名。
再次打印一系列消息,向名单中的每位嘉宾发出邀请。
练 习 3 - 6 : 添 加 嘉 宾 你刚找到了一个更大的餐桌,可容纳
更多的嘉宾。请想想你还想邀请哪三位嘉宾。
以完成练习3-4或练习3-5时编写的程序为基础,在程序
末尾添加一条print 语句,指出你找到了一个更大的餐
桌。
使用insert() 将一位新嘉宾添加到名单开头。
使用insert() 将另一位新嘉宾添加到名单中间。
使用append() 将最后一位新嘉宾添加到名单末尾。
打印一系列消息,向名单中的每位嘉宾发出邀请。
练 习 3 - 7 : 缩 减 名 单 你刚得知新购买的餐桌无法及时送
达,因此只能邀请两位嘉宾。
以完成练习3-6时编写的程序为基础,在程序末尾添加一
行代码,打印一条你只能邀请两位嘉宾共进晚餐的消
息。
使用pop() 不断地删除名单中的嘉宾,直到只有两位嘉
宾为止。每次从名单中弹出一位嘉宾时,都打印一条消
息,让该嘉宾知悉你很抱歉,无法邀请他来共进晚餐。
对于余下两位嘉宾中的每一位,都打印一条消息,指出
他依然在受邀人之列。
使用del 将最后两位嘉宾从名单中删除,让名单变成空
的。打印该名单,核实程序结束时名单确实是空的。
<!-- p101 -->
3.3 组织列表
在你创建的列表中,元素的排列顺序常常是无法预测的,因为你
并非总能控制用户提供数据的顺序。这虽然在大多数情况下是不
可避免的,但你经常需要以特定的顺序呈现信息。有时候,你希
望保留列表元素最初的排列顺序,而有时候又需要调整排列顺
序。Python提供了很多组织列表的方式,可根据具体情况选用。
3 . 3 . 1 使 用 方 法 sort() 对 列 表 永 久 排 序
Python方法sort() 让你能够较为轻松地对列表进行排序。假设
你有一个汽车列表,并要让其中的汽车按字母顺序排列。为简化
这项任务,假设该列表中的所有值都是小写的。
cars.py
cars = ['bmw', 'audi', 'toyota', 'subaru']
❶ cars.sort()
print(cars)
方法sort() (见 ❶ )永久性地修改列表元素的排列顺序。现
在,汽车是按字母顺序排列的,再也无法恢复到原来的排列顺
序:
['audi', 'bmw', 'subaru', 'toyota']
还可以按与字母顺序相反的顺序排列列表元素,只需向sort()
方法传递参数reverse=True 即可。下面的示例将汽车列表按
与字母顺序相反的顺序排列:
cars = ['bmw', 'audi', 'toyota', 'subaru']
cars.sort(reverse=True)
print(cars)
<!-- p102 -->
同样,对列表元素排列顺序的修改是永久性的:
['toyota', 'subaru', 'bmw', 'audi']
3 . 3 . 2 使 用 函 数 sorted() 对 列 表 临 时 排 序
要保留列表元素原来的排列顺序,同时以特定的顺序呈现它们,
可使用函数sorted() 。函数sorted() 让你能够按特定顺序
显示列表元素,同时不影响它们在列表中的原始排列顺序。
下面尝试来对汽车列表调用这个函数。
cars = ['bmw', 'audi', 'toyota', 'subaru']
❶ print("Here is the original list:")
print(cars)
❷ print("\nHere is the sorted list:")
print(sorted(cars))
❸ print("\nHere is the original list again:")
print(cars)
首先按原始顺序打印列表(见 ❶ ),再按字母顺序显示该列表
(见 ❷ )。以特定顺序显示列表后,我们进行核实,确认列表元
素的排列顺序与以前相同(见 ❸ )。
Here is the original list:
['bmw', 'audi', 'toyota', 'subaru']
Here is the sorted list:
['audi', 'bmw', 'subaru', 'toyota']
❹ Here is the original list again:
['bmw', 'audi', 'toyota', 'subaru']
<!-- p103 -->
注意,调用函数sorted() 后,列表元素的排列顺序并没有变
(见 ❹ )。如果要按与字母顺序相反的顺序显示列表,也可向函
数sorted() 传递参数reverse=True 。
注 意 在并非所有的值都是小写时,按字母顺序排列列表
要复杂些。决定排列顺序时,有多种解读大写字母的方式,
要指定准确的排列顺序,可能比我们这里所做的要复杂。然
而,大多数排序方式是以本节介绍的知识为基础的。
3.3.3 倒着打印列表
要反转列表元素的排列顺序,可使用方法reverse() 。假设汽
车列表是按购买时间排列的,可轻松地按相反的顺序排列其中的
汽车:
cars = ['bmw', 'audi', 'toyota', 'subaru']
print(cars)
cars.reverse()
print(cars)
注意,reverse() 不是按与字母顺序相反的顺序排列列表元
素,而只是反转列表元素的排列顺序:
['bmw', 'audi', 'toyota', 'subaru']
['subaru', 'toyota', 'audi', 'bmw']
方法reverse() 永久性地修改列表元素的排列顺序,但可随时
恢复到原来的排列顺序,只需对列表再次调用reverse() 即
可。
3.3.4 确定列表的长度
<!-- p104 -->
使用函数len() 可快速获悉列表的长度。在下面的示例中,列表
包含四个元素,因此其长度为4 :
>> cars = ['bmw', 'audi', 'toyota', 'subaru']
>> len(cars)
需要完成如下任务时,len() 很有用:明确还有多少个外星人未
被射杀,确定需要管理多少项可视化数据,计算网站有多少注册
用户,等等。
注 意 Python计算列表元素数时从1开始,因此确定列表长
度时,你应该不会遇到差一错误。
动 手 试 一 试
练 习 3 - 8 : 放 眼 世 界 想出至少5个你渴望去旅游的地方。
将这些地方存储在一个列表中,并确保其中的元素不是按字
母顺序排列的。
按原始排列顺序打印该列表。不要考虑输出是否整洁的问
题,只管打印原始Python列表。
使用sorted() 按字母顺序打印这个列表,同时不要修
改它。
再次打印该列表,核实排列顺序未变。
使用sorted() 按与字母顺序相反的顺序打印这个列
表,同时不要修改它。
再次打印该列表,核实排列顺序未变。
使用reverse() 修改列表元素的排列顺序。打印该列
表,核实排列顺序确实变了。
使用reverse() 再次修改列表元素的排列顺序。打印
该列表,核实已恢复到原来的排列顺序。
<!-- p105 -->
使用sort() 修改该列表,使其元素按字母顺序排列。
打印该列表,核实排列顺序确实变了。
使用sort() 修改该列表,使其元素按与字母顺序相反
的顺序排列。打印该列表,核实排列顺序确实变了。
练 习 3 - 9 : 晚 餐 嘉 宾 在完成练习3-4~练习3-7时编写的程
序之一中,使用len() 打印一条消息,指出你邀请了多少位
嘉宾来共进晚餐。
练 习 3 - 1 0 : 尝 试 使 用 各 个 函 数 想想可存储到列表中的东
西,如山川、河流、国家、城市、语言或你喜欢的任何东
西。编写一个程序,在其中创建一个包含这些元素的列表,
然后,对于本章介绍的每个函数,都至少使用一次来处理这
个列表。
<!-- p106 -->
3.4 使用列表时避免索引错误
刚开始使用列表时,经常会遇到一种错误。假设你有一个包含三
个元素的列表,却要求获取第四个元素:
motorcycles.py
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles[3])
这将导致索 引 错 误 :
Traceback (most recent call last):
File "motorcycles.py", line 2, in <module>
print(motorcycles[3])
IndexError: list index out of range
Python试图向你提供位于索引3 处的元素,但它搜索列表
motorcycles 时,却发现索引3 处没有元素。鉴于列表索引差
一的特征,这种错误很常见。有些人从1开始数,因此以为第三个
元素的索引为3。然而在Python中,第三个元素的索引为2,因为
索引是从0开始的。
索引错误意味着Python在指定索引处找不到元素。程序发生索引
错误时,请尝试将指定的索引减1,然后再次运行程序,看看结果
是否正确。
别忘了,每当需要访问最后一个列表元素时,都可使用索引-1
。这在任何情况下都行之有效,即便你最后一次访问列表后,其
长度发生了变化:
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles[-1])
<!-- p107 -->
索引-1 总是返回最后一个列表元素,这里为值'suzuki' :
'suzuki'
仅当列表为空时,这种访问最后一个元素的方式才会导致错误:
motorcycles = []
print(motorcycles[-1])
列表motorcycles 不包含任何元素,因此Python返回一条索引
错误消息:
Traceback (most recent call last):
File "motorcyles.py", line 3, in <module>
print(motorcycles[-1])
IndexError: list index out of range
注 意 发生索引错误却找不到解决办法时,请尝试将列表
或其长度打印出来。列表可能与你以为的截然不同,在程序
对其进行了动态处理时尤其如此。通过查看列表或其包含的
元素数,可帮助你找出这种逻辑错误。
动 手 试 一 试
练 习 3 - 1 1 : 有 意 引 发 错 误 如果你还没有在程序中遇到过
索引错误,就尝试引发一个这种错误。在你的一个程序中,
修改其中的索引,以引发索引错误。关闭程序前,务必消除
这个错误。
<!-- p108 -->
3.5 小结
在本章中,你学习了:列表是什么以及如何使用其中的元素;如
何定义列表以及如何增删元素;如何对列表进行永久性排序,以
及如何为展示列表而进行临时排序;如何确定列表的长度,以及
在使用列表时如何避免索引错误。
在第4章,你将学习如何以更高效的方式处理列表元素。通过使用
为数不多的几行代码来遍历列表元素,你就能高效地处理它们,
即便列表包含数千乃至数百万个元素。
<!-- p109 -->
第 4 章 操作列表
在第3章,你学习了如何创建简单的列表,还学
习了如何操作列表元素。在本章中,你将学习如何遍 历 整
个列表,这只需要几行代码,无论列表有多长。循环让你能
够对列表的每个元素都采取一个或一系列相同的措施,从而
高效地处理任何长度的列表,包括包含数千乃至数百万个元
素的列表。
<!-- p110 -->
4.1 遍历整个列表
你经常需要遍历列表的所有元素,对每个元素执行相同的操作。
例如,在游戏中,可能需要将每个界面元素平移相同的距离;对
于包含数字的列表,可能需要对每个元素执行相同的统计运算;
在网站中,可能需要显示文章列表中的每个标题。需要对列表中
的每个元素都执行相同的操作时,可使用Python中的for 循环。
假设我们有一个魔术师名单,需要将其中每个魔术师的名字都打
印出来。为此,可以分别获取名单中的每个名字,但这种做法会
导致多个问题。例如,如果名单很长,将包含大量重复的代码。
另外,每当名单的长度发生变化时,都必须修改代码。通过使用
for 循环,可以让Python去处理这些问题。
下面使用for 循环来打印魔术师名单中的所有名字:
magicians.py
❶ magicians = ['alice', 'david', 'carolina']
❷ for magician in magicians:
❸ print(magician)
首先,像第3章那样定义一个列表(见 ❶ )。接下来,定义一个
for 循环(见 ❷ )。这行代码让Python从列表magicians 中取
出一个名字,并将其与变量magician 相关联。最后,让Python
打印前面赋给变量magician 的名字(见 ❸ )。这样,对于列表
中的每个名字,Python都将重复执行 ❷ 处和 ❸ 处的代码行。你可
以这样解读这些代码:对于列表magicians 中的每位魔术师,
都将其名字打印出来。输出很简单,就是列表中所有的名字:
alice
david
carolina
<!-- p111 -->
4.1.1 深入研究循环
循环这种概念很重要,因为它是让计算机自动完成重复工作的常
见方式之一。例如,在前面magicians.py中使用的简单循环里,
Python将首先读取其中的第一行代码:
for magician in magicians:
这行代码让Python获取列表magicians 中的第一个值'alice'
,并将其与变量magician 相关联。接下来,Python读取下一行
代码:
print(magician)
它让Python打印magician 的值,依然是'alice' 。鉴于该列
表还包含其他值,Python返回到循环的第一行:
for magician in magicians:
Python获取列表中的下一个名字'david',并将其与变量magician
相关联,再执行下面这行代码:
print(magician)
Python再次打印变量magician 的值,当前为'david' 。接下
来,Python再次执行整个循环,对列表中的最后一个
值'carolina' 进行处理。至此,列表中没有其他的值了,因此
<!-- p112 -->
Python接着执行程序的下一行代码。在这个示例中,for 循环后
面没有其他代码,因此程序就此结束。
刚开始使用循环时请牢记,对列表中的每个元素,都将执行循环
指定的步骤,而不管列表包含多少个元素。如果列表包含一百万
个元素,Python就重复执行指定的步骤一百万次,且通常速度非
常快。
另外,编写for 循环时,可以给依次与列表中每个值相关联的临
时变量指定任意名称。然而,选择描述单个列表元素的有意义名
称大有裨益。例如,对于小猫列表、小狗列表和一般性列表,像
下面这样编写for 循环的第一行代码是不错的选择:
for cat in cats:
for dog in dogs:
for item in list_of_items:
这些命名约定有助于你明白for 循环中将对每个元素执行的操
作。使用单数和复数式名称,可帮助你判断代码段处理的是单个
列表元素还是整个列表。
4.1.2 在 for 循环中执行更多操作
在for 循环中,可对每个元素执行任何操作。下面来扩展前面的
示例,对于每位魔术师,都打印一条消息,指出他的表演太精彩
了。
magicians.py
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
❶ print(f"{magician.title()}, that was a great trick!")
相比于前一个示例,唯一的不同是为每位魔术师打印了一条以其
名字为抬头的消息(见 ❶ )。这个循环第一次迭代时,变量
<!-- p113 -->
magician 的值为'alice' ,因此Python打印的第一条消息的
抬头为'Alice' ;第二次迭代时,消息的抬头为'David' ;第
三次迭代时,抬头为'Carolina' 。
下面的输出表明,对于列表中的每位魔术师,都打印了一条个性
化消息:
Alice, that was a great trick!
David, that was a great trick!
Carolina, that was a great trick!
在for 循环中,想包含多少行代码都可以。在代码行for
magician in magicians 后面,每个缩进的代码行都是循环
的一部分,将针对列表中的每个值都执行一次。因此,可对列表
中的每个值执行任意次数的操作。
下面再添加一行代码,告诉每位魔术师,我们期待他的下一次表
演:
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
print(f"{magician.title()}, that was a great trick!")
❶ print(f"I can't wait to see your next trick,
{magician.title()}.\n")
两个函数调用print() 都缩进了,因此它们都将针对列表中的
每位魔术师执行一次。第二个函数调用print() 中的换行
符"\n" (见 ❶ )在每次迭代结束后都插入一个空行,从而整洁
地将针对各位魔术师的消息编组:
Alice, that was a great trick!
I can't wait to see your next trick, Alice.
David, that was a great trick!
I can't wait to see your next trick, David.
Carolina, that was a great trick!
<!-- p114 -->
I can't wait to see your next trick, Carolina.
在for 循环中,想包含多少行代码都可以。实际上,你会发现使
用for 循环对每个元素执行众多不同的操作很有用。
4 . 1 . 3 在 for 循 环 结 束 后 执 行 一 些 操 作
for 循环结束后怎么办呢?通常,你需要提供总结性输出或接着
执行程序必须完成的其他任务。
在for 循环后面,没有缩进的代码都只执行一次,不会重复执
行。下面来打印一条向全体魔术师致谢的消息,感谢他们的精彩
表演。想要在打印给各位魔术师的消息后面打印一条给全体魔术
师的致谢消息,需要将相应的代码放在for 循环后面,且不缩
进:
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
print(f"{magician.title()}, that was a great trick!")
print(f"I can't wait to see your next trick,
{magician.title()}.\n")
❶ print("Thank you, everyone. That was a great magic show!")
你在前面看到了,开头两个函数调用print() 针对列表中的每
位魔术师重复执行。然而,第三个函数调用print() 没有缩进
(见 ❶ ),因此只执行一次:
Alice, that was a great trick!
I can't wait to see your next trick, Alice.
David, that was a great trick!
I can't wait to see your next trick, David.
Carolina, that was a great trick!
I can't wait to see your next trick, Carolina.
Thank you, everyone. That was a great magic show!
<!-- p115 -->
使用for 循环处理数据是一种对数据集执行整体操作的不错方
式。例如,你可能使用for 循环来初始化游戏:遍历角色列表,
将每个角色显示到屏幕上。然后在循环后面添加一个不缩进的代
码块,在屏幕上绘制所有角色后显示一个Play Now按钮。
<!-- p116 -->
4.2 避免缩进错误
Python根据缩进来判断代码行与前一个代码行的关系。在前面的
示例中,向各位魔术师显示消息的代码行是for 循环的一部分,
因为它们缩进了。Python通过使用缩进让代码更易读。简单地
说,它要求你使用缩进让代码整洁而结构清晰。在较长的Python
程序中,你将看到缩进程度各不相同的代码块,从而对程序的组
织结构有大致的认识。
开始编写必须正确缩进的代码时,需要注意一些常见的缩 进 错 误
。例如,程序员有时候会将不需要缩进的代码块缩进,而对于必
须缩进的代码块却忘了缩进。查看这样的错误示例有助于你以后
避开它们,以及在它们出现在程序中时进行修复。
下面来看一些较为常见的缩进错误。
4.2.1 忘记缩进
对于位于for 语句后面且属于循环组成部分的代码行,一定要缩
进。如果忘记缩进, Python会提醒你:
magicians.py
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
❶ print(magician)
函数调用print() (见 ❶ )应缩进却没有缩进。Python没有找到
期望缩进的代码块时,会让你知道哪行代码有问题。
File "magicians.py", line 3
print(magician)
^
IndentationError: expected an indented block
<!-- p117 -->
通常,将紧跟在for 语句后面的代码行缩进,可消除这种缩进错
误。
4.2.2 忘记缩进额外的代码行
有时候,循环能够运行且不会报告错误,但结果可能出人意料。
试图在循环中执行多项任务,却忘记缩进其中的一些代码行时,
就会出现这种情况。
例如,如果忘记缩进循环中的第二行代码(它告诉每位魔术师,
我们期待其下次表演),就会出现这种情况:
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
print(f"{magician.title()}, that was a great trick!")
❶ print(f"I can't wait to see your next trick,
{magician.title()}.\n")
第二个函数调用print() (见 ❶ )原本需要缩进,但Python发现
for 语句后面有一行代码是缩进的,因此没有报告错误。最终的
结果是,对于列表中的每位魔术师,都执行了第一个函数调用
print() ,因为它缩进了;而第二个函数调用print() 没有缩
进,因此只在循环结束后执行一次。由于变量magician 的终值
为'carolina' ,结果只有她收到了消息“looking forward to
the next trick”:
Alice, that was a great trick!
David, that was a great trick!
Carolina, that was a great trick!
I can't wait to see your next trick, Carolina.
这是一个逻 辑 错 误 。从语法上看,这些Python代码是合法的,
但由于存在逻辑错误,结果并不符合预期。如果你预期某项操作
<!-- p118 -->
将针对每个列表元素都执行一次,但它总共只执行了一次,请确
定需要将一行还是多行代码缩进。
4.2.3 不必要的缩进
如果你不小心缩进了无须缩进的代码行,Python将指出这一点:
hello_world.py
message = "Hello Python world!"
❶ print(message)
函数调用print() (见 ❶ )无须缩进,因为它并非循环的组成部
分。因此Python将指出这种错误:
File "hello_world.py", line 2
print(message)
^
IndentationError: unexpected indent
为避免意外缩进错误,请只缩进需要缩进的代码。在前面编写的
程序中,只有要在for 循环中对每个元素执行的代码需要缩进。
4.2.4 循环后不必要的缩进
如果不小心缩进了应在循环结束后执行的代码,这些代码将针对
每个列表元素重复执行。在有些情况下,这可能导致Python报告
语法错误,但在大多数情况下,这只会导致逻辑错误。
例如,如果不小心缩进了感谢全体魔术师精彩表演的代码行,结
果将如何呢?
magicians.py
<!-- p119 -->
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
print(f"{magician.title()}, that was a great trick!")
print(f"I can't wait to see your next trick,
{magician.title()}.\n")
❶ print("Thank you everyone, that was a great magic show!")
由于 ❶ 处的代码行缩进了,它将针对列表中的每位魔术师执行一
次,如下所示:
Alice, that was a great trick!
I can't wait to see your next trick, Alice.
Thank you everyone, that was a great magic show!
David, that was a great trick!
I can't wait to see your next trick, David.
Thank you everyone, that was a great magic show!
Carolina, that was a great trick!
I can't wait to see your next trick, Carolina.
Thank you everyone, that was a great magic show!
这也是一个逻辑错误,与4.2.2节的错误类似。Python不知道你的
本意,只要代码符合语法,它就会运行。如果原本只应执行一次
的操作执行了多次,可能要对执行该操作的代码取消缩进。
4.2.5 遗漏了冒号
for 语句末尾的冒号告诉Python,下一行是循环的第一行。
magicians = ['alice', 'david', 'carolina']
❶ for magician in magicians
print(magician)
<!-- p120 -->
如果不小心遗漏了冒号,如 ❶ 所示,将导致语法错误,因为
Python不知道你意欲何为。这种错误虽然易于消除,但并不那么
容易发现。程序员为找出这样的单字符错误,花费的时间多得令
人惊讶。此类错误之所以难以发现,是因为通常在人们的意料之
外。
动 手 试 一 试
练 习 4 - 1 : 比 萨 想出至少三种你喜欢的比萨,将其名称存
储在一个列表中,再使用for 循环将每种比萨的名称打印出
来。
修改这个for 循环,使其打印包含比萨名称的句子,而
不仅仅是比萨的名称。对于每种比萨,都显示一行输
出,下面是一个例子。
I like pepperoni pizza.
在程序末尾添加一行代码,它不在for 循环中,指出你
有多喜欢比萨。输出应包含针对每种比萨的消息,还有
一个总结性句子,下面是一个例子。
I really love pizza!
练 习 4 - 2 : 动 物 想出至少三种有共同特征的动物,将其名
称存储在一个列表中,再使用for 循环将每种动物的名称打
印出来。
修改这个程序,使其针对每种动物都打印一个句子,下
面是一个例子。
A dog would make a great pet.
在程序末尾添加一行代码,指出这些动物的共同之处,
如打印下面这样的句子。
<!-- p121 -->
Any of these animals would make a great pet!
<!-- p122 -->
4.3 创建数值列表
需要存储一组数的原因有很多。例如,在游戏中,需要跟踪每个
角色的位置,还可能需要跟踪玩家的几个最高得分;在数据可视
化中,处理的几乎都是由数(如温度、距离、人口数量、经度和
纬度等)组成的集合。
列表非常适合用于存储数字集合,而Python提供了很多工具,可
帮助你高效地处理数字列表。明白如何有效地使用这些工具后,
即便列表包含数百万个元素,你编写的代码也能运行得很好。
4.3.1 使用函数 range()
Python函数range() 让你能够轻松地生成一系列数。例如,可
以像下面这样使用函数range() 来打印一系列数:
first_numbers.py
for value in range(1, 5):
print(value)
上述代码好像应该打印数1~5,但实际上不会打印5:
在这个示例中,range() 只打印数1~4。这是编程语言中常见
的差一行为的结果。函数range() 让Python从指定的第一个值
开始数,并在到达你指定的第二个值时停止。因为它在第二个值
处停止,所以输出不包含该值(这里为5)。
<!-- p123 -->
要打印数1~5,需要使用range(1,6) :
for value in range(1, 6):
print(value)
这样,输出将从1开始、到5结束:
使用range() 时,如果输出不符合预期,请尝试将指定的值加1
或减1。
调用函数range() 时,也可只指定一个参数,这样它将从0开
始。例如,range(6) 返回数0~5。
4.3.2 使用 range() 创建数字列表
要创建数字列表,可使用函数list() 将range() 的结果直接
转换为列表。如果将range() 作为list() 的参数,输出将是
一个数字列表。
在前一节的示例中,只是将一系列数打印出来。要将这组数转换
为列表,可使用list() :
numbers = list(range(1, 6))
print(numbers)
结果如下:
<!-- p124 -->
[1, 2, 3, 4, 5]
使用函数range() 时,还可指定步长。为此,可给这个函数指
定第三个参数,Python将根据这个步长来生成数。
例如,下面的代码打印1~10的偶数:
even_numbers.py
even_numbers = list(range(2, 11, 2))
print(even_numbers)
在这个示例中,函数range() 从2开始数,然后不断加2,直到
达到或超过终值(11),因此输出如下:
[2, 4, 6, 8, 10]
使用函数range() 几乎能够创建任何需要的数集。例如,如何
创建一个列表,其中包含前10个整数(1~10)的平方呢?在
Python中,用两个星号(** )表示乘方运算。下面的代码演示
了如何将前10个整数的平方加入一个列表中:
squares.py
❶ squares = []
❷ for value in range(1, 11):
❸ square = value ** 2
❹ squares.append(square)
❺ print(squares)
<!-- p125 -->
首先,创建一个名为squares 的空列表(见 ❶ )。接下来,使用
函数range() 让Python遍历1~10的值(见 ❷ )。在循环中,计
算当前值的平方,并将结果赋给变量square (见 ❸ )。然后,
将新计算得到的平方值附加到列表squares 末尾(见 ❹ )。最
后,循环结束后,打印列表squares (见 ❺ ):
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
为了让代码更简洁,可不使用临时变量square ,而直接将每个
计算得到的值附加到列表末尾:
squares = []
for value in range(1,11):
❶ squares.append(value**2)
print(squares)
❶ 处的代码与squares.py中 ❸ 处和 ❹ 处的代码等效。在循环中,计
算每个值的平方,并立即将结果附加到列表squares 的末尾。
创建更复杂的列表时,可使用上述两种方法中的任何一种。有时
候,使用临时变量会让代码更易读;而在其他情况下,这样做只
会让代码无谓地变长。你首先应该考虑的是,编写清晰易懂且能
完成所需功能的代码,等到审核代码时,再考虑采用更高效的方
法。
4 . 3 . 3 对 数 字 列 表 执 行 简 单 的 统 计 计 算
有几个专门用于处理数字列表的Python函数。例如,你可以轻松
地找出数字列表的最大值、最小值和总和:
>> digits = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
>> min(digits)
>> max(digits)
<!-- p126 -->
>> sum(digits)
注 意 考虑到版面,本节使用的数字列表都很短,但这里
介绍的知识也适用于包含数百万个数的列表。
4.3.4 列表解析
前面介绍的生成列表squares 的方式包含三四行代码,而列表
解析让你只需编写一行代码就能生成这样的列表。列 表 解 析 将
for 循环和创建新元素的代码合并成一行,并自动附加新元素。
面向初学者的书并非都会介绍列表解析,这里之所以介绍列表解
析,是因为等你开始阅读他人编写的代码时,很可能会遇到它。
下面的示例使用列表解析创建你在前面看到的平方数列表:
squares.py
squares = [value**2 for value in range(1, 11)]
print(squares)
要使用这种语法,首先指定一个描述性的列表名,如squares
。然后,指定一个左方括号,并定义一个表达式,用于生成要存
储到列表中的值。在这个示例中,表达式为value**2 ,它计算
平方值。接下来,编写一个for 循环,用于给表达式提供值,再
加上右方括号。在这个示例中,for 循环为for value in
range(1,11) ,它将值1~10提供给表达式value**2 。请注
意,这里的for 语句末尾没有冒号。
结果与前面的平方数列表相同:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
<!-- p127 -->
要创建自己的列表解析,需要经过一定的练习,但能够熟练地创
建常规列表后,你会发现这样做是完全值得的。当你觉得编写三
四行代码来生成列表有点繁复时,就应考虑创建列表解析了。
动 手 试 一 试
练 习 4 - 3 : 数 到 2 0 使用一个for 循环打印数1~
20(含)。
练 习 4 - 4 : 一 百 万 创建一个包含数1~1 000 000的列表,
再使用一个for 循环将这些数打印出来。(如果输出的时间
太长,按Ctrl + C停止输出或关闭输出窗口。)
练 习 4 - 5 : 一 百 万 求 和 创建一个包含数1~1 000 000的列
表,再使用min() 和max() 核实该列表确实是从1开始、到
1 000 000结束的。另外,对这个列表调用函数sum() ,看
看Python将一百万个数相加需要多长时间。
练 习 4 - 6 : 奇 数 通过给函数range() 指定第三个参数来
创建一个列表,其中包含1~20的奇数,再使用一个for 循
环将这些数打印出来。
练 习 4 - 7 : 3 的 倍 数 创建一个列表,其中包含3~30能被3
整除的数,再使用一个for 循环将这个列表中的数打印出
来。
练 习 4 - 8 : 立 方 将同一个数乘三次称为立 方 。例如,在
Python中,2的立方用2**3 表示。请创建一个列表,其中包
含前10个整数(1~10)的立方,再使用一个for 循环将这
些立方数打印出来。
练 习 4 - 9 : 立 方 解 析 使用列表解析生成一个列表,其中包
含前10个整数的立方。
<!-- p128 -->
4.4 使用列表的一部分
在第3章中,你学习了如何访问单个列表元素。在本章中,你一直
在学习如何处理列表的所有元素。你还可以处理列表的部分元
素,Python称之为切 片 。
4.4.1 切片
要创建切片,可指定要使用的第一个元素和最后一个元素的索
引。与函数range() 一样,Python在到达第二个索引之前的元
素后停止。要输出列表中的前三个元素,需要指定索引0和3,这
将返回索引为0、1和2的元素。
下面的示例处理的是一个运动队成员列表:
players.py
players = ['charles', 'martina', 'michael', 'florence',
'eli']
❶ print(players[0:3])
❶ 处的代码打印该列表的一个切片,其中只包含三名队员。输出
也是一个列表,其中包含前三名队员:
['charles', 'martina', 'michael']
你可以生成列表的任意子集。例如,如果要提取列表的第二、第
三和第四个元素,可将起始索引指定为1 ,并将终止索引指定为
4 :
players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[1:4])
<!-- p129 -->
此时,切片始于'martina' 、终于'florence' 。
['martina', 'michael', 'florence']
如果没有指定第一个索引,Python将自动从列表开头开始:
players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[:4])
由于没有指定起始索引,Python从列表开头开始提取:
['charles', 'martina', 'michael', 'florence']
要让切片终止于列表末尾,也可使用类似的语法。例如,如果要
提取从第三个元素到列表末尾的所有元素,可将起始索引指定为
2 ,并省略终止索引:
players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[2:])
Python将返回从第三个元素到列表末尾的所有元素:
['michael', 'florence', 'eli']
无论列表多长,这种语法都能够让你输出从特定位置到列表末尾
的所有元素。上一章说过,负数索引返回离列表末尾相应距离的
元素,因此你可以输出列表末尾的任意切片。例如,如果要输出
名单上的最后三名队员,可使用切片players[-3:] :
<!-- p130 -->
players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[-3:])
上述代码打印最后三名队员的名字,即便队员名单长度发生变
化,也依然如此。
注 意 可在表示切片的方括号内指定第三个值。这个值告
诉Python在指定范围内每隔多少元素提取一个。
4.4.2 遍历切片
如果要遍历列表的部分元素,可在for 循环中使用切片。下面的
示例遍历前三名队员,并打印他们的名字:
players = ['charles', 'martina', 'michael', 'florence',
'eli']
print("Here are the first three players on my team:")
❶ for player in players[:3]:
print(player.title())
❶ 处的代码没有遍历整个队员列表,而只遍历前三名队员:
Here are the first three players on my team:
Charles
Martina
Michael
在很多情况下,切片都很有用。例如,编写游戏时,你可以在玩
家退出游戏时将其最终得分加入一个列表中,然后将该列表按降
序排列以获取三个最高得分,再创建一个只包含前三个得分的切
片;处理数据时,可使用切片来进行批量处理;编写Web应用程序
时,可使用切片来分页显示信息,并在每页显示数量合适的信
息。
<!-- p131 -->
4.4.3 复制列表
我们经常需要根据既有列表创建全新的列表。下面来介绍复制列
表的工作原理,以及复制列表可提供极大帮助的一种情形。
要复制列表,可创建一个包含整个列表的切片,方法是同时省略
起始索引和终止索引([:] )。这让Python创建一个始于第一个
元素、终止于最后一个元素的切片,即整个列表的副本。
例如,假设有一个列表包含你最喜欢的四种食品,而你想再创建
一个列表,并在其中包含一位朋友喜欢的所有食品。不过,你喜
欢的食品,这位朋友也都喜欢,因此可通过复制来创建这个列
表:
foods.py
❶ my_foods = ['pizza', 'falafel', 'carrot cake']
❷ friend_foods = my_foods[:]
print("My favorite foods are:")
print(my_foods)
print("\nMy friend's favorite foods are:")
print(friend_foods)
首先,创建一个你喜欢的食品列表,名为my_foods (见 ❶ )。
然后创建一个名为friend_foods 的新列表(见 ❷ )。在不指定
任何索引的情况下从列表my_foods 中提取一个切片,从而创建
这个列表的副本,并将该副本赋给变量friend_foods 。打印
这两个列表后,我们发现其包含的食品相同:
My favorite foods are:
['pizza', 'falafel', 'carrot cake']
My friend's favorite foods are:
['pizza', 'falafel', 'carrot cake']
<!-- p132 -->
为核实确实有两个列表,下面在每个列表中都添加一种食品,并
核实每个列表都记录了相应人员喜欢的食品:
my_foods = ['pizza', 'falafel', 'carrot cake']
❶ friend_foods = my_foods[:]
❷ my_foods.append('cannoli')
❸ friend_foods.append('ice cream')
print("My favorite foods are:")
print(my_foods)
print("\nMy friend's favorite foods are:")
print(friend_foods)
与前一个示例一样,首先将my_foods 的元素复制到新列表
friend_foods 中(见 ❶ )。接下来,在每个列表中都添加一种
食品:在列表my_foods 中添加'cannoli' (见 ❷ ),而在
friend_foods 中添加'ice cream' (见 ❸ )。最后,打印
这两个列表,核实这两种食品分别包含在正确的列表中。
My favorite foods are:
❹ ['pizza', 'falafel', 'carrot cake', 'cannoli']
My friend's favorite foods are:
❺ ['pizza', 'falafel', 'carrot cake', 'ice cream']
❹ 处的输出表明,'cannoli' 包含在你喜欢的食品列表中,
而'ice cream' 不在。 ❺ 处的输出表明,'ice cream' 包含
在你朋友喜欢的食品列表中,而'cannoli' 不在。如果只是将
my_foods 赋给friend_foods ,就不能得到两个列表。例
如,下面演示了在不使用切片的情况下复制列表的情况:
my_foods = ['pizza', 'falafel', 'carrot cake']
这行不通 :
❶ friend_foods = my_foods
<!-- p133 -->
my_foods.append('cannoli')
friend_foods.append('ice cream')
print("My favorite foods are:")
print(my_foods)
print("\nMy friend's favorite foods are:")
print(friend_foods)
这里将my_foods 赋给friend_foods ,而不是将my_foods
的副本赋给friend_foods (见 ❶ )。这种语法实际上是让
Python将新变量friend_foods 关联到已与my_foods 相关联
的列表,因此这两个变量指向同一个列表。有鉴于此,当我们
将'cannoli' 添加到my_foods 中时,它也将出现在
friend_foods 中。同样,虽然'ice cream' 好像只被加入
到了friend_foods 中,但它也将出现在这两个列表中。
输出表明,两个列表是相同的,这并非我们想要的结果:
My favorite foods are:
['pizza', 'falafel', 'carrot cake', 'cannoli', 'ice cream']
My friend's favorite foods are:
['pizza', 'falafel', 'carrot cake', 'cannoli', 'ice cream']
注 意 暂时不要考虑这个示例中的细节。当试图使用列表
的副本时结果出乎意料,基本上都要确认你是否像第一个示
例那样使用切片复制了列表。
动 手 试 一 试
练 习 4 - 1 0 : 切 片 选择你在本章编写的一个程序,在末尾
添加几行代码,以完成如下任务。
<!-- p134 -->
打印消息“The first three items in the list
are:”,再使用切片来打印列表的前三个元素。
打印消息“Three items from the middle of the list
are:”,再使用切片来打印列表的中间三个元素。
打印消息“The last three items in the list
are:”,再使用切片来打印列表的末尾三个元素。
练 习 4 - 1 1 : 你 的 比 萨 , 我 的 比 萨 在你为完成练习4-1而编
写的程序中,创建比萨列表的副本,并将其赋给变量
friend_pizzas ,再完成如下任务。
在原来的比萨列表中添加一种比萨。
在列表friend_pizzas 中添加另一种比萨。
核实有两个不同的列表。为此,打印消息“My favorite
pizzas are:”,再使用一个for 循环来打印第一个列
表;打印消息“My friend's favorite pizzas
are:”,再使用一个for 循环来打印第二个列表。核实
新增的比萨被添加到了正确的列表中。
练 习 4 - 1 2 : 使 用 多 个 循 环 在本节中,为节省篇幅,程序
foods.py的每个版本都没有使用for 循环来打印列表。请选
择一个版本的foods.py,在其中编写两个for 循环,将各个
食品列表打印出来。
<!-- p135 -->
4.5 元组
列表非常适合用于存储在程序运行期间可能变化的数据集。列表
是可以修改的,这对处理网站的用户列表或游戏中的角色列表至
关重要。然而,有时候你需要创建一系列不可修改的元素,元组
可以满足这种需求。Python将不能修改的值称为不 可 变 的 ,而
不可变的列表被称为元 组 。
4.5.1 定义元组
元组看起来很像列表,但使用圆括号而非中括号来标识。定义元
组后,就可使用索引来访问其元素,就像访问列表元素一样。
例如,如果有一个大小不应改变的矩形,可将其长度和宽度存储
在一个元组中,从而确保它们是不能修改的:
dimensions.py
❶ dimensions = (200, 50)
❷ print(dimensions[0])
print(dimensions[1])
首先定义元组dimensions (见 ❶ ),为此使用了圆括号而不是
方括号。接下来,分别打印该元组的各个元素,使用的语法与访
问列表元素时使用的语法相同(见 ❷ ):
下面来尝试修改元组dimensions 的一个元素,看看结果如何:
dimensions = (200, 50)
❶ dimensions[0] = 250
<!-- p136 -->
❶ 处的代码试图修改第一个元素的值,导致Python返回类型错误
消息。由于试图修改元组的操作是被禁止的,因此Python指出不
能给元组的元素赋值:
Traceback (most recent call last):
File "dimensions.py", line 2, in <module>
dimensions[0] = 250
TypeError: 'tuple' object does not support item assignment
这很好,因为我们希望Python在代码试图修改矩形的尺寸时引发
错误。
注 意 严格地说,元组是由逗号标识的,圆括号只是让元
组看起来更整洁、更清晰。如果你要定义只包含一个元素的
元组,必须在这个元素后面加上逗号:
my_t = (3,)
创建只包含一个元素的元组通常没有意义,但自动生成的元
组有可能只有一个元素。
4.5.2 遍历元组中的所有值
像列表一样,也可以使用for 循环来遍历元组中的所有值:
dimensions = (200, 50)
for dimension in dimensions:
print(dimension)
就像遍历列表时一样,Python返回元组中所有的元素:
<!-- p137 -->
4.5.3 修改元组变量
虽然不能修改元组的元素,但可以给存储元组的变量赋值。因
此,如果要修改前述矩形的尺寸,可重新定义整个元组:
❶ dimensions = (200, 50)
print("Original dimensions:")
for dimension in dimensions:
print(dimension)
❷ dimensions = (400, 100)
❸ print("\nModified dimensions:")
for dimension in dimensions:
print(dimension)
首先定义一个元组,并将其存储的尺寸打印出来(见 ❶ )。接下
来,将一个新元组关联到变量dimensions (见 ❷ )。然后,打
印新的尺寸(见 ❸ )。这次,Python不会引发任何错误,因为给
元组变量重新赋值是合法的:
Original dimensions:
Modified dimensions:
相比于列表,元组是更简单的数据结构。如果需要存储的一组值
在程序的整个生命周期内都不变,就可以使用元组。
动 手 试 一 试
<!-- p138 -->
练 习 4 - 1 3 : 自 助 餐 有一家自助式餐馆,只提供五种简单
的食品。请想出五种简单的食品,并将其存储在一个元组
中。
使用一个for 循环将该餐馆提供的五种食品都打印出
来。
尝试修改其中的一个元素,核实Python确实会拒绝你这
样做。
餐馆调整了菜单,替换了它提供的其中两种食品。请编
写一个这样的代码块:给元组变量赋值,并使用一个
for 循环将新元组的每个元素都打印出来。
<!-- p139 -->
4.6 设置代码格式
随着你编写的程序越来越长,有必要了解一些代码格式设置约
定。请花时间让你的代码尽可能易于阅读。这有助于你掌握程序
是做什么的,也可以帮助他人理解你编写的代码。
为确保所有人编写的代码结构都大致一致,Python程序员会遵循
一些格式设置约定。学会编写整洁的Python后,就能明白他人编
写的Python代码的整体结构——只要他们和你遵循相同的指南。
要成为专业程序员,应从现在开始就遵循这些指南,以养成良好
的习惯。
4.6.1 格式设置指南
要提出Python语言修改建议,需要编写P y t h o n 改 进 提 案
(Python Enhancement Proposal,PEP)。P E P 8 是最古老的
PEP之一,向Python程序员提供了代码格式设置指南。PEP 8的篇
幅很长,但基本上与复杂的编码结构相关。
Python格式设置指南的编写者深知,代码被阅读的次数比编写的
次数多。代码编写出来后,调试时需要阅读;给程序添加新功能
时,需要花很长的时间阅读;与其他程序员分享代码时,这些程
序员也会阅读。
如果一定要在让代码易于编写和易于阅读之间做出选择,Python
程序员几乎总是选择后者。下面的指南可帮助你从一开始就编写
出清晰的代码。
4.6.2 缩进
PEP 8建议每级缩进都使用四个空格。这既可提高可读性,又留下
了足够的多级缩进空间。
在字处理文档中,大家常常使用制表符而不是空格来缩进。对于
字处理文档来说,这样做的效果很好,但混合使用制表符和空格
<!-- p140 -->
会让Python解释器感到迷惑。每款文本编辑器都提供了一种设
置,可将你输入的制表符转换为指定数量的空格。你在编写代码
时绝对应该使用制表符键,但一定要对编辑器进行设置,使其在
文档中插入空格而不是制表符。
在程序中混合使用制表符和空格可能导致极难排查的问题。如果
混合使用了制表符和空格,可将文件中的所有制表符都转换为空
格,大多数编辑器提供了这样的功能。
4.6.3 行长
很多Python程序员建议每行不超过80字符。最初制定这样的指南
时,在大多数计算机中,终端窗口每行只能容纳79字符。当前,
计算机屏幕每行可容纳的字符数多得多,为何还要使用79字符的
标准行长呢?这里有别的原因。专业程序员通常会在同一个屏幕
上打开多个文件,使用标准行长可以让他们在屏幕上并排打开两
三个文件时同时看到各个文件的完整行。PEP 8还建议注释的行长
不应超过72字符,因为有些工具为大型项目自动生成文档时,会
在每行注释开头添加格式化字符。
PEP 8中有关行长的指南并非不可逾越的红线,有些小组将最大行
长设置为99字符。在学习期间,你不用过多考虑代码的行长,但
别忘了,协作编写程序时,大家几乎都遵守PEP 8指南。在大多数
编辑器中,可以设置一个视觉标志(通常是一条竖线),让你知
道不能越过的界线在什么地方。
注 意 附录B介绍了如何配置文本编辑器,使其在你按制表
符键时插入四个空格,并且显示一条垂直参考线,帮助你遵
守行长不超过79字符的约定。
4.6.4 空行
要将程序的不同部分分开,可使用空行。你应该使用空行来组织
程序文件,但也不能滥用。只要按本书的示例展示的那样做,就
能掌握其中的平衡。例如,如果你有五行创建列表的代码,还有
三行处理该列表的代码,那么用一个空行将这两部分隔开是合适
的。然而,你不应使用三四个空行将其隔开。
<!-- p141 -->
空行不会影响代码的运行,但会影响代码的可读性。Python解释
器根据水平缩进情况来解读代码,但不关心垂直间距。
4.6.5 其他格式设置指南
PEP 8还有很多其他的格式设置建议,但这些指南针对的程序大多
比目前为止本书提到的程序复杂。等介绍更复杂的Python结构
时,我们再来分享相关的PEP 8指南。
动 手 试 一 试
练 习 4 - 1 4 : P E P 8 请访问Python网站并搜索“PEP 8 —
Style Guide for Python Code”,阅读PEP 8格式设置指
南。当前,这些指南适用的情况不多,但可以大致浏览一
下。
练 习 4 - 1 5 : 代 码 审 核 从本章编写的程序中选择三个,根
据PEP 8指南对它们进行修改。
每级缩进都使用四个空格。对你使用的文本编辑器进行
设置,使其在你按Tab键时插入四个空格。如果你还没有
这样做,现在就去做吧(有关如何设置,请参阅附录
B)。
每行都不要超过80字符。对你使用的编辑器进行设置,
使其在第80个字符处显示一条垂直参考线。
不要在程序文件中过多使用空行。
<!-- p142 -->
4.7 小结
在本章中,你学习了:如何高效地处理列表中的元素;如何使用
for 循环遍历列表,Python如何根据缩进来确定程序的结构,以
及如何避免一些常见的缩进错误;如何创建简单的数字列表,以
及可对数字列表执行的一些操作;如何通过切片来使用列表的一
部分和复制列表。你还学习了元组(它对不应变化的值提供了一
定程度的保护),以及在代码变得越来越复杂时如何设置格式,
使其易于阅读。
在第5章中,你将学习如何使用if 语句在不同的条件下采取不同
的措施;如何将一组较复杂的条件测试组合起来,并在满足特定
条件时采取相应的措施。你还将学习如何在遍历列表时,通过使
用if 语句对特定元素采取特定的措施。
<!-- p143 -->
第 5 章 if 语句
编程时经常需要检查一系列条件,并据此决定
采取什么措施。在Python中,if 语句让你能够检查程序的
当前状态,并采取相应的措施。
在本章中,你将学习条件测试,以检查所关心的任何条件。
你将学习简单的if 语句,以及创建一系列复杂的if 语句来
确定当前到底处于什么情形。接下来,你将把学到的知识应
用于列表,编写一个for 循环,以一种方式处理列表中的大
多数元素,并以另一种方式处理包含特定值的元素。
<!-- p144 -->
5.1 一个简单示例
下面是一个简短的示例,演示了如何使用if 语句来正确地处理
特殊情形。假设你有一个汽车列表,并想将其中每辆汽车的名称
打印出来。对于大多数汽车,应以首字母大写的方式打印其名
称,但对于汽车名'bmw' ,应以全大写的方式打印。下面的代码
遍历这个列表,并以首字母大写的方式打印其中的汽车名,不过
对于'bmw' ,则以全大写的方式打印:
cars.py
cars = ['audi', 'bmw', 'subaru', 'toyota']
for car in cars:
❶ if car == 'bmw':
print(car.upper())
else:
print(car.title())
这个示例中的循环首先检查当前的汽车名是否是'bmw' (见
❶ )。如果是,就以全大写方式打印,否则以首字母大写的方式
打印:
Audi
BMW
Subaru
Toyota
这个示例涵盖了本章将介绍的很多概念。下面先来介绍可用来在
程序中检查条件的测试。
<!-- p145 -->
5.2 条件测试
每条if 语句的核心都是一个值为True 或False 的表达式,这
种表达式称为条 件 测 试 。Python根据条件测试的值为True 还是
False 来决定是否执行if 语句中的代码。如果条件测试的值为
True ,Python就执行紧跟在if 语句后面的代码;如果为
False ,Python就忽略这些代码。
5.2.1 检查是否相等
大多数条件测试将一个变量的当前值同特定值进行比较。最简单
的条件测试检查变量的值是否与特定值相等:
❶ >>> car = 'bmw'
❷ >>> car == 'bmw'
True
首先使用一个等号将car 的值设置为'bmw' (见 ❶ ),这种做
法你已经见过很多次。接下来,使用两个等号(== )检查car
的值是否为'bmw' (见 ❷ )。这个相 等 运 算 符 在两边的值相等
时返回True ,否则返回False 。在本例中,两边的值相等,因
此Python返回True 。
如果变量car 的值不是'bmw' ,上述测试将返回False :
❶ >>> car = 'audi'
❷ >>> car == 'bmw'
False
一个等号是陈述,于是 ❶ 处的代码可解读为:将变量car 的值设
置为'audi' 。两个等号则是发问,于是 ❷ 处的代码可解读为:
<!-- p146 -->
变量car 的值是'bmw' 吗?大多数编程语言使用等号的方式与
这里演示的相同。
5.2.2 检查是否相等时忽略大小写
在Python中检查是否相等时区分大小写。例如,两个大小写不同
的值被视为不相等:
>> car = 'Audi'
>> car == 'audi'
False
如果大小写很重要,这种行为有其优点。但如果大小写无关紧
要,只想检查变量的值,可将变量的值转换为小写,再进行比
较:
>> car = 'Audi'
>> car.lower() == 'audi'
True
无论值'Audi' 的大小写如何,上述测试都将返回True ,因为
该测试不区分大小写。函数lower() 不会修改最初赋给变量car
的值,因此进行这样的比较时不会影响原来的变量:
❶ >>> car = 'Audi'
❷ >>> car.lower() == 'audi'
True
❸ >>> car
'Audi'
在 ❶ 处,将首字母大写的字符串'Audi' 赋给变量car 。在 ❷
处,获取变量car 的值并将其转换为小写,再将结果与字符
串'audi' 进行比较。这两个字符串相同,因此Python返回True
<!-- p147 -->
。从 ❸ 处的输出可知,方法lower() 并没有影响关联到变量car
的值。
网站采用类似的方式让用户输入的数据符合特定的格式。例如,
网站可能使用类似的测试来确保用户名是独一无二的,而并非只
是与另一个用户名的大小写不同。用户提交新的用户名时,将把
它转换为小写,并与所有既有用户名的小写版本进行比较。执行
这种检查时,如果已经有用户名'john' (不管大小写如何),
则用户提交用户名'John' 时将遭到拒绝。
5.2.3 检查是否不相等
要判断两个值是否不等,可结合使用惊叹号和等号(!= ),其
中的惊叹号表示不 ,其他很多编程语言中也是如此。
下面再使用一条if 语句来演示如何使用不等运算符。我们将把
要求的比萨配料赋给一个变量,再打印一条消息,指出顾客要求
的配料是否是意式小银鱼(anchovies):
toppings.py
requested_topping = 'mushrooms'
❸ if requested_topping != 'anchovies':
print("Hold the anchovies!")
❸ 处的代码行将requested_topping 的值与'anchovies'
进行比较。如果这两个值不相等,Python将返回True ,进而执
行紧跟在if 语句后面的代码;如果相等,Python将返回False
,因此不执行紧跟在if 语句后面的代码。
因为requested_topping 的值不是'anchovies' ,所以执
行函数调用print() :
Hold the anchovies!
<!-- p148 -->
你编写的大多数条件表达式检查两个值是否相等,但有时候检查
两个值是否不等的效率更高。
5.2.4 数值比较
检查数值非常简单。例如,下面的代码检查一个人是否是18岁:
>> age = 18
>> age == 18
True
还可检查两个数是否不等。例如,下面的代码在提供的答案不正
确时打印一条消息:
magic_number.py
answer = 17
❶ if answer != 42:
print("That is not the correct answer. Please try
again!")
answer 的值(17 )不是42 , ❶ 处的条件得到满足,因此缩进
的代码块得以执行:
That is not the correct answer. Please try again!
条件语句中可包含各种数学比较,如小于、小于等于、大于、大
于等于:
>> age = 19
>> age < 21
True
>> age <= 21
<!-- p149 -->
True
>> age > 21
False
>> age >= 21
False
在if 语句中可使用各种数学比较,这让你能够直接检查关心的
条件。
5.2.5 检查多个条件
你可能想同时检查多个条件。例如,有时候需要在两个条件都为
True 时才执行相应的操作,而有时候只要求一个条件为True
。在这些情况下,关键字and 和or 可助你一臂之力。
a. 使 用 and 检 查 多 个 条 件
要检查是否两个条件都为True ,可使用关键字and 将两个
条件测试合而为一。如果每个测试都通过了,整个表达式就
为True ;如果至少一个测试没有通过,整个表达式就为
False 。
例如,要检查是否两个人都不小于21岁,可使用下面的测
试:
❸ >>> age_0 = 22
>> age_1 = 18
❷ >>> age_0 >= 21 and age_1 >= 21
False
❸ >>> age_1 = 22
>> age_0 >= 21 and age_1 >= 21
True
在 ❶ 处,定义两个用于存储年龄的变量:age_0 和age_1
。在 ❷ 处,检查这两个变量是否都大于或等于21。左边的测
试通过了,但右边的测试没有通过,因此整个条件表达式的
<!-- p150 -->
结果为False 。在 ❸ 处,将age_1 改为22,这样age_1 的
值大于21,因此两个测试都通过了,导致整个条件表达式的
结果为True 。
为改善可读性,可将每个测试分别放在一对圆括号内,但并
非必须这样做。如果你使用圆括号,测试将类似于下面这
样:
(age_0 >= 21) and (age_1 >= 21)
b. 使 用 o r 检 查 多 个 条 件
关键字or 也能够让你检查多个条件,但只要至少一个条件
满足,就能通过整个测试。仅当两个测试都没有通过时,使
用or 的表达式才为False 。
下面再次检查两个人的年龄,但检查的条件是至少一个人的
年龄不小于21岁:
❶ >>> age_0 = 22
>> age_1 = 18
❷ >>> age_0 >= 21 or age_1 >= 21
True
❸ >>> age_0 = 18
>> age_0 >= 21 or age_1 >= 21
False
同样,首先定义两个用于存储年龄的变量(见 ❶ )。 ❷ 处对
age_0 的测试通过了,因此整个表达式的结果为True 。接
下来,将age_0 减小为18。在 ❸ 处的测试中,两个测试都没
有通过,因此整个表达式的结果为False 。
5 . 2 . 6 检 查 特 定 值 是 否 包 含 在 列 表 中
<!-- p151 -->
有时候,执行操作前必须检查列表是否包含特定的值。例如,结
束用户的注册过程前,可能需要检查他提供的用户名是否已包含
在用户名列表中。在地图程序中,可能需要检查用户提交的位置
是否包含在已知位置列表中。
要判断特定的值是否已包含在列表中,可使用关键字in 。下面
来看看你可能为比萨店编写的一些代码。这些代码首先创建一个
列表,其中包含用户点的比萨配料,然后检查特定的配料是否包
含在该列表中。
>> requested_toppings = ['mushrooms', 'onions',
'pineapple']
❶ >>> 'mushrooms' in requested_toppings
True
❷ >>> 'pepperoni' in requested_toppings
False
在 ❶ 处和 ❷ 处,关键字in 让Python 检查列表
requested_toppings 是否包含'mushrooms'
和'pepperoni' 。这种技术很有用,让你能够在创建一个列表
后,轻松地检查其中是否包含特定的值。
5 . 2 . 7 检 查 特 定 值 是 否 不 包 含 在 列 表 中
还有些时候,确定特定的值未包含在列表中很重要。在这种情况
下,可使用关键字not in 。例如,有一个列表,其中包含被禁
止在论坛上发表评论的用户,可以在允许用户提交评论前检查他
是否被禁言:
banned_users.py
banned_users = ['andrew', 'carolina', 'david']
user = 'marie'
❶ if user not in banned_users:
print(f"{user.title()}, you can post a response if you
wish.")
<!-- p152 -->
❶ 处的代码行明白易懂:如果user 的值未包含在列表
banned_users 中,Python将返回True ,进而执行缩进的代
码行。
用户'marie' 未包含在列表banned_users 中,因此她将看到
一条邀请她发表评论的消息:
Marie, you can post a response if you wish.
5.2.8 布尔表达式
随着你对编程的了解越来越深入,将遇到术语布 尔 表 达 式 ,它
不过是条件测试的别名。与条件表达式一样,布尔表达式的结果
要么为True ,要么为False 。
布尔值通常用于记录条件,如游戏是否正在运行,或者用户是否
可以编辑网站的特定内容:
game_active = True
can_edit = False
在跟踪程序状态或程序中重要的条件方面,布尔值提供了一种高
效的方式。
动 手 试 一 试
练 习 5 - 1 : 条 件 测 试 编写一系列条件测试,将每个测试以
及对其结果的预测和实际结果打印出来。你编写的代码应类
似于下面这样:
car = 'subaru'
print("Is car == 'subaru'? I predict True.")
<!-- p153 -->
print(car == 'subaru')
print("\nIs car == 'audi'? I predict False.")
print(car == 'audi')
详细研究实际结果,直到你明白它为何为True 或
False 。
创建至少10个测试,且其中结果分别为True 和False
的测试都至少有5个。
练 习 5 - 2 : 更 多 条 件 测 试 你并非只能创建10个测试。如果
想尝试做更多比较,可再编写一些测试,并将它们加入
conditional_tests.py中。对于下面列出的各种情况,至少
编写两个结果分别为True 和False 的测试。
检查两个字符串相等和不等。
使用方法lower() 的测试。
涉及相等、不等、大于、小于、大于等于和小于等于的
数值测试。
使用关键字and 和or 的测试。
测试特定的值是否包含在列表中。
测试特定的值是否未包含在列表中。
<!-- p154 -->
5.3 if 语句
理解条件测试后,就可以开始编写if 语句了。if 语句有很多
种,选择使用哪种取决于要测试的条件数。前面讨论条件测试
时,列举了多个if 语句示例,下面更深入地讨论这个主题。
5.3.1 简单的 if 语句
最简单的if 语句只有一个测试和一个操作:
if conditional_test:
do something
第一行可包含任何条件测试,而在紧跟在测试后面的缩进代码块
中,可执行任何操作。如果条件测试的结果为True ,Python就
会执行紧跟在if 语句后面的代码,否则Python将忽略这些代
码。
假设有一个表示某人年龄的变量,而你想知道这个人是否符合投
票的年龄,可使用如下代码:
voting.py
age = 19
❶ if age >= 18:
❷ print("You are old enough to vote!")
在 ❶ 处,Python检查变量age 的值是否大于或等于18。答案是肯
定的,因此Python执行 ❷ 处缩进的函数调用print() :
You are old enough to vote!
<!-- p155 -->
在if 语句中,缩进的作用与在for 循环中相同。如果测试通过
了,将执行if 语句后面所有缩进的代码行,否则将忽略它们。
在紧跟if 语句后面的代码块中,可根据需要包含任意数量的代
码行。下面在一个人符合投票年龄时再打印一行输出,问他是否
登记了:
age = 19
if age >= 18:
print("You are old enough to vote!")
print("Have you registered to vote yet?")
条件测试通过了,而且两个函数调用print() 都缩进了,因此
它们都将执行:
You are old enough to vote!
Have you registered to vote yet?
如果age 的值小于18,这个程序将不会有任何输出。
5.3.2 if-else 语句
我们经常需要在条件测试通过时执行一个操作,在没有通过时执
行另一个操作。在这种情况下,可使用Python提供的if-else
语句。if-else 语句块类似于简单的if 语句,但其中的else
语句让你能够指定条件测试未通过时要执行的操作。
下面的代码在一个人符合投票年龄时显示与前面相同的消息,在
不符合时显示一条新消息:
age = 17
❶ if age >= 18:
print("You are old enough to vote!")
print("Have you registered to vote yet?")
❷ else:
<!-- p156 -->
print("Sorry, you are too young to vote.")
print("Please register to vote as soon as you turn 18!")
如果 ❶ 处的条件测试通过了,就执行第一组缩进的函数调用
print() 。如果测试结果为False ,就执行 ❷ 处的else 代码
块。这次age 小于18,条件测试未通过,因此执行else 代码块
中的代码:
Sorry, you are too young to vote.
Please register to vote as soon as you turn 18!
上述代码之所以可行,是因为只存在两种情形:要么符合投票年
龄,要么不符合。if-else 结构非常适合用于让Python执行两
种操作之一的情形。在这样简单的if-else 结构中,总是会执
行两个操作中的一个。
5.3.3 if-elif-else 结构
我们经常需要检查超过两个的情形,为此可使用Python提供的
if-elif-else 结构。Python只执行if-elif-else 结构中的
一个代码块。它依次检查每个条件测试,直到遇到通过了的条件
测试。测试通过后,Python将执行紧跟在它后面的代码,并跳过
余下的测试。
在现实世界中,很多情况下需要考虑的情形超过两个。例如,来
看一个根据年龄段收费的游乐场:
4岁以下免费;
4~18岁收费25美元;
18岁(含)以上收费40美元。
如果只使用一条if 语句,该如何确定门票价格呢?下面的代码
确定一个人所属的年龄段,并打印一条包含门票价格的消息:
<!-- p157 -->
amusement_park.py
age = 12
❶ if age < 4:
print("Your admission cost is $0.")
❷ elif age < 18:
print("Your admission cost is $25.")
❸ else:
print("Your admission cost is $40.")
❶ 处的if 测试检查一个人是否不满4岁。如果是,Python就打印
一条合适的消息,并跳过余下测试。 ❷ 处的elif 代码行其实是
另一个if 测试,仅在前面的测试未通过时才会运行。在这里,
我们知道这个人不小于4岁,因为第一个测试未通过。如果这个人
未满18岁,Python将打印相应的消息,并跳过else 代码块。如
果if 测试和elif 测试都未通过,Python将运行 ❸ 处else 代码
块中的代码。
在本例中, ❶ 处测试的结果为False ,因此不执行其代码块。然
而,第二个测试的结果为True (12小于18),因此执行其代码
块。输出为一个句子,向用户指出门票价格:
Your admission cost is $25.
只要年龄超过17岁,前两个测试就都不能通过。在这种情况下,
将执行else 代码块,指出门票价格为40美元。
为了让代码更简洁,可不在if-elif-else 代码块中打印门票
价格,而只在其中设置门票价格,并在它后面添加一个简单的函
数调用print() :
age = 12
if age < 4:
❶ price = 0
<!-- p158 -->
elif age < 18:
❷ price = 25
else:
❸ price = 40
❹ print(f"Your admission cost is ${price}.")
❶ 处、 ❷ 处和 ❸ 处的代码行像前一个示例那样,根据人的年龄设置
变量price 的值。在if-elif-else 结构中设置price 的值
后,一条未缩进的函数调用print() ❹ 会根据这个变量的值打印
一条消息,指出门票的价格。
这些代码的输出与前一个示例相同,但if-elif-else 结构的
作用更小:它只确定门票价格,而不是在确定门票价格的同时打
印一条消息。除效率更高外,这些修订后的代码还更容易修改:
要调整输出消息的内容,只需修改一个而不是三个函数调用
print() 。
5.3.4 使用多个 elif 代码块
可根据需要使用任意数量的elif 代码块。例如,假设前述游乐
场要给老年人打折,可再添加一个条件测试,判断顾客是否符合
打折条件。下面假设对于65岁(含)以上的老人,可半价(即20
美元)购买门票:
age = 12
if age < 4:
price = 0
elif age < 18:
price = 25
❶ elif age < 65:
price = 40
❷ else:
price = 20
print(f"Your admission cost is ${price}.")
<!-- p159 -->
这些代码大多未变。第二个elif 代码块(见 ❶ )通过检查确定
年龄不到65岁后,才将门票价格设置为全票价格——40美元。请
注意,在else 代码块(见 ❷ )中,必须将所赋的值改为20,因
为仅当年龄超过65岁(含)时,才会执行这个代码块。
5.3.5 省略 else 代码块
Python并不要求if-elif 结构后面必须有else 代码块。在有
些情况下,else 代码块很有用;而在其他一些情况下,使用一
条elif 语句来处理特定的情形更清晰:
age = 12
if age < 4:
price = 0
elif age < 18:
price = 25
elif age < 65:
price = 40
❶ elif age >= 65:
price = 20
print(f"Your admission cost is ${price}.")
❶ 处的elif 代码块在顾客的年龄超过65岁(含)时,将价格设
置为20美元。这比使用else 代码块更清晰些。经过这样的修改
后,每个代码块都仅在通过了相应的测试时才会执行。
else 是一条包罗万象的语句,只要不满足任何if 或elif 中的
条件测试,其中的代码就会执行。这可能引入无效甚至恶意的数
据。如果知道最终要测试的条件,应考虑使用一个elif 代码块
来代替else 代码块。这样就可以肯定,仅当满足相应的条件
时,代码才会执行。
5.3.6 测试多个条件
<!-- p160 -->
if-elif-else 结构功能强大,但仅适合用于只有一个条件满
足的情况:遇到通过了的测试后,Python就跳过余下的测试。这
种行为很好,效率很高,让你能够测试一个特定的条件。
然而,有时候必须检查你关心的所有条件。在这种情况下,应使
用一系列不包含elif 和else 代码块的简单if 语句。在可能有
多个条件为True 且需要在每个条件为True 时都采取相应措施
时,适合使用这种方法。
下面再来看看前面的比萨店示例。如果顾客点了两种配料,就需
要确保在其比萨中包含这些配料:
toppings.py
❶ requested_toppings = ['mushrooms', 'extra cheese']
❷ if 'mushrooms' in requested_toppings:
print("Adding mushrooms.")
❸ if 'pepperoni' in requested_toppings:
print("Adding pepperoni.")
❹ if 'extra cheese' in requested_toppings:
print("Adding extra cheese.")
print("\nFinished making your pizza!")
首先创建一个列表,其中包含顾客点的配料(见 ❶ )。 ❷ 处的if
语句检查顾客是否点了配料蘑菇(mushrooms)。如果点了,就打
印一条确认消息。 ❸ 处检查配料辣香肠(pepperoni)的代码也是
一个简单的if 语句,而不是elif 或else 语句。因此不管前一
个测试是否通过,都将进行这个测试。 ❹ 处的代码检查顾客是否
要求多加芝士(extra cheese)。不管前两个测试的结果如何,
都会执行这些代码。每当这个程序运行时,都会执行这三个独立
的测试。
因为本例检查了每个条件,所以将在比萨中添加蘑菇并多加芝
士:
<!-- p161 -->
Adding mushrooms.
Adding extra cheese.
Finished making your pizza!
如果像下面这样转而使用if-elif-else 结构,代码将不能正
确运行,因为有一个测试通过后,就会跳过余下的测试:
requested_toppings = ['mushrooms', 'extra cheese']
if 'mushrooms' in requested_toppings:
print("Adding mushrooms.")
elif 'pepperoni' in requested_toppings:
print("Adding pepperoni.")
elif 'extra cheese' in requested_toppings:
print("Adding extra cheese.")
print("\nFinished making your pizza!")
第一个测试检查列表中是否包含'mushrooms' 。它通过了,因
此将在比萨中添加蘑菇。然而,Python将跳过if-elif-else
结构中余下的测试,不再检查列表中是否包含'pepperoni'
和'extra cheese' 。结果是,将添加顾客点的第一种配料,
但不会添加其他配料:
Adding mushrooms.
Finished making your pizza!
总之,如果只想执行一个代码块,就使用if-elif-else 结
构;如果要执行多个代码块,就使用一系列独立的if 语句。
动 手 试 一 试
练 习 5 - 3 : 外 星 人 颜 色 假设在游戏中刚射杀了一个外星
人,请创建一个名为alien_color 的变量,并将其赋值
<!-- p162 -->
为'green' 、'yellow' 或'red' 。
编写一条if 语句,检查外星人是否是绿色的。如果
是,就打印一条消息,指出玩家获得了5分。
编写这个程序的两个版本,在一个版本中上述测试通过
了,而在另一个版本中未通过(未通过测试时没有输
出)。
练 习 5 - 4 : 外 星 人 颜 色 2 像练习5-3那样设置外星人的颜
色,并编写一个if-else 结构。
如果外星人是绿色的,就打印一条消息,指出玩家因射
杀该外星人获得了5分。
如果外星人不是绿色的,就打印一条消息,指出玩家获
得了10分。
编写这个程序的两个版本,在一个版本中执行if 代码
块,在另一个版本中执行else 代码块。
练 习 5 - 5 : 外 星 人 颜 色 3 将练习5-4中的if-else 结构改
为if-elif-else 结构。
如果外星人是绿色的,就打印一条消息,指出玩家获得
了5分。
如果外星人是黄色的,就打印一条消息,指出玩家获得
了10分。
如果外星人是红色的,就打印一条消息,指出玩家获得
了15分。
编写这个程序的三个版本,分别在外星人为绿色、黄色
和红色时打印一条消息。
练 习 5 - 6 : 人 生 的 不 同 阶 段 设置变量age 的值,再编写
一个if-elif-else 结构,根据age 的值判断一个人处于
人生的哪个阶段。
如果年龄小于2岁,就打印一条消息,指出这个人是婴
儿。
如果年龄为2(含)~4岁,就打印一条消息,指出这个
人是幼儿。
<!-- p163 -->
如果年龄为4(含)~13岁,就打印一条消息,指出这个
人是儿童。
如果年龄为13(含)~20岁,就打印一条消息,指出这
个人是青少年。
如果年龄为20(含)~65岁,就打印一条消息,指出这
个人是成年人。
如果年龄超过65岁(含),就打印一条消息,指出这个
人是老年人。
练 习 5 - 7 : 喜 欢 的 水 果 创建一个列表,其中包含你喜欢的
水果,再编写一系列独立的if 语句,检查列表中是否包含
特定的水果。
将该列表命名为favorite_fruits ,并在其中包含三
种水果。
编写5条if 语句,每条都检查某种水果是否包含在列表
中。如果是,就打印一条消息,下面是一个例子。
You really like bananas!
<!-- p164 -->
5.4 使用 if 语句处理列表
通过结合使用if 语句和列表,可完成一些有趣的任务:对列表
中特定的值做特殊处理;高效地管理不断变化的情形,如餐馆是
否还有特定的食材;证明代码在各种情形下都将按预期那样运
行。
5.4.1 检查特殊元素
本章开头通过一个简单示例演示了如何处理特殊值'bmw' ——它
需要采用不同的格式进行打印。现在你对条件测试和if 语句有
了大致的认识,下面就来进一步研究如何检查列表中的特殊值,
并对其做合适的处理。
继续使用前面的比萨店示例。这家比萨店在制作比萨时,每添加
一种配料都打印一条消息。通过创建一个列表,在其中包含顾客
点的配料,并使用一个循环来指出添加到比萨中的配料,能以极
高的效率编写这样的代码:
toppings.py
requested_toppings = ['mushrooms', 'green peppers', 'extra
cheese']
for requested_topping in requested_toppings:
print(f"Adding {requested_topping}.")
print("\nFinished making your pizza!")
输出很简单,因为上述代码不过是一个简单的for 循环:
Adding mushrooms.
Adding green peppers.
Adding extra cheese.
Finished making your pizza!
<!-- p165 -->
然而,如果比萨店的青椒用完了,该如何处理呢?为妥善地处理
这种情况,可在for 循环中包含一条if 语句:
requested_toppings = ['mushrooms', 'green peppers', 'extra
cheese']
for requested_topping in requested_toppings:
❶ if requested_topping == 'green peppers':
print("Sorry, we are out of green peppers right
now.")
❷ else:
print(f"Adding {requested_topping}.")
print("\nFinished making your pizza!")
这里在比萨中添加每种配料前都进行检查。 ❶ 处的代码检查顾客
是否点了青椒。如果是,就显示一条消息,指出不能点青椒的原
因。 ❷ 处的else 代码块确保其他配料都将添加到比萨中。
输出表明,已经妥善地处理了顾客点的每种配料:
Adding mushrooms.
Sorry, we are out of green peppers right now.
Adding extra cheese.
Finished making your pizza!
5.4.2 确定列表不是空的
到目前为止,我们对于处理的每个列表都做了一个简单的假设
——假设它们都至少包含一个元素。因为马上就要让用户来提供
存储在列表中的信息,所以不能再假设循环运行时列表不是空
的。有鉴于此,在运行for 循环前确定列表是否为空很重要。
<!-- p166 -->
下面在制作比萨前检查顾客点的配料列表是否为空。如果列表为
空,就向顾客确认是否要点原味比萨;如果列表不为空,就像前
面的示例那样制作比萨:
❶ requested_toppings = []
❷ if requested_toppings:
for requested_topping in requested_toppings:
print(f"Adding {requested_topping}.")
print("\nFinished making your pizza!")
❸ else:
print("Are you sure you want a plain pizza?")
首先创建一个空列表,其中不包含任何配料(见 ❶ )。 ❷ 处进行
简单的检查,而不是直接执行for 循环。在if 语句中将列表名
用作条件表达式时,Python将在列表至少包含一个元素时返回
True ,并在列表为空时返回False 。如果
requested_toppings 不为空,就运行与前一个示例相同的
for 循环;否则,就打印一条消息,询问顾客是否确实要点不加
任何配料的原味比萨(见 ❸ )。
在这里,这个列表为空,因此输出如下——询问顾客是否确实要
点原味比萨:
Are you sure you want a plain pizza?
如果这个列表不为空,输出将显示在比萨中添加的各种配料。
5.4.3 使用多个列表
顾客的要求往往五花八门,在比萨配料方面尤其如此。如果顾客
要在比萨中添加炸薯条,该怎么办呢?可使用列表和if 语句来
确定能否满足顾客的要求。
<!-- p167 -->
来看看在制作比萨前如何拒绝怪异的配料要求。下面的示例定义
了两个列表,其中第一个列表包含比萨店供应的配料,而第二个
列表包含顾客点的配料。这次对于requested_toppings 中的
每个元素,都检查它是否是比萨店供应的配料,再决定是否在比
萨中添加它:
❶ available_toppings = ['mushrooms', 'olives', 'green
peppers',
'pepperoni', 'pineapple', 'extra
cheese']
❷ requested_toppings = ['mushrooms', 'french fries', 'extra
cheese']
❸ for requested_topping in requested_toppings:
❹ if requested_topping in available_toppings:
print(f"Adding {requested_topping}.")
❺ else:
print(f"Sorry, we don't have {requested_topping}.")
print("\nFinished making your pizza!")
❶ 处定义了一个列表,其中包含比萨店供应的配料。请注意,如
果比萨店供应的配料是固定的,也可使用一个元组来存储它们。
❷ 处又创建了一个列表,其中包含顾客点的配料。请注意那个不
同寻常的配料——'french fries' 。在 ❸ 处遍历顾客点的配
料列表。在这个循环中,对于顾客点的每种配料,都检查它是否
包含在供应的配料列表中(见 ❹ )。如果答案是肯定的,就将其
加入比萨中,否则将运行else 代码块(见 ❺ ):打印一条消
息,告诉顾客不供应这种配料。
这些代码的输出整洁而翔实:
Adding mushrooms.
Sorry, we don't have french fries.
Adding extra cheese.
Finished making your pizza!
<!-- p168 -->
通过为数不多的几行代码,我们高效地处理了一种真实的情形!
动 手 试 一 试
练 习 5 - 8 : 以 特 殊 方 式 跟 管 理 员 打 招 呼 创建一个至少包含
5个用户名的列表,且其中一个用户名为'admin' 。想象你
要编写代码,在每位用户登录网站后都打印一条问候消息。
遍历用户名列表,并向每位用户打印一条问候消息。
如果用户名为'admin' ,就打印一条特殊的问候消
息,如下所示。
Hello admin, would you like to see a status
report?
否则,打印一条普通的问候消息,如下所示。
Hello Jaden, thank you for logging in again.
练 习 5 - 9 : 处 理 没 有 用 户 的 情 形 在为完成练习5-8编写的
程序中,添加一条if 语句,检查用户名列表是否为空。
如果为空,就打印如下消息。
We need to find some users!
删除列表中的所有用户名,确定将打印正确的消息。
练 习 5 - 1 0 : 检 查 用 户 名 按下面的说明编写一个程序,模
拟网站如何确保每位用户的用户名都独一无二。
创建一个至少包含5个用户名的列表,并将其命名为
current_users 。
再创建一个包含5个用户名的列表,将其命名为
new_users ,并确保其中有一两个用户名也包含在列
<!-- p169 -->
表current_users 中。
遍历列表new_users ,对于其中的每个用户名,都检
查它是否已被使用。如果是,就打印一条消息,指出需
要输入别的用户名;否则,打印一条消息,指出这个用
户名未被使用。
确保比较时不区分大小写。换句话说,如果用户
名'John' 已被使用,应拒绝用户名'JOHN' 。(为
此,需要创建列表current_users 的副本,其中包含
当前所有用户名的小写版本。)
练 习 5 - 1 1 : 序 数 序数表示位置,如1st和2nd。序数大多
以th结尾,只有1、2和3例外。
在一个列表中存储数字1~9。
遍历这个列表。
在循环中使用一个if-elif-else 结构,以打印每个
数字对应的序数。输出内容应为"1st 2nd 3rd 4th
5th 6th 7th 8th 9th" ,但每个序数都独占一行。
<!-- p170 -->
5.5 设置 if 语句的格式
本章的每个示例都展示了良好的格式设置习惯。在条件测试的格
式设置方面,PEP 8提供的唯一建议是,在诸如== 、>= 和<=
等比较运算符两边各添加一个空格。例如:
if age < 4:
要比
if age<4:
更好。
这样的空格不会影响Python对代码的解读,而只是让代码阅读起
来更容易。
动 手 试 一 试
练 习 5 - 1 2 : 设 置 if 语 句 的 格 式 审核你在本章编写的程
序,确保正确地设置了条件测试的格式。
练 习 5 - 1 3 : 自 己 的 想 法 与刚拿起本书时相比,现在你是
一名能力更强的程序员了。鉴于你对如何在程序中模拟现实
情形有了更深入的认识,可以考虑使用程序来解决一些问题
了。随着编程技能不断提高,你可能想解决一些问题,请将
这方面的想法记录下来。想想你可能想编写的游戏、想研究
的数据集以及想创建的Web应用程序。
<!-- p171 -->
5.6 小结
在本章中,你学习了:如何编写结果要么为True 要么为False
的条件测试;如何编写简单的if 语句、if-else 语句和if-
elif-else 结构,并且在程序中使用这些结构来测试特定的条
件,以确定这些条件是否满足;如何在利用高效的for 循环的同
时,以不同于其他元素的方式对特定的列表元素进行处理。你还
再次学习了Python就代码格式提出的建议,从而确保即便编写的
程序越来越复杂,其代码依然易于阅读和理解。
在第6章,你将学习Python字典。字典类似于列表,但让你能够将
不同的信息关联起来。你将学习如何创建和遍历字典,以及如何
将字典同列表和if 语句结合起来使用。学习字典让你能够模拟
更多现实世界的情形。
<!-- p172 -->
第 6 章 字典
在本章中,你将学习能够将相关信息关联起来
的Python字典,以及如何访问和修改字典中的信息。字典可
存储的信息量几乎不受限制,因此我们会演示如何遍历字典
中的数据。另外,你还将学习存储字典的列表、存储列表的
字典和存储字典的字典。
理解字典后,就能够更准确地为各种真实物体建模。你可以
创建一个表示人的字典,然后想在其中存储多少信息就存储
多少信息:姓名、年龄、地址、职业,以及能描述他的任何
方面。你还能够存储任意两种相关的信息,如一系列单词及
其含义,一系列人名及其喜欢的数,以及一系列山脉及其海
拔,等等。
<!-- p173 -->
6.1 一个简单的字典
来看一个包含外星人的游戏,这些外星人的颜色和分数各不相
同。下面是一个简单的字典,存储了有关特定外星人的信息:
alien.py
alien_0 = {'color': 'green', 'points': 5}
print(alien_0['color'])
print(alien_0['points'])
字典alien_0 存储了外星人的颜色和分数。最后两行代码访问
并显示这些信息,结果如下:
green
与大多数编程概念一样,要熟练使用字典,也需要一段时间的练
习。使用字典一段时间后,你就会明白为何它们能够高效地模拟
现实世界中的情形。
<!-- p174 -->
6.2 使用字典
在Python中,字 典 是一系列键 值 对 。每个键 都与一个值相关
联,你可使用键来访问相关联的值。与键相关联的值可以是数、
字符串、列表乃至字典。事实上,可将任何Python对象用作字典
中的值。
在Python中,字典用放在花括号({} )中的一系列键值对表
示,如前面的示例所示:
alien_0 = {'color': 'green', 'points': 5}
键 值 对 是两个相关联的值。指定键时,Python将返回与之相关
联的值。键和值之间用冒号分隔,而键值对之间用逗号分隔。在
字典中,想存储多少个键值对都可以。
最简单的字典只有一个键值对,如下述修改后的字典alien_0
所示:
alien_0 = {'color': 'green'}
这个字典只存储了一项有关alien_0 的信息,具体地说是这个
外星人的颜色。在该字典中,字符串'color' 是一个键,与之
相关联的值为'green' 。
6.2.1 访问字典中的值
要获取与键相关联的值,可依次指定字典名和放在方括号内的
键,如下所示:
alien.py
<!-- p175 -->
alien_0 = {'color': 'green'}
print(alien_0['color'])
这将返回字典alien_0 中与键'color' 相关联的值:
green
字典中可包含任意数量的键值对。例如,下面是最初的字典
alien_0 ,其中包含两个键值对:
alien_0 = {'color': 'green', 'points': 5}
现在,你可访问外星人alien_0 的颜色和分数。如果玩家射杀
了这个外星人,就可以使用下面的代码来确定应获得多少分:
alien_0 = {'color': 'green', 'points': 5}
❶ new_points = alien_0['points']
❷ print(f"You just earned {new_points} points!")
上述代码首先定义了一个字典。然后,从这个字典中获取与
键'points' 相关联的值(见 ❶ ),并将这个值赋给变量
new_points 。接下来,将这个整数转换为字符串,并打印一条
消息,指出玩家获得了多少分(见 ❷ ):
You just earned 5 points!
如果在外星人被射杀时运行这段代码,就将获取该外星人的分
数。
<!-- p176 -->
6.2.2 添加键值对
字典是一种动态结构,可随时在其中添加键值对。要添加键值
对,可依次指定字典名、用方括号括起的键和相关联的值。
下面来在字典alien_0 中添加两项信息:外星人的
坐标和
坐标,让我们能够在屏幕的特定位置显示该外星人。我们将这个
外星人放在屏幕左边缘,且离屏幕顶部25像素的地方。由于屏幕
坐标系的原点通常为左上角,要将该外星人放在屏幕左边缘,可
将
坐标设置为0;要将该外星人放在离屏幕顶部25像素的地方,
可将
坐标设置为25,如下所示:
alien.py
alien_0 = {'color': 'green', 'points': 5}
print(alien_0)
❶ alien_0['x_position'] = 0
❷ alien_0['y_position'] = 25
print(alien_0)
首先定义前面一直在使用的字典,然后打印这个字典,以显示其
信息快照。在 ❶ 处,我们在这个字典中新增了一个键值对,其中
的键为'x_position' ,值为0。在 ❷ 处重复这样的操作,但使
用的键为'y_position' 。打印修改后的字典时,将看到这两
个新增的键值对:
{'color': 'green', 'points': 5}
{'color': 'green', 'points': 5, 'y_position': 25,
'x_position': 0}
这个字典的最终版本包含四个键值对:原来的两个指定外星人的
颜色和分数,而新增的两个指定其位置。
注 意 在Python 3.7中,字典中元素的排列顺序与定义时
相同。如果将字典打印出来或遍历其元素,将发现元素的排
<!-- p177 -->
列顺序与添加顺序相同。
6.2.3 先创建一个空字典
在空字典中添加键值对有时候可提供便利,而有时候必须这样
做。为此,可先使用一对空花括号定义一个字典,再分行添加各
个键值对。例如,下面演示了如何以这种方式创建字典alien_0
:
alien.py
alien_0 = {}
alien_0['color'] = 'green'
alien_0['points'] = 5
print(alien_0)
这里首先定义了空字典alien_0 ,再在其中添加颜色和分数,
得到前述示例一直在使用的字典:
{'color': 'green', 'points': 5}
使用字典来存储用户提供的数据或在编写能自动生成大量键值对
的代码时,通常需要先定义一个空字典。
6.2.4 修改字典中的值
要修改字典中的值,可依次指定字典名、用方括号括起的键,以
及与该键相关联的新值。例如,假设随着游戏的进行,需要将一
个外星人从绿色改为黄色:
alien.py
<!-- p178 -->
alien_0 = {'color': 'green'}
print(f"The alien is {alien_0['color']}.")
alien_0['color'] = 'yellow'
print(f"The alien is now {alien_0['color']}.")
首先定义一个表示外星人alien_0 的字典,其中只包含这个外
星人的颜色。接下来,将与键'color' 相关联的值改
为'yellow' 。输出表明,这个外星人确实从绿色变成了黄色:
The alien is green.
The alien is now yellow.
来看一个更有趣的例子,对一个能够以不同速度移动的外星人进
行位置跟踪。为此,我们将存储该外星人的当前速度,并据此确
定该外星人将向右移动多远:
alien_0 = {'x_position': 0, 'y_position': 25, 'speed':
'medium'}
print(f"Original position: {alien_0['x_position']}")
向右移动外星人。
根据当前速度确定将外星人向右移动多远。
❶ if alien_0['speed'] == 'slow':
x_increment = 1
elif alien_0['speed'] == 'medium':
x_increment = 2
else:
这个外星人的移动速度肯定很快。
x_increment = 3
新位置为旧位置加上移动距离。
❷ alien_0['x_position'] = alien_0['x_position'] + x_increment
print(f"New position: {alien_0['x_position']}")
首先定义一个外星人,其中包含初始
坐标和
坐标,还有速
度'medium' 。出于简化考虑,省略了颜色和分数,但即便包含
<!-- p179 -->
这些键值对,本例的工作原理也不会有任何变化。我们还打印了
x_position 的初始值,旨在让用户知道这个外星人向右移动了
多远。
❶ 处使用一个if-elif-else 结构来确定外星人应向右移动多
远,并将这个值赋给变量x_increment 。如果外星人的速度
为'slow' ,它将向右移动一个单位;如果速度为'medium' ,
将向右移动两个单位;如果为'fast' ,将向右移动三个单位。
确定移动距离后,将其与x_position 的当前值相加(见 ❷ ),
再将结果关联到字典中的键x_position 。
因为这是一个速度中等的外星人,所以其位置将向右移两个单
位:
Original x-position: 0
New x-position: 2
这种技术很棒:通过修改外星人字典中的值,可改变外星人的行
为。例如,要将这个速度中等的外星人变成速度很快的外星人,
可添加如下代码行:
alien_0['speed'] = 'fast'
这样,再次运行这些代码时,其中的if-elif-else 结构将把
一个更大的值赋给变量x_increment 。
6.2.5 删除键值对
对于字典中不再需要的信息,可使用del 语句将相应的键值对彻
底删除。使用del 语句时,必须指定字典名和要删除的键。
例如,下面的代码从字典alien_0 中删除键'points' 及其
值:
<!-- p180 -->
alien.py
alien_0 = {'color': 'green', 'points': 5}
print(alien_0)
❶ del alien_0['points']
print(alien_0)
❶ 处的代码行让Python将键'points' 从字典alien_0 中删
除,同时删除与这个键相关联的值。输出表明,键'points' 及
其值5 已从字典中删除,但其他键值对未受影响:
{'color': 'green', 'points': 5}
{'color': 'green'}
注 意 删除的键值对会永远消失。
6.2.6 由类似对象组成的字典
在前面的示例中,字典存储的是一个对象(游戏中的一个外星
人)的多种信息,但你也可以使用字典来存储众多对象的同一种
信息。例如,假设你要调查很多人,询问他们最喜欢的编程语
言,可使用一个字典来存储这种简单调查的结果,如下所示:
favorite_languages = {
'jen': 'python',
'sarah': 'c',
'edward': 'ruby',
'phil': 'python',
}
如你所见,我们将一个较大的字典放在了多行中。每个键都是一
个被调查者的名字,而每个值都是被调查者喜欢的语言。确定需
要使用多行来定义字典时,要在输入左花括号后按回车键。在下
一行缩进四个空格,指定第一个键值对,并在它后面加上一个逗
<!-- p181 -->
号。此后再按回车键时,文本编辑器将自动缩进后续键值对,且
缩进量与第一个键值对相同。
定义好字典后,在最后一个键值对的下一行添加一个右花括号,
并缩进四个空格,使其与字典中的键对齐。一种不错的做法是,
在最后一个键值对后面也加上逗号,为以后在下一行添加键值对
做好准备。
注 意 对于较长的列表和字典,大多数编辑器提供了以类
似方式设置格式的功能。对于较长的字典,还有其他一些可
行的格式设置方式,因此在你的编辑器或其他源代码中,你
可能会看到稍微不同的格式设置方式。
给定被调查者的名字,可使用这个字典轻松地获悉他喜欢的语
言:
favorite_languages.py
favorite_languages = {
'jen': 'python',
'sarah': 'c',
'edward': 'ruby',
'phil': 'python',
}
❶ language = favorite_languages['sarah'].title()
print(f"Sarah's favorite language is {language}.")
为获悉Sarah喜欢的语言,我们使用如下代码:
favorite_languages['sarah']
在 ❶ 处,使用这种语法获取Sarah喜欢的语言,并将其赋给变量
language 。创建这个新变量让函数调用print() 变得整洁得
多。输出指出了Sarah喜欢的语言:
<!-- p182 -->
Sarah's favorite language is C.
这种语法可用来从字典中获取任何人喜欢的语言。
6.2.7 使用 get() 来访问值
使用放在方括号内的键从字典中获取感兴趣的值时,可能会引发
问题:如果指定的键不存在就会出错。
如果你要求获取外星人的分数,而这个外星人没有分数,结果将
如何呢?下面来看一看:
alien_no_points.py
alien_0 = {'color': 'green', 'speed': 'slow'}
print(alien_0['points'])
这将导致Python显示traceback,指出存在键值错误(KeyError
):
Traceback (most recent call last):
File "alien_no_points.py", line 2, in <module>
print(alien_0['points'])
KeyError: 'points'
第10章将详细介绍如何处理类似的错误,但就字典而言,可使用
方法get() 在指定的键不存在时返回一个默认值,从而避免这样
的错误。
方法get() 的第一个参数用于指定键,是必不可少的;第二个参
数为指定的键不存在时要返回的值,是可选的:
alien_0 = {'color': 'green', 'speed': 'slow'}
<!-- p183 -->
point_value = alien_0.get('points', 'No point value
assigned.')
print(point_value)
如果字典中有键'points' ,将获得与之相关联的值;如果没
有,将获得指定的默认值。虽然这里没有键'points' ,但将获
得一条清晰的消息,不会引发错误:
No point value assigned.
如果指定的键有可能不存在,应考虑使用方法get() ,而不要使
用方括号表示法。
注 意 调用get() 时,如果没有指定第二个参数且指定的
键不存在,Python将返回值None 。这个特殊值表示没有相
应的值。None 并非错误,而是一个表示所需值不存在的特
殊值,第8章将介绍它的其他用途。
动 手 试 一 试
练 习 6 - 1 : 人 使用一个字典来存储一个熟人的信息,包括
名、姓、年龄和居住的城市。该字典应包含键first_name
、last_name 、age 和city 。将存储在该字典中的每项
信息都打印出来。
练 习 6 - 2 : 喜 欢 的 数 使用一个字典来存储一些人喜欢的
数。请想出5个人的名字,并将这些名字用作字典中的键;找
出每个人喜欢的一个数,并将这些数作为值存储在字典中。
打印每个人的名字和喜欢的数。为了让这个程序更有趣,通
过询问朋友确保数据是真实的。
练 习 6 - 3 : 词 汇 表 Python字典可用于模拟现实生活中的字
典。为避免混淆,我们将后者称为词汇表。
<!-- p184 -->
想出你在前面学过的5个编程术语,将其用作词汇表中的
键,并将它们的含义作为值存储在词汇表中。
以整洁的方式打印每个术语及其含义。为此,可先打印
术语,在它后面加上一个冒号,再打印其含义;也可在
一行打印术语,再使用换行符(\n )插入一个空行,
然后在下一行以缩进的方式打印其含义。
<!-- p185 -->
6.3 遍历字典
一个Python字典可能只包含几个键值对,也可能包含数百万个键
值对。鉴于字典可能包含大量数据,Python支持对字典进行遍
历。字典可用于以各种方式存储信息,因此有多种遍历方式:可
遍历字典的所有键值对,也可仅遍历键或值。
6.3.1 遍历所有键值对
探索各种遍历方法前,先来看一个新字典,它用于存储有关网站
用户的信息。下面的字典存储一名用户的用户名、名和姓:
user_0 = {
'username': 'efermi',
'first': 'enrico',
'last': 'fermi',
}
利用本章前面介绍过的知识,可访问user_0 的任何一项信息,
但如果要获悉该用户字典中的所有信息,该如何办呢?可使用
for 循环来遍历这个字典:
user.py
user_0 = {
'username': 'efermi',
'first': 'enrico',
'last': 'fermi',
}
❶ for key, value in user_0.items():
❷ print(f"\nKey: {key}")
❸ print(f"Value: {value}")
<!-- p186 -->
如 ❶ 所示,要编写遍历字典的for 循环,可声明两个变量,用于
存储键值对中的键和值。这两个变量可以使用任意名称。下面的
代码使用了简单的变量名,这完全可行:
for k, v in user_0.items()
for 语句的第二部分包含字典名和方法items() (见 ❶ ),它
返回一个键值对列表。接下来,for 循环依次将每个键值对赋给
指定的两个变量。在本例中,使用这两个变量来打印每个键(见
❷ )及其相关联的值(见 ❸ )。第一个函数调用print() 中
的"\n" 确保在输出每个键值对前都插入一个空行:
Key: username
Value: efermi
Key: first
Value: enrico
Key: last
Value: fermi
在6.2.6节的示例favorite_languages.py中,字典存储的是不同
人的同一种信息。对于类似这样的字典,遍历所有的键值对很合
适。如果遍历字典favorite_languages ,将得到其中每个人
的姓名和喜欢的编程语言。由于该字典中的键都是人名,值都是
语言,因此在循环中使用变量name 和language ,而不是key
和value 。这让人更容易明白循环的作用:
favorite_languages.py
favorite_languages = {
'jen': 'python',
'sarah': 'c',
'edward': 'ruby',
'phil': 'python',
}
<!-- p187 -->
❶ for name, language in favorite_languages.items():
❷ print(f"{name.title()}'s favorite language is
{language.title()}.")
❶ 处的代码让Python遍历字典中的每个键值对,并将键赋给变量
name ,将值赋给变量language 。这些描述性名称能够让人非
常轻松地明白函数调用print() (见 ❷ )是做什么的。
仅使用几行代码,就将全部调查结果显示出来了:
Jen's favorite language is Python.
Sarah's favorite language is C.
Edward's favorite language is Ruby.
Phil's favorite language is Python.
即便字典存储的是上千乃至上百万人的调查结果,这种循环也管
用。
6.3.2 遍历字典中的所有键
在不需要使用字典中的值时,方法keys() 很有用。下面来遍历
字典favorite_languages ,并将每个被调查者的名字都打印
出来:
favorite_languages = {
'jen': 'python',
'sarah': 'c',
'edward': 'ruby',
'phil': 'python',
}
❶ for name in favorite_languages.keys():
print(name.title())
<!-- p188 -->
❶ 处的代码行让Python提取字典favorite_languages 中的所
有键,并依次将它们赋给变量name 。输出列出了每个被调查者
的名字:
Jen
Sarah
Edward
Phil
遍历字典时,会默认遍历所有的键。因此,如果将上述代码中
的:
for name in favorite_languages:
替换为:
for name in favorite_languages.keys():
输出将不变。
显式地使用方法keys() 可让代码更容易理解,你可以选择这样
做,但是也可以省略它。
在这种循环中,可使用当前键来访问与之相关联的值。下面来打
印两条消息,指出两位朋友喜欢的语言。像前面一样遍历字典中
的名字,但在名字为指定朋友的名字时,打印一条消息,指出其
喜欢的语言:
favorite_languages = {
--snip--
}
❶ friends = ['phil', 'sarah']
for name in favorite_languages.keys():
print(f"Hi {name.title()}.")
<!-- p189 -->
❷ if name in friends:
❸ language = favorite_languages[name].title()
print(f"\t{name.title()}, I see you love
{language}!")
❶ 处创建了一个列表,其中包含要收到打印消息的朋友。在循环
中,打印每个人的名字,并检查当前的名字是否在列表friends
中(见 ❷ )。如果在,就打印一句特殊的问候语,其中包含这位
朋友喜欢的语言。为获悉朋友喜欢的语言,我们使用了字典名,
并将变量name 的当前值作为键(见 ❸ )。
每个人的名字都会被打印,但只对朋友打特殊消息:
Hi Jen.
Hi Sarah.
Sarah, I see you love C!
Hi Edward.
Hi Phil.
Phil, I see you love Python!
还可使用方法keys() 确定某个人是否接受了调查。下面的代码
确定Erin是否接受了调查:
favorite_languages = {
'jen': 'python',
'sarah': 'c',
'edward': 'ruby',
'phil': 'python',
}
❶ if 'erin' not in favorite_languages.keys():
print("Erin, please take our poll!")
方法keys() 并非只能用于遍历:实际上,它返回一个列表,其
中包含字典中的所有键。因此 ❶ 处的代码行只核实'erin' 是否
<!-- p190 -->
包含在这个列表中。因为她并不包含在这个列表中,所以打印一
条消息,邀请她参加调查:
Erin, please take our poll!
6 . 3 . 3 按 特 定 顺 序 遍 历 字 典 中 的 所 有 键
从Python 3.7起,遍历字典时将按插入的顺序返回其中的元素。
不过在有些情况下,你可能要按与此不同的顺序遍历字典。
要以特定顺序返回元素,一种办法是在for 循环中对返回的键进
行排序。为此,可使用函数sorted() 来获得按特定顺序排列的
键列表的副本:
favorite_languages = {
'jen': 'python',
'sarah': 'c',
'edward': 'ruby',
'phil': 'python',
}
for name in sorted(favorite_languages.keys()):
print(f"{name.title()}, thank you for taking the poll.")
这条for 语句类似于其他for 语句,不同之处是对方法
dictionary.keys() 的结果调用了函数sorted() 。这让
Python列出字典中的所有键,并在遍历前对这个列表进行排序。
输出表明,按顺序显示了所有被调查者的名字:
Edward, thank you for taking the poll.
Jen, thank you for taking the poll.
Phil, thank you for taking the poll.
Sarah, thank you for taking the poll.
6.3.4 遍历字典中的所有值
<!-- p191 -->
如果主要对字典包含的值感兴趣,可使用方法values() 来返回
一个值列表,不包含任何键。例如,假设我们想获得一个列表,
其中只包含被调查者选择的各种语言,而不包含被调查者的名
字,可以这样做:
favorite_languages = {
'jen': 'python',
'sarah': 'c',
'edward': 'ruby',
'phil': 'python',
}
print("The following languages have been mentioned:")
for language in favorite_languages.values():
print(language.title())
这条for 语句提取字典中的每个值,并将其依次赋给变量
language 。通过打印这些值,就获得了一个包含被调查者所选
择语言的列表:
The following languages have been mentioned:
Python
C
Ruby
Python
这种做法提取字典中所有的值,而没有考虑是否重复。涉及的值
很少时,这也许不是问题,但如果被调查者很多,最终的列表可
能包含大量重复项。为剔除重复项,可使用集合(set)。集 合
中的每个元素都必须是独一无二的:
favorite_languages = {
--snip--
}
print("The following languages have been mentioned:")
❶ for language in set(favorite_languages.values()):
print(language.title())
<!-- p192 -->
通过对包含重复元素的列表调用set() ,可让Python找出列表中
独一无二的元素,并使用这些元素来创建一个集合。 ❶ 处使用
set() 来提取favorite_languages.values() 中不同的语
言。
结果是一个不重复的列表,其中列出了被调查者提及的所有语
言:
The following languages have been mentioned:
Python
C
Ruby
随着你更深入地学习Python,经常会发现它内置的功能可帮助你
以希望的方式处理数据。
注 意 可使用一对花括号直接创建集合,并在其中用逗号
分隔元素:
>> languages = {'python', 'ruby', 'python', 'c'}
>> languages
{'ruby', 'python', 'c'}
集合和字典很容易混淆,因为它们都是用一对花括号定义
的。当花括号内没有键值对时,定义的很可能是集合。不同
于列表和字典,集合不会以特定的顺序存储元素。
动 手 试 一 试
练 习 6 - 4 : 词 汇 表 2 现在你知道了如何遍历字典,可以整
理为完成练习6-3而编写的代码,将其中的一系列函数调用
print() 替换为一个遍历字典中键和值的循环。确定该循
环正确无误后,再在词汇表中添加5个Python术语。当你再次
<!-- p193 -->
运行这个程序时,这些新术语及其含义将自动包含在输出
中。
练 习 6 - 5 : 河 流 创建一个字典,在其中存储三条重要河流
及其流经的国家。例如,一个键值对可能是'nile':
'egypt' 。
使用循环为每条河流打印一条消息,下面是一个例子。
The Nile runs through Egypt.
使用循环将该字典中每条河流的名字打印出来。
使用循环将该字典包含的每个国家的名字打印出来。
练 习 6 - 6 : 调 查 在6.3.1节编写的程序
favorite_languages.py中执行以下操作。
创建一个应该会接受调查的人员名单,其中有些人已包
含在字典中,而其他人未包含在字典中。
遍历这个人员名单。对于已参与调查的人,打印一条消
息表示感谢;对于还未参与调查的人,打印一条消息邀
请他参加。
<!-- p194 -->
6.4 嵌套
有时候,需要将一系列字典存储在列表中,或将列表作为值存储
在字典中,这称为嵌 套 。你可以在列表中嵌套字典、在字典中
嵌套列表甚至在字典中嵌套字典。正如下面的示例将演示的,嵌
套是一项强大的功能。
6.4.1 字典列表
字典alien_0 包含一个外星人的各种信息,但无法存储第二个
外星人的信息,更别说屏幕上全部外星人的信息了。如何管理成
群结队的外星人呢?一种办法是创建一个外星人列表,其中每个
外星人都是一个字典,包含有关该外星人的各种信息。例如,下
面的代码创建一个包含三个外星人的列表:
aliens.py
alien_0 = {'color': 'green', 'points': 5}
alien_1 = {'color': 'yellow', 'points': 10}
alien_2 = {'color': 'red', 'points': 15}
❶ aliens = [alien_0, alien_1, alien_2]
for alien in aliens:
print(alien)
首先创建三个字典,其中每个字典都表示一个外星人。然后在 ❶
处将这些字典都存储到一个名为aliens 的列表中。最后,遍历
这个列表,并将每个外星人都打印出来:
{'color': 'green', 'points': 5}
{'color': 'yellow', 'points': 10}
{'color': 'red', 'points': 15}
<!-- p195 -->
更符合现实的情形是,外星人不止三个,且每个外星人都是使用
代码自动生成的。在下面的示例中,使用range() 生成了30个
外星人:
创建一个用于存储外星人的空列表。
aliens = []
创建 30 个绿色的外星人。
❶ for alien_number in range(30):
❷ new_alien = {'color': 'green', 'points': 5, 'speed':
'slow'}
❸ aliens.append(new_alien)
显示前 5 个外星人。
❹ for alien in aliens[:5]:
print(alien)
print("...")
显示创建了多少个外星人。
❺ print(f"Total number of aliens: {len(aliens)}")
在本例中,首先创建一个空列表,用于存储接下来将创建的所有
外星人。在 ❶ 处,range() 返回一系列数,其唯一的用途是告诉
Python要重复这个循环多少次。每次执行这个循环时,都创建一
个外星人(见 ❷ ),并将其附加到列表aliens 末尾(见 ❸ )。
在 ❹ 处,使用一个切片来打印前5个外星人。在 ❺ 处,打印列表的
长度,以证明确实创建了30个外星人:
{'color': 'green', 'points': 5, 'speed': slow'}
{'color': 'green', 'points': 5, 'speed': slow'}
{'color': 'green', 'points': 5, 'speed': slow'}
{'color': 'green', 'points': 5, 'speed': slow'}
{'color': 'green', 'points': 5, 'speed': slow'}
...
Total number of aliens: 30
这些外星人都具有相同的特征,但在Python看来,每个外星人都
是独立的,这让我们能够独立地修改每个外星人。
<!-- p196 -->
在什么情况下需要处理成群结队的外星人呢?想象一下,可能随
着游戏的进行,有些外星人会变色且加快移动速度。必要时,可
使用for 循环和if 语句来修改某些外形人的颜色。例如,要将
前三个外星人修改为黄色、速度为中等且值10分,可这样做:
创建一个用于存储外星人的空列表。
aliens = []
创建 30 个绿色的外星人。
for alien_number in range (30):
new_alien = {'color': 'green', 'points': 5, 'speed':
'slow'}
aliens.append(new_alien)
for alien in aliens[:3]:
if alien['color'] == 'green':
alien['color'] = 'yellow'
alien['speed'] = 'medium'
alien['points'] = 10
显示前 5 个外星人。
for alien in aliens[:5]:
print(alien)
print("...")
鉴于要修改前三个外星人,我们遍历一个只包含这些外星人的切
片。当前,所有外星人都是绿色的,但情况并非总是如此,因此
编写一条if 语句来确保只修改绿色外星人。如果外星人是绿色
的,就将其颜色改为'yellow' ,将其速度改为'medium' ,
并将其分数改为10 ,如下面的输出所示:
{'color': 'yellow', 'points': 10, 'speed': 'medium'}
{'color': 'yellow', 'points': 10, 'speed': 'medium'}
{'color': 'yellow', 'points': 10, 'speed': 'medium'}
{'color': 'green', 'points': 5, 'speed': 'slow'}
{'color': 'green', 'points': 5, 'speed': 'slow'}
...
可进一步扩展这个循环,在其中添加一个elif 代码块,将黄色
外星人改为移动速度快且值15分的红色外星人,如下所示(这里
<!-- p197 -->
只列出了循环,而没有列出整个程序):
for alien in aliens[0:3]:
if alien['color'] == 'green':
alien['color'] = 'yellow'
alien['speed'] = 'medium'
alien['points'] = 10
elif alien['color'] == 'yellow':
alien['color'] = 'red'
alien['speed'] = 'fast'
alien['points'] = 15
经常需要在列表中包含大量的字典,而其中每个字典都包含特定
对象的众多信息。例如,你可能需要为网站的每个用户创建一个
字典(就像6.3.1节的user.py中那样),并将这些字典存储在一
个名为users 的列表中。在这个列表中,所有字典的结构都相
同,因此你可以遍历这个列表,并以相同的方式处理其中的每个
字典。
6.4.2 在字典中存储列表
有时候,需要将列表存储在字典中,而不是将字典存储在列表
中。例如,你如何描述顾客点的比萨呢?如果使用列表,只能存
储要添加的比萨配料;但如果使用字典,就不仅可在其中包含配
料列表,还可包含其他有关比萨的描述。
在下面的示例中,存储了比萨的两方面信息:外皮类型和配料列
表。配料列表是一个与键'toppings' 相关联的值。要访问该列
表,我们使用字典名和键'toppings' ,就像访问字典中的其他
值一样。这将返回一个配料列表,而不是单个值:
pizza.py
存储所点比萨的信息。
❶ pizza = {
'crust': 'thick',
'toppings': ['mushrooms', 'extra cheese'],
}
<!-- p198 -->
概述所点的比萨。
❷ print(f"You ordered a {pizza['crust']}-crust pizza "
"with the following toppings:")
❸ for topping in pizza['toppings']:
print("\"f+topping)
首先创建一个字典,其中存储了有关顾客所点比萨的信息(见
❶ )。在这个字典中,一个键是'crust' ,与之相关联的值是字
符串'thick' ;下一个键是'toppings' ,与之相关联的值是
一个列表,其中存储了顾客要求添加的所有配料。制作前,我们
概述了顾客所点的比萨(见 ❷ )。如果函数调用print() 中的字
符串很长,可以在合适的位置分行。只需要在每行末尾都加上引
号,同时对于除第一行外的其他各行,都在行首加上引号并缩
进。这样,Python将自动合并圆括号内的所有字符串。为打印配
料,编写一个for 循环(见 ❸ )。为访问配料列表,使用
键'toppings' ,这样Python将从字典中提取配料列表。
下面的输出概述了要制作的比萨:
You ordered a thick-crust pizza with the following toppings:
mushrooms
extra cheese
每当需要在字典中将一个键关联到多个值时,都可以在字典中嵌
套一个列表。在本章前面有关喜欢的编程语言的示例中,如果将
每个人的回答都存储在一个列表中,被调查者就可选择多种喜欢
的语言。在这种情况下,当我们遍历字典时,与每个被调查者相
关联的都是一个语言列表,而不是一种语言;因此,在遍历该字
典的for 循环中,我们需要再使用一个for 循环来遍历与被调
查者相关联的语言列表:
favorite_languages.py
❶ favorite_languages = {
'jen': ['python', 'ruby'],
<!-- p199 -->
'sarah': ['c'],
'edward': ['ruby', 'go'],
'phil': ['python', 'haskell'],
}
❷ for name, languages in favorite_languages.items():
print(f"\n{name.title()}'s favorite languages are:")
❸ for language in languages:
print(f"\t{language.title()}")
如你所见,现在与每个名字相关联的值都是一个列表(见 ❶ )。
请注意,有些人喜欢的语言只有一种,而有些人有多种。遍历字
典时(见 ❷ ),使用变量languages 来依次存储对字典中每个
值的引用,因为我们知道这些值都是列表。在遍历字典的主循环
中,使用了另一个for 循环(见 ❸ )来遍历每个人喜欢的语言列
表。现在,每个人想列出多少种喜欢的语言都可以:
Jen's favorite languages are:
Python
Ruby
Sarah's favorite languages are:
C
Edward's favorite languages are:
Ruby
Go
Phil's favorite languages are:
Python
Haskell
为进一步改进这个程序,可在遍历字典的for 循环开头添加一条
if 语句,通过查看len(languages) 的值来确定当前的被调
查者喜欢的语言是否有多种。如果他喜欢的语言有多种,就像以
前一样显示输出;如果只有一种,就相应修改输出的措辞,如显
示Sarah's favorite language is C 。
<!-- p200 -->
注 意 列表和字典的嵌套层级不应太多。如果嵌套层级比
前面的示例多得多,很可能有更简单的解决方案。
6.4.3 在字典中存储字典
可在字典中嵌套字典,但这样做时,代码可能很快复杂起来。例
如,如果有多个网站用户,每个都有独特的用户名,可在字典中
将用户名作为键,然后将每位用户的信息存储在一个字典中,并
将该字典作为与用户名相关联的值。在下面的程序中,存储了每
位用户的三项信息:名、姓和居住地。为访问这些信息,我们遍
历所有的用户名,并访问与每个用户名相关联的信息字典:
many_users.py
users = {
'aeinstein': {
'first': 'albert',
'last': 'einstein',
'location': 'princeton',
},
'mcurie': {
'first': 'marie',
'last': 'curie',
'location': 'paris',
},
}
❶ for username, user_info in users.items():
❷ print(f"\nUsername: {username}")
❸ full_name = f"{user_info['first']} {user_info['last']}"
location = user_info['location']
❹ print(f"\tFull name: {full_name.title()}")
print(f"\tLocation: {location.title()}")
首先定义一个名为users 的字典,其中包含两个键:用户
名'aeinstein' 和'mcurie' 。与每个键盘相关联的值都是一
个字典,其中包含用户的名、姓和居住地。在 ❶ 处,遍历字典
<!-- p201 -->
users ,让Python依次将每个键赋给变量username ,并依次
将与当前键相关联的字典赋给变量user_info 。在循环内部的
❷ 处,将用户名打印出来。
在 ❸ 处,开始访问内部的字典。变量user_info 包含用户信息
字典,而该字典包含三个键:'first' 、'last'
和'location' 。对于每位用户,都使用这些键来生成整洁的姓
名和居住地,然后打印有关用户的简要信息(见 ❹ ):
Username: aeinstein
Full name: Albert Einstein
Location: Princeton
Username: mcurie
Full name: Marie Curie
Location: Paris
请注意,表示每位用户的字典都具有相同的结构。虽然Python并
没有这样的要求,但这使得嵌套的字典处理起来更容易。倘若表
示每位用户的字典都包含不同的键,for 循环内部的代码将更复
杂。
动 手 试 一 试
练 习 6 - 7 : 人 们 在为完成练习6-1而编写的程序中,再创
建两个表示人的字典,然后将这三个字典都存储在一个名为
people 的列表中。遍历这个列表,将其中每个人的所有信
息都打印出来。
练 习 6 - 8 : 宠 物 创建多个表示宠物的字典,每个字典都包
含宠物的类型及其主人的名字。将这些字典存储在一个名为
pets 的列表中,再遍历该列表,并将有关每个宠物的所有
信息都打印出来。
练 习 6 - 9 : 喜 欢 的 地 方 创建一个名为favorite_places
的字典。在这个字典中,将三个人的名字用作键,并存储每
个人喜欢的1~3个地方。为了让这个练习更有趣些,可以让
<!-- p202 -->
一些朋友说出他们喜欢的几个地方。遍历这个字典,并将其
中每个人的名字及其喜欢的地方打印出来。
练 习 6 - 1 0 : 喜 欢 的 数 2 修改为完成练习6-2而编写的程
序,让每个人都可以有多个喜欢的数,然后将每个人的名字
及其喜欢的数打印出来。
练 习 6 - 1 1 : 城 市 创建一个名为cities 的字典,将三个
城市名用作键。对于每座城市,都创建一个字典,并在其中
包含该城市所属的国家、人口约数以及一个有关该城市的事
实。在表示每座城市的字典中,应包含country 、
population 和fact 等键。将每座城市的名字以及有关信
息都打印出来。
练 习 6 - 1 2 : 扩 展 本章的示例足够复杂,能以很多方式进
行扩展。请对本章的一个示例进行扩展:添加键和值、调整
程序要解决的问题或改进输出的格式。
<!-- p203 -->
6.5 小结
在本章中,你学习了:如何定义字典,以及如何使用存储在字典
中的信息;如何访问和修改字典中的元素,以及如何遍历字典中
的所有信息;如何遍历字典中所有的键值对、所有的键和所有的
值;如何在列表中嵌套字典、在字典中嵌套列表以及在字典中嵌
套字典。
在下一章中,你将学习while 循环以及如何从用户那里获取输
入。这是激动人心的一章,让你知道如何将程序变成交互性的:
能够对用户输入做出响应。
<!-- p204 -->
第 7 章 用户输入和 while 循环
大多数程序旨在解决最终用户的问题,为此通
常需要从用户那里获取一些信息。例如,假设有人要判断自
己是否到了投票年龄。要编写回答这个问题的程序,就需要
知道用户的年龄,才能给出答案。因此,这种程序需要让用
户输 入 年龄,再将其与投票年龄进行比较,以判断用户是
否到了投票年龄,从而给出结果。
在本章中,你将学习如何接受用户输入,以便程序进行处
理。程序需要一个名字时,你需要提示用户输入该名字;程
序需要一个名单时,你需要提示用户输入一系列名字。为
此,你将使用函数input() 。
你还将学习如何让程序不断地运行,以便用户根据需要输入
信息,并在程序中使用这些信息。为此,你将使用while 循
环让程序不断运行,直到指定的条件不满足为止。
通过获取用户输入并学会控制程序的运行时间,你就能编写
出交互式程序。
<!-- p205 -->
7.1 函数 input() 的工作原理
函数input() 让程序暂停运行,等待用户输入一些文本。获取
用户输入后,Python将其赋给一个变量,以方便你使用。
例如,下面的程序让用户输入一些文本,再将这些文本呈现给用
户:
parrot.py
message = input("Tell me something, and I will repeat it back
to you: ")
print(message)
函数input() 接受一个参数——要向用户显示的提 示
(prompt)或说明,让用户知道该如何做。在本例中,Python运
行第一行代码时,用户将看到提示Tell me something, and
I will repeat it back to you: 。程序等待用户输入,
并在用户按回车键后继续运行。输入被赋给变量message ,接
下来的print(message) 将输入呈现给用户:
Tell me something, and I will repeat it back to you: Hello
everyone!
Hello everyone!
注 意 Sublime Text等众多编辑器不能运行提示用户输入
的程序。你可以使用Sublime Text来编写提示用户输入的程
序,但必须从终端运行它们。详情请参阅1.5节。
7.1.1 编写清晰的程序
每当使用函数input() 时,都应指定清晰易懂的提示,准确地
指出希望用户提供什么样的信息——指出用户应该输入何种信息
<!-- p206 -->
的任何提示都行,如下所示:
greeter.py
name = input("Please enter your name: ")
print(f"\nHello, {name}!")
通过在提示末尾(这里是冒号后面)包含一个空格,可将提示与
用户输入分开,让用户清楚地知道其输入始于何处,如下所示:
Please enter your name: Eric
Hello, Eric!
有时候,提示可能超过一行。例如,你可能需要指出获取特定输
入的原因。在这种情况下,可将提示赋给一个变量,再将该变量
传递给函数input() 。这样,即便提示超过一行,input() 语
句也会非常清晰。
greeter.py
prompt = "If you tell us who you are, we can personalize the
messages you see."
prompt += "\nWhat is your first name? "
name = input(prompt)
print(f"\nHello, {name}!")
本例演示了一种创建多行字符串的方式。第一行将消息的前半部
分赋给变量prompt 中。在第二行中,运算符+= 在前面赋给变
量prompt 的字符串末尾附加一个字符串。
最终的提示占据两行,且问号后面有一个空格,这也是为了使其
更加清晰:
<!-- p207 -->
If you tell us who you are, we can personalize the messages
you see.
What is your first name? Eric
Hello, Eric!
7.1.2 使用 int() 来获取数值输入
使用函数input() 时,Python将用户输入解读为字符串。请看
下面让用户输入年龄的解释器会话:
>> age = input("How old are you? ")
How old are you? 21
>> age
'21'
用户输入的是数21,但我们请求Python提供变量age 的值时,它
返回的是'21' ——用户输入数值的字符串表示。我们怎么知道
Python将输入解读成了字符串呢?因为这个数用引号括起了。如
果只想打印输入,这一点问题都没有;但如果试图将输入作为数
来使用,就会引发错误:
>> age = input("How old are you? ")
How old are you? 21
❶ >>> age >= 18
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
❷ TypeError: unorderable types: str() >= int()
试图将输入用于数值比较时(见 ❶ ),Python会引发错误,因为
它无法将字符串和整数进行比较:不能将赋给age 的字符
串'21' 与数值18 进行比较(见 ❷ )。
为解决这个问题,可使用函数int() ,它让Python将输入视为数
值。函数int() 将数的字符串表示转换为数值表示,如下所示:
<!-- p208 -->
>> age = input("How old are you? ")
How old are you? 21
❶ >>> age = int(age)
>> age >= 18
True
在本例中,用户根据提示输入21 后,Python将这个数解读为字
符串,但随后int() 将这个字符串转换成了数值表示(见 ❶ )。
这样Python就能运行条件测试了:将变量age (它现在表示的是
数值21)同18 进行比较,看它是否大于或等于18。测试结果为
True 。
如何在实际程序中使用函数int() 呢?请看下面的程序,它判断
一个人是否满足坐过山车的身高要求:
rollercoaster.py
height = input("How tall are you, in inches? ")
height = int(height)
if height >= 48:
print("\nYou're tall enough to ride!")
else:
print("\nYou'll be able to ride when you're a little
older.")
在此程序中,为何可以将height 同48 进行比较呢?因为在比
较前,height = int(height) 将输入转换成了数值表示。如
果输入的数大于或等于48,就指出用户满足身高条件:
How tall are you, in inches? 71
You're tall enough to ride!
将数值输入用于计算和比较前,务必将其转换为数值表示。
<!-- p209 -->
7.1.3 求模运算符
处理数值信息时,求 模 运 算 符 (%)是个很有用的工具,它将两
个数相除并返回余数:
>> 4 % 3
>> 5 % 3
>> 6 % 3
>> 7 % 3
求模运算符不会指出一个数是另一个数的多少倍,只指出余数是
多少。
如果一个数可被另一个数整除,余数就为0,因此求模运算将返回
0。可利用这一点来判断一个数是奇数还是偶数:
even_or_odd.py
number = input("Enter a number, and I'll tell you if it's even
or odd: ")
number = int(number)
if number % 2 == 0:
print(f"\nThe number {number} is even.")
else:
print(f"\nThe number {number} is odd.")
偶数都能被2整除,因此如果对一个数和2执行求模运算的结果为
0,即number % 2 == 0 ,那么这个数就是偶数;否则就是奇
数。
Enter a number, and I'll tell you if it's even or odd: 42
The number 42 is even.
<!-- p210 -->
动 手 试 一 试
练 习 7 - 1 : 汽 车 租 赁 编写一个程序,询问用户要租赁什么
样的汽车,并打印一条消息,下面是一个例子。
Let me see if I can find you a Subaru.
练 习 7 - 2 : 餐 馆 订 位 编写一个程序,询问用户有多少人用
餐。如果超过8位,就打印一条消息,指出没有空桌;否则指
出有空桌。
练 习 7 - 3 : 1 0 的 整 数 倍 让用户输入一个数,并指出该数是
否是10的整数倍。
<!-- p211 -->
7.2 while 循环简介
for 循环用于针对集合中的每个元素都执行一个代码块,而
while 循环则不断运行,直到指定的条件不满足为止。
7.2.1 使用 while 循环
可使用while 循环来数数。例如,下面的while 循环从1数到
5:
counting.py
current_number = 1
while current_number <= 5:
print(current_number)
current_number += 1
在第一行,将1 赋给变量current_number ,从而指定从1开始
数。将接下来的while 循环设置成:只要current_number 小
于或等于5,就接着运行这个循环。循环中的代码打印
current_number 的值,再使用代码current_number += 1
(代码current_number = current_number + 1 的简写)
将其值加1。
只要满足条件current_number <= 5 ,Python就接着运行这
个循环。因为1小于5,所以Python打印1 并将current_number
加1,使其为2;因为2小于5,所以Python打印2 并将
current_number 加1,使其为3;依此类推。一旦
current_number 大于5,循环就将停止,整个程序也将结束:
<!-- p212 -->
你每天使用的程序很可能就包含while 循环。例如,游戏使用
while 循环,确保在玩家想玩时不断运行,并在玩家想退出时停
止运行。如果程序在用户没有让它停止时停止运行,或者在用户
要退出时还继续运行,那就太没有意思了。因此,while 循环很
有用。
7.2.2 让用户选择何时退出
可以使用while 循环让程序在用户愿意时不断运行,如下面的程
序parrot.py所示。我们在其中定义了一个退 出 值 ,只要用户输
入的不是这个值,程序就将接着运行:
parrot.py
❶ prompt = "\nTell me something, and I will repeat it back to
you:"
prompt += "\nEnter 'quit' to end the program. "
❷ message = ""
❸ while message != 'quit':
message = input(prompt)
print(message)
❶ 处定义了一条提示消息,告诉用户有两个选择:要么输入一条
消息,要么输入退出值(这里为'quit' )。接下来,创建变量
message (见 ❷ ),用于记录用户输入的值。我们将变量
message 的初始值设置为空字符串"" ,让Python首次执行
while 代码行时有可供检查的东西。Python首次执行while 语
句时,需要将message 的值与'quit' 进行比较,但此时用户
还没有输入。如果没有可供比较的东西,Python将无法继续运行
程序。为解决这个问题,必须给变量message 指定初始值。虽
然这个初始值只是一个空字符串,但符合要求,能够让Python执
行while 循环所需的比较。只要message 的值不是'quit' ,
这个循环(见 ❸ )就会不断运行。
<!-- p213 -->
首次遇到这个循环时,message 是一个空字符串,因此Python
进入该循环。执行到代码行message = input(prompt) 时,
Python显示提示消息,并等待用户输入。不管用户输入是什么,
都将赋给变量message 并打印出来。接下来,Python重新检查
while 语句中的条件。只要用户输入的不是单词'quit' ,
Python就会再次显示提示消息并等待用户输入。等到用户终于输
入'quit' 后,Python停止执行while 循环,整个程序也到此
结束:
Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. Hello everyone!
Hello everyone!
Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. Hello again.
Hello again.
Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. quit
quit
这个程序很好,唯一美中不足的是,它将单词'quit' 也作为一
条消息打印了出来。为修复这种问题,只需使用一个简单的if
测试:
prompt = "\nTell me something, and I will repeat it back to
you:"
prompt += "\nEnter 'quit' to end the program. "
message = ""
while message != 'quit':
message = input(prompt)
if message != 'quit':
print(message)
现在,程序在显示消息前将做简单的检查,仅在消息不是退出值
时才打印它:
<!-- p214 -->
Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. Hello everyone!
Hello everyone!
Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. Hello again.
Hello again.
Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. quit
7.2.3 使用标志
在前一个示例中,我们让程序在满足指定条件时执行特定的任
务。但在更复杂的程序中,很多不同的事件会导致程序停止运
行。在这种情况下,该怎么办呢?
例如,有多种事件可能导致游戏结束,如玩家失去所有飞船、时
间已用完,或者要保护的城市被全部摧毁。导致程序结束的事件
有很多时,如果在一条while 语句中检查所有这些条件,将既复
杂又困难。
在要求很多条件都满足才继续运行的程序中,可定义一个变量,
用于判断整个程序是否处于活动状态。这个变量称为标 志
(flag),充当程序的交通信号灯。可以让程序在标志为True
时继续运行,并在任何事件导致标志的值为False 时让程序停止
运行。这样,在while 语句中就只需检查一个条件:标志的当前
值是否为True 。然后将所有其他测试(是否发生了应将标志设
置为False 的事件)都放在其他地方,从而让程序更整洁。
下面在前一节的程序parrot.py中添加一个标志。将其命名为
active (你可给它指定任何名称),用于判断程序是否应继续
运行:
prompt = "\nTell me something, and I will repeat it back to
you:"
prompt += "\nEnter 'quit' to end the program. "
❶ active = True
<!-- p215 -->
❷ while active:
message = input(prompt)
❸ if message == 'quit':
active = False
❹ else:
print(message)
将变量active 设置为True (见 ❶ ),让程序最初处于活动状
态。这样做简化了while 语句,因为不需要在其中做任何比较
——相关的逻辑由程序的其他部分处理。只要变量active 为
True ,循环就将继续运行(见 ❷ )。
在while 循环中,在用户输入后使用一条if 语句来检查变量
message 的值。如果用户输入的是'quit' (见 ❸ ),就将变
量active 设置为False 。这将导致while 循环不再继续执
行。如果用户输入的不是'quit' (见 ❹ ),就将输入作为一条
消息打印出来。
这个程序的输出与前一个示例相同。前一个示例将条件测试直接
放在了while 语句中,而这个程序则使用一个标志来指出程序是
否处于活动状态。这样,如果要添加测试(如elif 语句)以检
查是否发生了其他导致active 变为False 的事件,就会很容
易。在复杂的程序(如很多事件会导致程序停止运行的游戏)
中,标志很有用:在任意一个事件导致活动标志变成False 时,
主游戏循环将退出,此时可显示一条游戏结束消息,并让用户选
择是否要重新玩。
7.2.4 使用 break 退出循环
要立即退出while 循环,不再运行循环中余下的代码,也不管条
件测试的结果如何,可使用break 语句。break 语句用于控制
程序流程,可用来控制哪些代码行将执行、哪些代码行不执行,
从而让程序按你的要求执行你要执行的代码。
<!-- p216 -->
例如,来看一个让用户指出他到过哪些地方的程序。在这个程序
中,可在用户输入'quit' 后使用break 语句立即退出while
循环:
cities.py
prompt = "\nPlease enter the name of a city you have
visited:"
prompt += "\n(Enter 'quit' when you are finished.) "
❶ while True:
city = input(prompt)
if city == 'quit':
break
else:
print(f"I'd love to go to {city.title()}!")
以while True (见 ❶ )打头的循环将不断运行,直到遇到
break 语句。这个程序中的循环不断让用户输入他到过的城市的
名字,直到用户输入'quit' 为止。用户输入'quit' 后,将执
行break 语句,导致Python退出循环:
Please enter the name of a city you have visited:
(Enter 'quit' when you are finished.) New York
I'd love to go to New York!
Please enter the name of a city you have visited:
(Enter 'quit' when you are finished.) San Francisco
I'd love to go to San Francisco!
Please enter the name of a city you have visited:
(Enter 'quit' when you are finished.) quit
注 意 在任何Python循环中都可使用break 语句。例如,
可使用break 语句来退出遍历列表或字典的for 循环。
7.2.5 在循环中使用 continue
<!-- p217 -->
要返回循环开头,并根据条件测试结果决定是否继续执行循环,
可使用continue 语句,它不像break 语句那样不再执行余下
的代码并退出整个循环。例如,来看一个从1数到10但只打印其中
奇数的循环:
counting.py
current_number = 0
while current_number < 10:
❶ current_number += 1
if current_number % 2 == 0:
continue
print(current_number)
首先将current_number 设置为0,由于它小于10,Python进入
while 循环。进入循环后,以步长1的方式往上数(见 ❶ ),因
此current_number 为1。接下来,if 语句检查
current_number 与2的求模运算结果。如果结果为0(意味着
current_number 可被2整除),就执行continue 语句,让
Python忽略余下的代码,并返回循环的开头。如果当前的数不能
被2整除,就执行循环中余下的代码,将这个数打印出来:
7.2.6 避免无限循环
每个while 循环都必须有停止运行的途径,这样才不会没完没了
地执行下去。例如,下面的循环从1数到5:
counting.py
<!-- p218 -->
x = 1
while x <= 5:
print(x)
x += 1
但如果像下面这样不小心遗漏了代码行x += 1 ,这个循环将没
完没了地运行:
这个循环将没完没了地运行!
x = 1
while x <= 5:
print(x)
在这里,x 的初始值为1 ,但根本不会变。因此条件测试x <=
5 始终为True ,导致while 循环没完没了地打印1 ,如下所
示:
--snip--
每个程序员都会偶尔因不小心而编写出无限循环,在循环的退出
条件比较微妙时尤其如此。如果程序陷入无限循环,可按Ctrl +
C,也可关闭显示程序输出的终端窗口。
要避免编写无限循环,务必对每个while 循环进行测试,确保其
按预期那样结束。如果你希望程序在用户输入特定值时结束,可
运行程序并输入这样的值。如果在这种情况下程序没有结束,请
检查程序处理这个值的方式,确认程序至少有一个这样的地方能
让循环条件为False ,或者让break 语句得以执行。
注 意 Sublime Text等一些编辑器内嵌了输出窗口,这可
能导致难以结束无限循环,不得不通过关闭编辑器来结束。
<!-- p219 -->
在这种情况下,可在输出窗口中单击鼠标,再按Ctrl + C,
这样应该能够结束无限循环。
动 手 试 一 试
练 习 7 - 4 : 比 萨 配 料 编写一个循环,提示用户输入一系列
比萨配料,并在用户输入'quit' 时结束循环。每当用户输
入一种配料后,都打印一条消息,指出我们会在比萨中添加
这种配料。
练 习 7 - 5 : 电 影 票 有家电影院根据观众的年龄收取不同的
票价:不到3岁的观众免费;3~12岁的观众收费10美元;超
过12岁的观众收费15美元。请编写一个循环,在其中询问用
户的年龄,并指出其票价。
练 习 7 - 6 : 三 种 出 路 以不同的方式完成练习7-4或练习7-
5,在程序中采取如下做法。
在while 循环中使用条件测试来结束循环。
使用变量active 来控制循环结束的时机。
使用break 语句在用户输入'quit' 时退出循环。
练 习 7 - 7 : 无 限 循 环 编写一个没完没了的循环,并运行它
(要结束该循环,可按Ctrl + C,也可关闭显示输出的窗
口)。
<!-- p220 -->
7 . 3 使 用 while 循 环 处 理 列 表 和 字
典
到目前为止,我们每次都只处理了一项用户信息:获取用户的输
入,再将输入打印出来或做出应答;循环再次运行时,获悉另一
个输入值并做出响应。然而,要记录大量的用户和信息,需要在
while 循环中使用列表和字典。
for 循环是一种遍历列表的有效方式,但不应在for 循环中修
改列表,否则将导致Python难以跟踪其中的元素。要在遍历列表
的同时对其进行修改,可使用while 循环。通过将while 循环
同列表和字典结合起来使用,可收集、存储并组织大量输入,供
以后查看和显示。
7.3.1 在列表之间移动元素
假设有一个列表包含新注册但还未验证的网站用户。验证这些用
户后,如何将他们移到另一个已验证用户列表中呢?一种办法是
使用一个while 循环,在验证用户的同时将其从未验证用户列表
中提取出来,再将其加入另一个已验证用户列表中。代码可能类
似于下面这样:
confirmed_users.py
首先,创建一个待验证用户列表
和一个用于存储已验证用户的空列表。
❶ unconfirmed_users = ['alice', 'brian', 'candace']
confirmed_users = []
验证每个用户,直到没有未验证用户为止。
将每个经过验证的用户都移到已验证用户列表中。
❷ while unconfirmed_users:
❸ current_user = unconfirmed_users.pop()
print(f"Verifying user: {current_user.title()}")
❹ confirmed_users.append(current_user)
<!-- p221 -->
显示所有已验证的用户。
print("\nThe following users have been confirmed:")
for confirmed_user in confirmed_users:
print(confirmed_user.title())
首先创建一个未验证用户列表(见 ❶ ),其中包含用户Alice、
Brian和Candace,还创建了一个空列表,用于存储已验证的用
户。 ❷ 处的while 循环将不断运行,直到列表
unconfirmed_users 变成空的。在此循环中, ❸ 处的方法
pop() 以每次一个的方式从列表unconfirmed_users 末尾删
除未验证的用户。由于Candace位于列表unconfirmed_users
末尾,其名字将首先被删除、赋给变量current_user 并加入
列表confirmed_users 中(见 ❹ )。接下来是Brian,然后是
Alice。
为模拟用户验证过程,我们打印一条验证消息并将用户加入已验
证用户列表中。未验证用户列表越来越短,而已验证用户列表越
来越长。未验证用户列表为空后结束循环,再打印已验证用户列
表:
Verifying user: Candace
Verifying user: Brian
Verifying user: Alice
The following users have been confirmed:
Candace
Brian
Alice
7 . 3 . 2 删 除 为 特 定 值 的 所 有 列 表 元 素
在第3章中,我们使用函数remove() 来删除列表中的特定值。
这之所以可行,是因为要删除的值只在列表中出现一次。如果要
删除列表中所有为特定值的元素,该怎么办呢?
<!-- p222 -->
假设你有一个宠物列表,其中包含多个值为'cat' 的元素。要删
除所有这些元素,可不断运行一个while 循环,直到列表中不再
包含值'cat' ,如下所示:
pets.py
pets = ['dog', 'cat', 'dog', 'goldfish', 'cat', 'rabbit',
'cat']
print(pets)
while 'cat' in pets:
pets.remove('cat')
print(pets)
首先创建一个列表,其中包含多个值为'cat' 的元素。打印这个
列表后,Python进入while 循环,因为它发现'cat' 在列表中
至少出现了一次。进入该循环后,Python删除第一个'cat' 并返
回到while 代码行,然后发现'cat' 还包含在列表中,因此再
次进入循环。它不断删除'cat' ,直到这个值不再包含在列表
中,然后退出循环并再次打印列表:
['dog', 'cat', 'dog', 'goldfish', 'cat', 'rabbit', 'cat']
['dog', 'dog', 'goldfish', 'rabbit']
7.3.3 使用用户输入来填充字典
可使用while 循环提示用户输入任意多的信息。下面创建一个调
查程序,其中的循环每次执行时都提示输入被调查者的名字和回
答。我们将收集的数据存储在一个字典中,以便将回答同被调查
者关联起来:
mountain_poll.py
responses = {}
<!-- p223 -->
设置一个标志,指出调查是否继续。
polling_active = True
while polling_active:
提示输入被调查者的名字和回答。
❶ name = input("\nWhat is your name? ")
response = input("Which mountain would you like to climb
someday? ")
将回答存储在字典中。
❷ responses[name] = response
看看是否还有人要参与调查。
❸ repeat = input("Would you like to let another person
respond? (yes/ no) ")
if repeat == 'no':
polling_active = False
调查结束,显示结果。
print("\n--- Poll Results ---")
❹ for name, response in responses.items():
print(f"{name} would like to climb {response}.")
这个程序首先定义了一个空字典(responses ),并设置了一
个标志(polling_active )用于指出调查是否继续。只要
polling_active 为True ,Python就运行while 循环中的代
码。
在这个循环中,提示用户输入其名字及其喜欢爬哪座山(见
❶ )。将这些信息存储在字典responses 中(见 ❷ ),然后询问
用户是否继续调查(见 ❸ )。如果用户输入yes ,程序将再次进
入while 循环;如果用户输入no ,标志polling_active 将
被设置为False ,而while 循环将就此结束。最后一个代码块
(见 ❹ )显示调查结果。
如果运行这个程序,并输入一些名字和回答,输出将类似于下面
这样:
What is your name? Eric
Which mountain would you like to climb someday? Denali
Would you like to let another person respond? (yes/ no) yes
<!-- p224 -->
What is your name? Lynn
Which mountain would you like to climb someday? Devil's Thumb
Would you like to let another person respond? (yes/ no) no
--- Poll Results ---
Eric would like to climb Denali.
Lynn would like to climb Devil's Thumb.
动 手 试 一 试
练 习 7 - 8 : 熟 食 店 创建一个名为sandwich_orders 的
列表,在其中包含各种三明治的名字,再创建一个名为
finished_sandwiches 的空列表。遍历列表
sandwich_orders ,对于其中的每种三明治,都打印一条
消息,如I made your tuna sandwich ,并将其移到列
表finished_sandwiches 中。所有三明治都制作好后,
打印一条消息,将这些三明治列出来。
练 习 7 - 9 : 五 香 烟 熏 牛 肉 卖 完 了 使用为完成练习7-8而创
建的列表sandwich_orders ,并确保'pastrami' 在其
中至少出现了三次。在程序开头附近添加这样的代码:打印
一条消息,指出熟食店的五香烟熏牛肉(pastrami)卖完
了;再使用一个while 循环将列表sandwich_orders 中
的'pastrami' 都删除。确认最终的列表
finished_sandwiches 未包含'pastrami' 。
练 习 7 - 1 0 : 梦 想 的 度 假 胜 地 编写一个程序,调查用户梦
想的度假胜地。使用类似于下面的提示,并编写一个打印调
查结果的代码块。
If you could visit one place in the world, where
would you go?
<!-- p225 -->
7.4 小结
在本章中,你学习了:如何在程序中使用input() 来让用户提
供信息;如何处理文本和数的输入,以及如何使用while 循环让
程序按用户的要求不断运行;多种控制while 循环流程的方式:
设置活动标志、使用break 语句以及使用continue 语句;如
何使用while 循环在列表之间移动元素,以及如何从列表中删除
所有包含特定值的元素;如何结合使用while 循环和字典。
在第8章中,你将学习函 数 。函数让你能够将程序分成多个很小
的部分,每部分都负责完成一项具体任务。你可以根据需要调用
同一个函数任意次,还可将函数存储在独立的文件中。使用函数
可让你编写的代码效率更高、更容易维护和排除故障,还可在众
多不同的程序中重用。
<!-- p226 -->
第 8 章 函数
在本章中,你将学习编写函 数 。函数是带名字
的代码块,用于完成具体的工作。要执行函数定义的特定任
务,可调 用 该函数。需要在程序中多次执行同一项任务
时,无须反复编写完成该任务的代码,只需要调用执行该任
务的函数,让Python运行其中的代码即可。你将发现,通过
使用函数,程序编写、阅读、测试和修复起来都更加容易。
你还将学习向函数传递信息的方式;学习如何编写主要任务
是显示信息的函数,以及旨在处理数据并返回一个或一组值
的函数;最后,学习如何将函数存储在称为模 块 的独立文
件中,让主程序文件的组织更为有序。
<!-- p227 -->
8.1 定义函数
下面是一个打印问候语的简单函数,名为greet_user() :
greeter.py
❶ def greet_user():
❷ """ 显示简单的问候语。 """
❸ print("Hello!")
❹ greet_user()
本例演示了最简单的函数结构。 ❶ 处的代码行使用关键字def 来
告诉Python,你要定义一个函数。这是函 数 定 义 ,向Python指
出了函数名,还可能在圆括号内指出函数为完成任务需要什么样
的信息。在这里,函数名为greet_user() ,它不需要任何信
息就能完成工作,因此括号是空的(即便如此,括号也必不可
少)。最后,定义以冒号结尾。
紧跟在def greet_user(): 后面的所有缩进行构成了函数
体。 ❷ 处的文本是称为文 档 字 符 串 (docstring)的注释,描述
了函数是做什么的。文档字符串用三引号括起,Python使用它们
来生成有关程序中函数的文档。
代码行print("Hello!") (见 ❸ )是函数体内的唯一一行代
码,因此greet_user() 只做一项工作:打印Hello! 。
要使用这个函数,可调用它。函 数 调 用 让Python执行函数的代
码。要调 用 函数,可依次指定函数名以及用圆括号括起的必要
信息,如 ❹ 处所示。由于这个函数不需要任何信息,调用它时只
需输入greet_user() 即可。和预期一样,它打印Hello! :
Hello!
<!-- p228 -->
8.1.1 向函数传递信息
只需稍作修改,就可让函数greet_user() 不仅向用户显示
Hello! ,还将用户的名字作为抬头。为此,可在函数定义def
greet_user() 的括号内添加username 。通过在这里添加
username ,可让函数接受你给username 指定的任何值。现
在,这个函数要求你调用它时给username 指定一个值。调用
greet_user() 时,可将一个名字传递给它,如下所示:
def greet_user(username):
""" 显示简单的问候语。 """
print(f"Hello, {username.title()}!")
greet_user('jesse')
代码greet_user('jesse') 调用函数greet_user() ,并
向它提供执行函数调用print() 所需的信息。这个函数接受你
传递给它的名字,并向这个人发出问候:
Hello, Jesse!
同样,greet_user('sarah') 调用函数greet_user() 并
向它传递'sarah' ,从而打印Hello, Sarah! 。可根据需要
调用函数greet_user() 任意次,调用时无论传入什么名字,
都将生成相应的输出。
8.1.2 实参和形参
前面定义函数greet_user() 时,要求给变量username 指定
一个值。调用这个函数并提供这种信息(人名)时,它将打印相
应的问候语。
<!-- p229 -->
在函数greet_user() 的定义中,变量username 是一个形 参
(parameter),即函数完成工作所需的信息。在代码
greet_user('jesse') 中,值'jesse' 是一个实 参
(argument),即调用函数时传递给函数的信息。调用函数时,
将要让函数使用的信息放在圆括号内。在
greet_user('jesse') 中,将实参'jesse' 传递给了函数
greet_user() ,这个值被赋给了形参username 。
注 意 大家有时候会形参、实参不分,因此如果你看到有
人将函数定义中的变量称为实参或将函数调用中的变量称为
形参,不要大惊小怪。
动 手 试 一 试
练 习 8 - 1 : 消 息 编写一个名为display_message() 的
函数,它打印一个句子,指出你在本章学的是什么。调用这
个函数,确认显示的消息正确无误。
练 习 8 - 2 : 喜 欢 的 图 书 编写一个名为favorite_book()
的函数,其中包含一个名为title 的形参。这个函数打印一
条消息,下面是一个例子。
One of my favorite books is Alice in Wonderland.
调用这个函数,并将一本图书的名称作为实参传递给它。
<!-- p230 -->
8.2 传递实参
函数定义中可能包含多个形参,因此函数调用中也可能包含多个
实参。向函数传递实参的方式很多:可使用位 置 实 参 ,这要求
实参的顺序与形参的顺序相同;也可使用关 键 字 实 参 ,其中每
个实参都由变量名和值组成;还可使用列表和字典。下面依次介
绍这些方式。
8.2.1 位置实参
调用函数时,Python必须将函数调用中的每个实参都关联到函数
定义中的一个形参。为此,最简单的关联方式是基于实参的顺
序。这种关联方式称为位 置 实 参 。
为明白其中的工作原理,来看一个显示宠物信息的函数。这个函
数指出一个宠物属于哪种动物以及它叫什么名字,如下所示:
pets.py
❶ def describe_pet(animal_type, pet_name):
""" 显示宠物的信息。 """
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
❷ describe_pet('hamster', 'harry')
这个函数的定义表明,它需要一种动物类型和一个名字(见
❶ )。调用describe_pet() 时,需要按顺序提供一种动物类
型和一个名字。例如,在刚才的函数调用中,实参'hamster'
被赋给形参animal_type ,而实参'harry' 被赋给形参
pet_name (见 ❷ )。在函数体内,使用了这两个形参来显示宠
物的信息。
输出描述了一只名为Harry的仓鼠:
<!-- p231 -->
I have a hamster.
My hamster's name is Harry.
a. 多 次 调 用 函 数
可以根据需要调用函数任意次。要再描述一个宠物,只需再
次调用describe_pet() 即可:
def describe_pet(animal_type, pet_name):
""" 显示宠物的信息。 """
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is
{pet_name.title()}.")
describe_pet('hamster', 'harry')
describe_pet('dog', 'willie')
第二次调用describe_pet() 函数时,向它传递了实
参'dog' 和'willie' 。与第一次调用时一样,Python将
实参'dog' 关联到形参animal_type ,并将实
参'willie' 关联到形参pet_name 。与前面一样,这个
函数完成了任务,但打印的是一条名为Willie的小狗的信
息。至此,有一只名为Harry的仓鼠,还有一条名为Willie的
小狗:
I have a hamster.
My hamster's name is Harry.
I have a dog.
My dog's name is Willie.
多次调用函数是一种效率极高的工作方式。只需在函数中编
写一次描述宠物的代码,然后每当需要描述新宠物时,都调
用该函数并向它提供新宠物的信息。即便描述宠物的代码增
加到了10行,依然只需使用一行调用函数的代码,就可描述
一个新宠物。
<!-- p232 -->
在函数中,可根据需要使用任意数量的位置实参,Python将
按顺序将函数调用中的实参关联到函数定义中相应的形参。
b. 位 置 实 参 的 顺 序 很 重 要
使用位置实参来调用函数时,如果实参的顺序不正确,结果
可能出乎意料:
def describe_pet(animal_type, pet_name):
""" 显示宠物的信息。 """
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is
{pet_name.title()}.")
describe_pet('harry', 'hamster')
在这个函数调用中,先指定名字,再指定动物类型。由于实
参'harry' 在前,这个值将赋给形参animal_type 。同
理,'hamster' 将赋给形参pet_name 。结果是有一个名
为Hamster的harry:
I have a harry.
My harry's name is Hamster.
如果你得到的结果像上面一样可笑,请确认函数调用中实参
的顺序与函数定义中形参的顺序一致。
8.2.2 关键字实参
关 键 字 实 参 是传递给函数的名称值对。因为直接在实参中将名
称和值关联起来,所以向函数传递实参时不会混淆(不会得到名
为Hamster的harry这样的结果)。关键字实参让你无须考虑函数
调用中的实参顺序,还清楚地指出了函数调用中各个值的用途。
<!-- p233 -->
下面来重新编写pets.py,在其中使用关键字实参来调用
describe_pet() :
def describe_pet(animal_type, pet_name):
""" 显示宠物的信息。 """
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet(animal_type='hamster', pet_name='harry')
函数describe_pet() 还和之前一样,但调用这个函数时,向
Python明确地指出了各个实参对应的形参。看到这个函数调用
时,Python知道应该将实参'hamster' 和'harry' 分别赋给
形参animal_type 和pet_name 。输出正确无误,指出有一只
名为Harry的仓鼠。
关键字实参的顺序无关紧要,因为Python知道各个值该赋给哪个
形参。下面两个函数调用是等效的:
describe_pet(animal_type='hamster', pet_name='harry')
describe_pet(pet_name='harry', animal_type='hamster')
注 意 使用关键字实参时,务必准确指定函数定义中的形
参名。
8.2.3 默认值
编写函数时,可给每个形参指定默 认 值 。在调用函数中给形参
提供了实参时,Python将使用指定的实参值;否则,将使用形参
的默认值。因此,给形参指定默认值后,可在函数调用中省略相
应的实参。使用默认值可简化函数调用,还可清楚地指出函数的
典型用法。
例如,如果你发现调用describe_pet() 时,描述的大多是小
狗,就可将形参animal_type 的默认值设置为'dog' 。这
<!-- p234 -->
样,调用describe_pet() 来描述小狗时,就可不提供这种信
息:
def describe_pet(pet_name, animal_type='dog'):
""" 显示宠物的信息。 """
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet(pet_name='willie')
这里修改了函数describe_pet() 的定义,在其中给形参
animal_type 指定了默认值'dog' 。这样,调用这个函数
时,如果没有给animal_type 指定值,Python就将把这个形参
设置为'dog' :
I have a dog.
My dog's name is Willie.
请注意,在这个函数的定义中,修改了形参的排列顺序。因为给
animal_type 指定了默认值,无须通过实参来指定动物类型,
所以在函数调用中只包含一个实参——宠物的名字。然而,
Python依然将这个实参视为位置实参,因此如果函数调用中只包
含宠物的名字,这个实参将关联到函数定义中的第一个形参。这
就是需要将pet_name 放在形参列表开头的原因。
现在,使用这个函数的最简单方式是在函数调用中只提供小狗的
名字:
describe_pet('willie')
这个函数调用的输出与前一个示例相同。只提供了一个实
参'willie' ,这个实参将关联到函数定义中的第一个形参
pet_name 。由于没有给animal_type 提供实参,Python将使
用默认值'dog' 。
<!-- p235 -->
如果要描述的动物不是小狗,可使用类似于下面的函数调用:
describe_pet(pet_name='harry', animal_type='hamster')
由于显式地给animal_type 提供了实参,Python将忽略这个形
参的默认值。
注 意 使用默认值时,必须先在形参列表中列出没有默认
值的形参,再列出有默认值的实参。这让Python依然能够正
确地解读位置实参。
8.2.4 等效的函数调用
鉴于可混合使用位置实参、关键字实参和默认值,通常有多种等
效的函数调用方式。请看下面对函数describe_pet() 的定
义,其中给一个形参提供了默认值:
def describe_pet(pet_name, animal_type='dog'):
基于这种定义,在任何情况下都必须给pet_name 提供实参。指
定该实参时可采用位置方式,也可采用关键字方式。如果要描述
的动物不是小狗,还必须在函数调用中给animal_type 提供实
参。同样,指定该实参时可以采用位置方式,也可采用关键字方
式。
下面对这个函数的所有调用都可行:
一条名为 Willie 的小狗。
describe_pet('willie')
describe_pet(pet_name='willie')
一只名为 Harry 的仓鼠。
describe_pet('harry', 'hamster')
describe_pet(pet_name='harry', animal_type='hamster')
describe_pet(animal_type='hamster', pet_name='harry')
<!-- p236 -->
这些函数调用的输出与前面的示例相同。
注 意 使用哪种调用方式无关紧要,只要函数调用能生成
你期望的输出就行。使用对你来说最容易理解的调用方式即
可。
8.2.5 避免实参错误
等你开始使用函数后,如果遇到实参不匹配错误,不要大惊小
怪。你提供的实参多于或少于函数完成工作所需的信息时,将出
现实参不匹配错误。例如,如果调用函数describe_pet() 时
没有指定任何实参,结果将如何呢?
def describe_pet(animal_type, pet_name):
""" 显示宠物的信息。 """
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet()
Python发现该函数调用缺少必要的信息,traceback指出了这一
点:
Traceback (most recent call last):
❶ File "pets.py", line 6, in <module>
❷ describe_pet()
❸ TypeError: describe_pet() missing 2 required positional
arguments: 'animal_
type' and 'pet_name'
在 ❶ 处,traceback指出了问题出在什么地方,让我们能够回过头
去找出函数调用中的错误。在 ❷ 处,指出了导致问题的函数调
用。在 ❸ 处,traceback指出该函数调用少了两个实参,并指出了
<!-- p237 -->
相应形参的名称。如果这个函数存储在一个独立的文件中,我们
也许无须打开这个文件并查看函数的代码,就能重新正确地编写
函数调用。
Python读取函数的代码并指出需要为哪些形参提供实参,这提供
了极大的帮助。这也是应该给变量和函数指定描述性名称的另一
个原因:如果这样做了,那么无论对于你,还是可能使用你编写
的代码的其他任何人来说,Python提供的错误消息都将更帮助。
如果提供的实参太多,将出现类似的traceback,帮助你确保函数
调用和函数定义匹配。
动 手 试 一 试
练 习 8 - 3 : T 恤 编写一个名为make_shirt() 的函数,它
接受一个尺码以及要印到T恤上的字样。这个函数应打印一个
句子,概要地说明T恤的尺码和字样。
使用位置实参调用该函数来制作一件T恤,再使用关键字实参
来调用这个函数。
练 习 8 - 4 : 大 号 T 恤 修改函数make_shirt() ,使其在默
认情况下制作一件印有“I love Python”字样的大号T恤。
调用这个函数来制作:一件印有默认字样的大号T恤,一件印
有默认字样的中号T恤,以及一件印有其他字样的T恤(尺码
无关紧要)。
练 习 8 - 5 : 城 市 编写一个名为describe_city() 的函
数,它接受一座城市的名字以及该城市所属的国家。这个函
数应打印一个简单的句子,下面是一个例子。
Reykjavik is in Iceland.
给用于存储国家的形参指定默认值。为三座不同的城市调用
这个函数,且其中至少有一座城市不属于默认国家。
<!-- p238 -->
8.3 返回值
函数并非总是直接显示输出,它还可以处理一些数据,并返回一
个或一组值。函数返回的值称为返 回 值 。在函数中,可使用
return 语句将值返回到调用函数的代码行。返回值让你能够将
程序的大部分繁重工作移到函数中去完成,从而简化主程序。
8.3.1 返回简单值
下面来看一个函数,它接受名和姓并返回整洁的姓名:
formatted_name.py
❶ def get_formatted_name(first_name, last_name):
""" 返回整洁的姓名。 """
❷ full_name = f"{first_name} {last_name}"
❸ return full_name.title()
❹ musician = get_formatted_name('jimi', 'hendrix')
print(musician)
函数get_formatted_name() 的定义通过形参接受名和姓(见
❶ )。它将姓和名合而为一,在中间加上一个空格,并将结果赋
给变量full_name (见 ❷ )。然后,将full_name 的值转换
为首字母大写格式,并将结果返回到函数调用行(见 ❸ )。
调用返回值的函数时,需要提供一个变量,以便将返回的值赋给
它。在这里,将返回值赋给了变量musician (见 ❹ )。输出为
整洁的姓名:
Jimi Hendrix
<!-- p239 -->
原本只需编写下面的代码就可输出整洁的姓名,相比于此,前面
做的工作好像太多了:
print("Jimi Hendrix")
但在需要分别存储大量名和姓的大型程序中,像
get_formatted_name() 这样的函数非常有用。可以分别存储
名和姓,每当需要显示姓名时都调用这个函数。
8.3.2 让实参变成可选的
有时候,需要让实参变成可选的,这样使用函数的人就能只在必
要时提供额外的信息。可使用默认值来让实参变成可选的。
例如,假设要扩展函数get_formatted_name() ,使其同时处
理中间名。为此,可将其修改成类似于下面这样:
def get_formatted_name(first_name, middle_name, last_name):
""" 返回整洁的姓名。 """
full_name = f"{first_name} {middle_name} {last_name}"
return full_name.title()
musician = get_formatted_name('john', 'lee', 'hooker')
print(musician)
只要同时提供名、中间名和姓,这个函数就能正确运行。它根据
这三部分创建一个字符串,在适当的地方加上空格,并将结果转
换为首字母大写格式:
John Lee Hooker
并非所有的人都有中间名,但如果调用这个函数时只提供了名和
姓,它将不能正确运行。为了让中间名变成可选的,可给形参
middle_name 指定一个空的默认值,并在用户没有提供中间名
<!-- p240 -->
时不使用这个形参。为让get_formatted_name() 在没有提供
中间名时依然可行,可将形参middle_name 的默认值设置为空
字符串,并将其移到形参列表的末尾:
❶ def get_formatted_name(first_name, last_name,
middle_name=''):
""" 返回整洁的姓名。 """
❷ if middle_name:
full_name = f"{first_name} {middle_name}
{last_name}"
❸ else:
full_name = f"{first_name} {last_name}"
return full_name.title()
musician = get_formatted_name('jimi', 'hendrix')
print(musician)
❹ musician = get_formatted_name('john', 'hooker', 'lee')
print(musician)
在本例中,姓名是根据三个可能提供的部分创建的。由于人都有
名和姓,因此在函数定义中首先列出了这两个形参。中间名是可
选的,因此在函数定义中最后列出该形参,并将其默认值设置为
空字符串(见 ❶ )。
在函数体中,检查是否提供了中间名。Python将非空字符串解读
为True ,因此如果函数调用中提供了中间名,if
middle_name 将为True (见 ❷ )。如果提供了中间名,就将
名、中间名和姓合并为姓名,再将其修改为首字母大写格式,并
返回到函数调用行。在函数调用行,将返回的值赋给变量
musician ,然后这个变量的值被打印出来。如果没有提供中间
名,middle_name 将为空字符串,导致if 测试未通过,进而
执行else 代码块(见 ❸ ):只使用名和姓来生成姓名,并将格
式设置好的姓名返回给函数调用行。在函数调用行,将返回的值
赋给变量musician ,然后这个变量的值被打印出来。
调用这个函数时,如果只想指定名和姓,调用起来将非常简单。
如果还要指定中间名,就必须确保它是最后一个实参,这样
<!-- p241 -->
Python才能正确地将位置实参关联到形参(见 ❹ )。
这个修改后的版本不仅适用于只有名和姓的人,而且适用于还有
中间名的人:
Jimi Hendrix
John Lee Hooker
可选值让函数能够处理各种不同的情形,同时确保函数调用尽可
能简单。
8.3.3 返回字典
函数可返回任何类型的值,包括列表和字典等较复杂的数据结
构。例如,下面的函数接受姓名的组成部分,并返回一个表示人
的字典:
person.py
def build_person(first_name, last_name):
""" 返回一个字典,其中包含有关一个人的信息。 """
❶ person = {'first': first_name, 'last': last_name}
❷ return person
musician = build_person('jimi', 'hendrix')
❸ print(musician)
函数build_person() 接受名和姓,并将这些值放到字典中
(见 ❶ )。存储first_name 的值时,使用的键为'first' ,
而存储last_name 的值时,使用的键为'last' 。最后,返回
表示人的整个字典(见 ❷ )。在 ❸ 处,打印这个返回的值,此时
原来的两项文本信息存储在一个字典中:
{'first': 'jimi', 'last': 'hendrix'}
<!-- p242 -->
这个函数接受简单的文本信息,并将其放在一个更合适的数据结
构中,让你不仅能打印这些信息,还能以其他方式处理它们。当
前,字符串'jimi' 和'hendrix' 被标记为名和姓。你可以轻
松地扩展这个函数,使其接受可选值,如中间名、年龄、职业或
其他任何要存储的信息。例如,下面的修改让你能存储年龄:
def build_person(first_name, last_name, age=None):
""" 返回一个字典,其中包含有关一个人的信息。 """
person = {'first': first_name, 'last': last_name}
if age:
person['age'] = age
return person
musician = build_person('jimi', 'hendrix', age=27)
print(musician)
在函数定义中,新增了一个可选形参age ,并将其默认值设置为
特殊值None (表示变量没有值)。可将None 视为占位值。在
条件测试中,None 相当于False 。如果函数调用中包含形参
age 的值,这个值将被存储到字典中。在任何情况下,这个函数
都会存储人的姓名,但可进行修改,使其同时存储有关人的其他
信息。
8.3.4 结合使用函数和 while 循环
可将函数同本书前面介绍的任何Python结构结合起来使用。例
如,下面将结合使用函数get_formatted_name() 和while
循环,以更正式的方式问候用户。下面尝试使用名和姓跟用户打
招呼:
greeter.py
def get_formatted_name(first_name, last_name):
""" 返回整洁的姓名。 """
full_name = f"{first_name} {last_name}"
return full_name.title()
<!-- p243 -->
这是一个无限循环!
while True:
❶ print("\nPlease tell me your name:")
f_name = input("First name: ")
l_name = input("Last name: ")
formatted_name = get_formatted_name(f_name, l_name)
print(f"\nHello, {formatted_name}!")
在本例中,使用的是get_formatted_name() 的简单版本,不
涉及中间名。while 循环让用户输入姓名:依次提示用户输入名
和姓(见 ❶ )。
但这个while 循环存在一个问题:没有定义退出条件。请用户提
供一系列输入时,该在什么地方提供退出途径呢?要让用户能够
尽可能容易地退出,因此每次提示用户输入时,都应提供退出途
径。每次提示用户输入时,都使用break 语句提供退出循环的简
单途径:
def get_formatted_name(first_name, last_name):
""" 返回整洁的姓名。 """
full_name = f"{first_name} {last_name}"
return full_name.title()
while True:
print("\nPlease tell me your name:")
print("(enter 'q' at any time to quit)")
f_name = input("First name: ")
if f_name == 'q':
break
l_name = input("Last name: ")
if l_name == 'q':
break
formatted_name = get_formatted_name(f_name, l_name)
print(f"\nHello, {formatted_name}!")
<!-- p244 -->
我们添加了一条消息来告诉用户如何退出,然后在每次提示用户
输入时,都检查他输入的是否是退出值。如果是,就退出循环。
现在,这个程序将不断地问候,直到用户输入的姓或名为'q' :
Please tell me your name:
(enter 'q' at any time to quit)
First name: eric
Last name: matthes
Hello, Eric Matthes!
Please tell me your name:
(enter 'q' at any time to quit)
First name: q
动 手 试 一 试
练 习 8 - 6 : 城 市 名 编写一个名为city_country() 的函
数,它接受城市的名称及其所属的国家。这个函数应返回一
个格式类似于下面的字符串:
"Santiago, Chile"
至少使用三个城市国家对来调用这个函数,并打印它返回的
值。
练 习 8 - 7 : 专 辑 编写一个名为make_album() 的函数,
它创建一个描述音乐专辑的字典。这个函数应接受歌手的名
字和专辑名,并返回一个包含这两项信息的字典。使用这个
函数创建三个表示不同专辑的字典,并打印每个返回的值,
以核实字典正确地存储了专辑的信息。
给函数make_album() 添加一个默认值为None 的可选形
参,以便存储专辑包含的歌曲数。如果调用这个函数时指定
了歌曲数,就将该值添加到表示专辑的字典中。调用这个函
数,并至少在一次调用中指定专辑包含的歌曲数。
<!-- p245 -->
练 习 8 - 8 : 用 户 的 专 辑 在为完成练习8-7编写的程序中,
编写一个while 循环,让用户输入专辑的歌手和名称。获取
这些信息后,使用它们来调用函数make_album() 并将创
建的字典打印出来。在这个while 循环中,务必提供退出途
径。
<!-- p246 -->
8.4 传递列表
你经常会发现,向函数传递列表很有用,其中包含的可能是名
字、数或更复杂的对象(如字典)。将列表传递给函数后,函数
就能直接访问其内容。下面使用函数来提高处理列表的效率。
假设有一个用户列表,我们要问候其中的每位用户。下面的示例
将包含名字的列表传递给一个名为greet_users() 的函数,这
个函数问候列表中的每个人:
greet_users.py
def greet_users(names):
""" 向列表中的每位用户发出简单的问候。 """
for name in names:
msg = f"Hello, {name.title()}!"
print(msg)
❶ usernames = ['hannah', 'ty', 'margot']
greet_users(usernames)
我们将greet_users() 定义为接受一个名字列表,并将其赋给
形参names 。这个函数遍历收到的列表,并对其中的每位用户打
印一条问候语。 ❶ 处定义了一个用户列表usernames ,然后调
用greet_users() 并将该列表传递给它:
Hello, Hannah!
Hello, Ty!
Hello, Margot!
输出完全符合预期。每位用户都看到了一条个性化的问候语。每
当需要问候一组用户时,都可调用这个函数。
8.4.1 在函数中修改列表
<!-- p247 -->
将列表传递给函数后,函数就可对其进行修改。在函数中对这个
列表所做的任何修改都是永久性的,这让你能够高效地处理大量
数据。
来看一家为用户提交的设计制作3D打印模型的公司。需要打印的
设计存储在一个列表中,打印后将移到另一个列表中。下面是在
不使用函数的情况下模拟这个过程的代码:
printing_models.py
首先创建一个列表,其中包含一些要打印的设计。
unprinted_designs = ['phone case', 'robot pendant',
'dodecahedron']
completed_models = []
模拟打印每个设计,直到没有未打印的设计为止。
打印每个设计后,都将其移到列表 completed_models 中。
while unprinted_designs:
current_design = unprinted_designs.pop()
print(f"Printing model: {current_design}")
completed_models.append(current_design)
显示打印好的所有模型。
print("\nThe following models have been printed:")
for completed_model in completed_models:
print(completed_model)
这个程序首先创建一个需要打印的设计列表,以及一个名为
completed_models 的空列表,每个设计打印后都将移到其
中。只要列表unprinted_designs 中还有设计,while 循环
就模拟打印设计的过程:从该列表末尾删除一个设计,将其赋给
变量current_design ,并显示一条消息指出正在打印当前的
设计,然后将该设计加入到列表completed_models 中。循环
结束后,显示已打印的所有设计:
Printing model: dodecahedron
Printing model: robot pendant
Printing model: phone case
The following models have been printed:
dodecahedron
<!-- p248 -->
robot pendant
phone case
为重新组织这些代码,可编写两个函数,每个都做一件具体的工
作。大部分代码与原来相同,只是效率更高。第一个函数负责处
理打印设计的工作,第二个概述打印了哪些设计:
❶ def print_models(unprinted_designs, completed_models):
"""
模拟打印每个设计,直到没有未打印的设计为止。
打印每个设计后,都将其移到列表 completed_models 中。
"""
while unprinted_designs:
current_design = unprinted_designs.pop()
print(f"Printing model: {current_design}")
completed_models.append(current_design)
❷ def show_completed_models(completed_models):
""" 显示打印好的所有模型。 """
print("\nThe following models have been printed:")
for completed_model in completed_models:
print(completed_model)
unprinted_designs = ['phone case', 'robot pendant',
'dodecahedron']
completed_models = []
print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)
❶ 处定义了函数print_models() ,它包含两个形参:一个需
要打印的设计列表和一个打印好的模型列表。给定这两个列表,
该函数模拟打印每个设计的过程:将设计逐个从未打印的设计列
表中取出,并加入打印好的模型列表中。 ❷ 处定义了函数
show_completed_models() ,它包含一个形参:打印好的模
型列表。给定这个列表,函数show_completed_models() 显
示打印出来的每个模型的名称。
<!-- p249 -->
这个程序的输出与未使用函数的版本相同,但组织更为有序。完
成大部分工作的代码都移到了两个函数中,让主程序更容易理
解。只要看看主程序,就会发现这个程序的功能清晰得多:
unprinted_designs = ['phone case', 'robot pendant',
'dodecahedron']
completed_models = []
print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)
我们创建了一个未打印的设计列表,还创建了一个空列表,用于
存储打印好的模型。接下来,由于已经定义了两个函数,只需调
用它们并传入正确的实参即可。我们调用print_models() 并
向它传递两个列表。像预期一样,print_models() 模拟打印
设计的过程。接下来,调用show_completed_models() ,并
将打印好的模型列表传递给它,让其能够指出打印了哪些模型。
描述性的函数名让别人阅读这些代码时也能明白,尽管没有任何
注释。
相比于没有使用函数的版本,这个程序更容易扩展和维护。如果
以后需要打印其他设计,只需再次调用print_models() 即
可。如果发现需要对打印代码进行修改,只需修改这些代码一
次,就能影响所有调用该函数的地方。与必须分别修改程序的多
个地方相比,这种修改的效率更高。
该程序还演示了这样一种理念:每个函数都应只负责一项具体的
工作。第一个函数打印每个设计,第二个显示打印好的模型。这
优于使用一个函数来完成这两项工作。编写函数时,如果发现它
执行的任务太多,请尝试将这些代码划分到两个函数中。别忘
了,总是可以在一个函数中调用另一个函数,这有助于将复杂的
任务划分成一系列步骤。
8.4.2 禁止函数修改列表
有时候,需要禁止函数修改列表。例如,假设像前一个示例那
样,你有一个未打印的设计列表,并编写了一个函数将这些设计
<!-- p250 -->
移到打印好的模型列表中。你可能会做出这样的决定:即便打印
好了所有设计,也要保留原来的未打印的设计列表,以供备案。
但由于你将所有的设计都移出了unprinted_designs ,这个
列表变成了空的,原来的列表没有了。为解决这个问题,可向函
数传递列表的副本而非原件。这样,函数所做的任何修改都只影
响副本,而原件丝毫不受影响。
要将列表的副本传递给函数,可以像下面这样做:
function_name(list_name_[:])
切片表示法[:] 创建列表的副本。在printing_models.py中,如
果不想清空未打印的设计列表,可像下面这样调用
print_models() :
print_models(unprinted_designs[:], completed_models)
这样函数print_models() 依然能够完成工作,因为它获得了
所有未打印的设计的名称,但使用的是列表
unprinted_designs 的副本,而不是列表
unprinted_designs 本身。像以前一样,列表
completed_models 也将包含打印好的模型的名称,但函数所
做的修改不会影响到列表unprinted_designs 。
虽然向函数传递列表的副本可保留原始列表的内容,但除非有充
分的理由,否则还是应该将原始列表传递给函数。这是因为让函
数使用现成的列表可避免花时间和内存创建副本,从而提高效
率,在处理大型列表时尤其如此。
动 手 试 一 试
练 习 8 - 9 : 消 息 创建一个列表,其中包含一系列简短的文
本消息。将该列表传递给一个名为show_messages() 的函
数,这个函数会打印列表中的每条文本消息。
<!-- p251 -->
练 习 8 - 1 0 : 发 送 消 息 在你为完成练习8-9而编写的程序
中,编写一个名为send_messages() 的函数,将每条消息
都打印出来并移到一个名为sent_messages 的列表中。调
用函数send_messages() ,再将两个列表都打印出来,确
认正确地移动了消息。
练 习 8 - 1 1 : 消 息 归 档 修改你为完成练习8-10而编写的程
序,在调用函数send_messages() 时,向它传递消息列表
的副本。调用函数send_messages() 后,将两个列表都打
印出来,确认保留了原始列表中的消息。
<!-- p252 -->
8.5 传递任意数量的实参
有时候,预先不知道函数需要接受多少个实参,好在Python允许
函数从调用语句中收集任意数量的实参。
例如,来看一个制作比萨的函数,它需要接受很多配料,但无法
预先确定顾客要多少种配料。下面的函数只有一个形参
*toppings ,但不管调用语句提供了多少实参,这个形参会将
它们统统收入囊中:
pizza.py
def make_pizza(*toppings):
""" 打印顾客点的所有配料。 """
print(toppings)
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
形参名*toppings 中的星号让Python创建一个名为toppings
的空元组,并将收到的所有值都封装到这个元组中。函数体内的
函数调用print() 通过生成输出,证明Python能够处理使用一
个值来调用函数的情形,也能处理使用三个值来调用函数的情
形。它以类似的方式处理不同的调用。注意,Python将实参封装
到一个元组中,即便函数只收到一个值:
('pepperoni',)
('mushrooms', 'green peppers', 'extra cheese')
现在,可以将函数调用print() 替换为一个循环,遍历配料列
表并对顾客点的比萨进行描述:
def make_pizza(*toppings):
""" 概述要制作的比萨。 """
<!-- p253 -->
print("\nMaking a pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
不管收到一个值还是三个值,这个函数都能妥善处理:
Making a pizza with the following toppings:
- pepperoni
Making a pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
不管函数收到的实参是多少个,这种语法都管用。
8 . 5 . 1 结 合 使 用 位 置 实 参 和 任 意 数 量 实 参
如果要让函数接受不同类型的实参,必须在函数定义中将接纳任
意数量实参的形参放在最后。Python先匹配位置实参和关键字实
参,再将余下的实参都收集到最后一个形参中。
例如,如果前面的函数还需要一个表示比萨尺寸的形参,必须将
其放在形参*toppings 的前面:
def make_pizza(size, *toppings):
""" 概述要制作的比萨。 """
print(f"\nMaking a {size}-inch pizza with the following
toppings:")
for topping in toppings:
print(f"- {topping}")
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
<!-- p254 -->
基于上述函数定义,Python将收到的第一个值赋给形参size ,
并将其他所有值都存储在元组toppings 中。在函数调用中,首
先指定表示比萨尺寸的实参,再根据需要指定任意数量的配料。
现在,每个比萨都有了尺寸和一系列配料,而且这些信息按正确
的顺序打印出来了——首先是尺寸,然后是配料:
Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
注 意 你经常会看到通用形参名*args ,它也收集任意数
量的位置实参。
8.5.2 使用任意数量的关键字实参
有时候,需要接受任意数量的实参,但预先不知道传递给函数的
会是什么样的信息。在这种情况下,可将函数编写成能够接受任
意数量的键值对——调用语句提供了多少就接受多少。一个这样
的示例是创建用户简介:你知道将收到有关用户的信息,但不确
定会是什么样的信息。在下面的示例中,函数
build_profile() 接受名和姓,还接受任意数量的关键字实
参:
user_profile.py
def build_profile(first, last, **user_info):
""" 创建一个字典,其中包含我们知道的有关用户的一切。 """
❶ user_info['first_name'] = first
user_info['last_name'] = last
return user_info
user_profile = build_profile('albert', 'einstein',
location='princeton',
field='physics')
<!-- p255 -->
print(user_profile)
函数build_profile() 的定义要求提供名和姓,同时允许根据
需要提供任意数量的名称值对。形参**user_info 中的两个星
号让Python创建一个名为user_info 的空字典,并将收到的所
有名称值对都放到这个字典中。在这个函数中,可以像访问其他
字典那样访问user_info 中的名称值对。
在build_profile() 的函数体内,将名和姓加入了字典
user_info 中(见 ❶ ),因为总是会从用户那里收到这两项信
息,而这两项信息没有放到这个字典中。接下来,将字典
user_info 返回到函数调用行。
我们调用build_profile() ,向它传递名('albert' )、
姓('einstein' )和两个键值对(location='princeton'
和field='physics' ),并将返回的user_info 赋给变量
user_profile ,再打印该变量:
{'location': 'princeton', 'field': 'physics',
'first_name': 'albert', 'last_name': 'einstein'}
在这里,返回的字典包含用户的名和姓,还有求学的地方和所学
专业。调用这个函数时,不管额外提供多少个键值对,它都能正
确地处理。
编写函数时,能以各种方式混合使用位置实参、关键字实参和任
意数量的实参。知道这些实参类型大有裨益,因为阅读别人编写
的代码时经常会见到它们。要正确地使用这些类型的实参并知道
其使用时机,需要经过一定的练习。就目前而言,牢记使用最简
单的方法来完成任务就好了。继续往下阅读,你就会知道在各种
情况下哪种方法的效率最高。
注 意 你经常会看到形参名**kwargs ,它用于收集任意
数量的关键字实参。
<!-- p256 -->
动 手 试 一 试
练 习 8 - 1 2 : 三 明 治 编写一个函数,它接受顾客要在三明
治中添加的一系列食材。这个函数只有一个形参(它收集函
数调用中提供的所有食材),并打印一条消息,对顾客点的
三明治进行概述。调用这个函数三次,每次都提供不同数量
的实参。
练 习 8 - 1 3 : 用 户 简 介 复制前面的程序user_profile.py,
在其中调用build_profile() 来创建有关你的简介。调用
这个函数时,指定你的名和姓,以及三个描述你的键值对。
练 习 8 - 1 4 : 汽 车 编写一个函数,将一辆汽车的信息存储
在字典中。这个函数总是接受制造商和型号,还接受任意数
量的关键字实参。这样调用该函数:提供必不可少的信息,
以及两个名称值对,如颜色和选装配件。这个函数必须能够
像下面这样进行调用:
car = make_car('subaru', 'outback', color='blue',
tow_package=True)
打印返回的字典,确认正确地处理了所有的信息。
<!-- p257 -->
8.6 将函数存储在模块中
使用函数的优点之一是可将代码块与主程序分离。通过给函数指
定描述性名称,可让主程序容易理解得多。你还可以更进一步,
将函数存储在称为模 块 的独立文件中,再将模块导 入 到主程序
中。import 语句允许在当前运行的程序文件中使用模块中的代
码。
通过将函数存储在独立的文件中,可隐藏程序代码的细节,将重
点放在程序的高层逻辑上。这还能让你在众多不同的程序中重用
函数。将函数存储在独立文件中后,可与其他程序员共享这些文
件而不是整个程序。知道如何导入函数还能让你使用其他程序员
编写的函数库。
导入模块的方法有多种,下面对每种进行简要的介绍。
8.6.1 导入整个模块
要让函数是可导入的,得先创建模块。模 块 是扩展名为.py的文
件,包含要导入到程序中的代码。下面来创建一个包含函数
make_pizza() 的模块。为此,将文件pizza.py中除函数
make_pizza() 之外的其他代码删除:
pizza.py
def make_pizza(size, *toppings):
""" 概述要制作的比萨。 """
print(f"\nMaking a {size}-inch pizza with the following
toppings:")
for topping in toppings:
print(f"- {topping}")
接下来,在pizza.py所在的目录中创建一个名为
making_pizzas.py的文件。这个文件导入刚创建的模块,再调用
make_pizza() 两次:
<!-- p258 -->
making_pizzas.py
import pizza
❶ pizza.make_pizza(16, 'pepperoni')
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra
cheese')
Python读取这个文件时,代码行import pizza 让Python打开
文件pizza.py,并将其中的所有函数都复制到这个程序中。你看
不到复制的代码,因为在这个程序即将运行时,Python在幕后复
制了这些代码。你只需知道,在making_pizzas.py中,可使用
pizza.py中定义的所有函数。
要调用被导入模块中的函数,可指定被导入模块的名称pizza 和
函数名make_pizza() ,并用句点分隔(见 ❶ )。这些代码的输
出与没有导入模块的原始程序相同:
Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese
这就是一种导入方法:只需编写一条import 语句并在其中指定
模块名,就可在程序中使用该模块中的所有函数。如果使用这种
import 语句导入了名为module_name.py的整个模块,就可使用
下面的语法来使用其中任何一个函数:
module_name.function_name()
8.6.2 导入特定的函数
<!-- p259 -->
还可以导入模块中的特定函数,这种导入方法的语法如下:
from module_name import function_name
通过用逗号分隔函数名,可根据需要从模块中导入任意数量的函
数:
from module_name import function_0, function_1, function_2
对于前面的making_pizzas.py示例,如果只想导入要使用的函
数,代码将类似于下面这样:
from pizza import make_pizza
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
使用这种语法时,调用函数时无须使用句点。由于在import 语
句中显式地导入了函数make_pizza() ,调用时只需指定其名
称即可。
8.6.3 使用 as 给函数指定别名
如果要导入函数的名称可能与程序中现有的名称冲突,或者函数
的名称太长,可指定简短而独一无二的别 名 :函数的另一个名
称,类似于外号。要给函数取这种特殊外号,需要在导入它时指
定。
下面给函数make_pizza() 指定了别名mp() 。这是在import
语句中使用make_pizza as mp 实现的,关键字as 将函数重
命名为指定的别名:
from pizza import make_pizza as mp
<!-- p260 -->
mp(16, 'pepperoni')
mp(12, 'mushrooms', 'green peppers', 'extra cheese')
上面的import 语句将函数make_pizza() 重命名为mp() 。
在这个程序中,每当需要调用make_pizza() 时,都可简写成
mp() 。Python将运行make_pizza() 中的代码,避免与这个
程序可能包含的函数make_pizza() 混淆。
指定别名的通用语法如下:
from module_name import function_name as fn
8.6.4 使用 as 给模块指定别名
还可以给模块指定别名。通过给模块指定简短的别名(如给模块
pizza 指定别名p ),让你能够更轻松地调用模块中的函数。
相比于pizza.make_pizza() ,p.make_pizza() 更为简
洁:
import pizza as p
p.make_pizza(16, 'pepperoni')
p.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
上述import 语句给模块pizza 指定了别名p ,但该模块中所
有函数的名称都没变。要调用函数make_pizza() ,可编写代
码p.make_pizza() 而非pizza.make_pizza() 。这样不仅
代码更简洁,还让你不用再关注模块名,只专注于描述性的函数
名。这些函数名明确指出了函数的功能,对于理解代码而言,比
模块名更重要。
给模块指定别名的通用语法如下:
<!-- p261 -->
import module_name as mn
8.6.5 导入模块中的所有函数
使用星号(* )运算符可让Python导入模块中的所有函数:
from pizza import *
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
import 语句中的星号让Python将模块pizza 中的每个函数都
复制到这个程序文件中。由于导入了每个函数,可通过名称来调
用每个函数,而无须使用句点表示法。然而,使用并非自己编写
的大型模块时,最好不要采用这种导入方法。这是因为如果模块
中有函数的名称与当前项目中使用的名称相同,可能导致意想不
到的结果:Python可能遇到多个名称相同的函数或变量,进而覆
盖函数,而不是分别导入所有的函数。
最佳的做法是,要么只导入需要使用的函数,要么导入整个模块
并使用句点表示法。这让代码更清晰,更容易阅读和理解。这里
之所以介绍这种导入方法,只是想让你在阅读别人编写的代码
时,能够理解类似于下面的import 语句:
from module_name import *
<!-- p262 -->
8.7 函数编写指南
编写函数时,需要牢记几个细节。应给函数指定描述性名称,且
只在其中使用小写字母和下划线。描述性名称可帮助你和别人明
白代码想要做什么。给模块命名时也应遵循上述约定。
每个函数都应包含简要地阐述其功能的注释。该注释应紧跟在函
数定义后面,并采用文档字符串格式。文档良好的函数让其他程
序员只需阅读文档字符串中的描述就能够使用它。他们完全可以
相信代码如描述的那样运行,并且只要知道函数的名称、需要的
实参以及返回值的类型,就能在自己的程序中使用它。
给形参指定默认值时,等号两边不要有空格:
def function_name(parameter_0, parameter_1='default value')
对于函数调用中的关键字实参,也应遵循这种约定:
function_name(value_0, parameter_1='value')
PEP 8建议代码行的长度不要超过79字符,这样只要编辑器窗口适
中,就能看到整行代码。如果形参很多,导致函数定义的长度超
过了79字符,可在函数定义中输入左括号后按回车键,并在下一
行按两次Tab键,从而将形参列表和只缩进一层的函数体区分开
来。
大多数编辑器会自动对齐后续参数列表行,使其缩进程度与你给
第一个参数列表行指定的缩进程度相同:
def function_name(
parameter_0, parameter_1, parameter_2,
parameter_3, parameter_4, parameter_5):
function body...
<!-- p263 -->
如果程序或模块包含多个函数,可使用两个空行将相邻的函数分
开,这样将更容易知道前一个函数在什么地方结束,下一个函数
从什么地方开始。
所有import 语句都应放在文件开头。唯一例外的情形是,在文
件开头使用了注释来描述整个程序。
动 手 试 一 试
练 习 8 - 1 5 : 打 印 模 型 将示例printing_models.py中的函
数放在一个名为printing_functions.py的文件中。在
printing_models.py的开头编写一条import 语句,并修改
该文件以使用导入的函数。
练 习 8 - 1 6 : 导 入 选择一个你编写的且只包含一个函数的
程序,将该函数放在另一个文件中。在主程序文件中,使用
下述各种方法导入这个函数,再调用它:
import module_name
from module_name import function_name
from module_name import function_name as fn
import module_name as mn
from module_name import *
练 习 8 - 1 7 : 函 数 编 写 指 南 选择你在本章中编写的三个程
序,确保它们遵循了本节介绍的函数编写指南。
<!-- p264 -->
8.8 小结
在本章中,你学习了:如何编写函数,以及如何传递实参,让函
数能够访问完成其工作所需的信息;如何使用位置实参和关键字
实参,以及如何接受任意数量的实参;显示输出的函数和返回值
的函数;如何将函数同列表、字典、if 语句和while 循环结合
起来使用;如何将函数存储在称为模 块 的独立文件中,让程序
文件更简单、更易于理解。最后,你学习了函数编写指南,遵循
这些指南可让程序始终结构良好,并对你和其他人来说易于阅
读。
程序员的目标之一是,编写简单的代码来完成任务,而函数有助
于你实现这样的目标。它们让你编写好代码块并确定其能够正确
运行后,就可置之不理。确定函数能够正确地完成其工作后,你
就可以接着投身于下一个编码任务。
函数让你编写代码一次后,想重用它们多少次就重用多少次。需
要运行函数中的代码时,只需编写一行函数调用代码,就可让函
数完成其工作。需要修改函数的行为时,只需修改一个代码块,
而所做的修改将影响调用这个函数的每个地方。
使用函数让程序更容易阅读,而良好的函数名概述了程序各个部
分的作用。相对于阅读一系列的代码块,阅读一系列函数调用让
你能够更快地明白程序的作用。
函数还让代码更容易测试和调试。如果程序使用一系列的函数来
完成其任务,而其中的每个函数都完成一项具体的工作,测试和
维护起来将容易得多:可编写分别调用每个函数的程序,并测试
每个函数是否在它可能遇到的各种情形下都能正确地运行。经过
这样的测试后你就能充满信心,深信每次调用这些函数时,它们
都将正确地运行。
在第9章,你将学习编写类。类将函数和数据整洁地封装起来,让
你能够灵活而高效地使用它们。
<!-- p265 -->
第 9 章 类
面 向 对 象 编 程 是最有效的软件编写方法之一。
在面向对象编程中,你编写表示现实世界中的事物和情景的
类,并基于这些类来创建对象。编写类时,你定义一大类对
象都有的通用行为。基于类创建对 象 时,每个对象都自动
具备这种通用行为,然后可根据需要赋予每个对象独特的个
性。使用面向对象编程可模拟现实情景,其逼真程度达到了
令人惊讶的地步。
根据类来创建对象称为实 例 化 ,这让你能够使用类的实
例。在本章中,你将编写一些类并创建其实例。你将指定可
在实例中存储什么信息,定义可对这些实例执行哪些操作。
你还将编写一些类来扩展既有类的功能,让相似的类能够高
效地共享代码。你将把自己编写的类存储在模块中,并在自
己的程序文件中导入其他程序员编写的类。
理解面向对象编程有助于你像程序员那样看世界,还可以帮
助你真正明白自己编写的代码:不仅是各行代码的作用,还
有代码背后更宏大的概念。了解类背后的概念可培养逻辑思
维,让你能够通过编写程序来解决遇到的几乎任何问题。
随着面临的挑战日益严峻,类还能让你以及与你合作的其他
程序员的生活更轻松。如果你与其他程序员基于同样的逻辑
来编写代码,你们就能明白对方所做的工作。你编写的程序
将能被众多合作者所理解,每个人都能事半功倍。
<!-- p266 -->
9.1 创建和使用类
使用类几乎可以模拟任何东西。下面来编写一个表示小狗的简单
类Dog ,它表示的不是特定的小狗,而是任何小狗。对于大多数
宠物狗,我们都知道些什么呢?它们都有名字和年龄。我们还知
道,大多数小狗还会蹲下和打滚。由于大多数小狗都具备上述两
项信息(名字和年龄)和两种行为(蹲下和打滚),我们的Dog
类将包含它们。这个类让Python知道如何创建表示小狗的对象。
编写这个类后,我们将使用它来创建表示特定小狗的实例。
9.1.1 创建 Dog 类
根据Dog 类创建的每个实例都将存储名字和年龄,我们赋予了每
条小狗蹲下(sit() )和打滚(roll_over() )的能力:
dog.py
❶ class Dog:
❷ """ 一次模拟小狗的简单尝试。 """
❸ def __init__(self, name, age):
""" 初始化属性 name 和 age 。 """
❹ self.name = name
self.age = age
❺ def sit(self):
""" 模拟小狗收到命令时蹲下。 """
print(f"{self.name} is now sitting.")
def roll_over(self):
""" 模拟小狗收到命令时打滚。 """
print(f"{self.name} rolled over!")
这里需要注意的地方很多,但也不用担心,本章充斥着这样的结
构,你有大把的机会熟悉它。 ❶ 处定义了一个名为Dog 的类。根
据约定,在Python中,首字母大写的名称指的是类。这个类定义
<!-- p267 -->
中没有圆括号,因为要从空白创建这个类。 ❷ 处编写了一个文档
字符串,对这个类的功能做了描述。
方 法 __init__()
类中的函数称为方 法 。你在前面学到的有关函数的一切都适用
于方法,就目前而言,唯一重要的差别是调用方法的方式。 ❸ 处
的方法__init__() 是一个特殊方法,每当你根据Dog 类创建
新实例时,Python都会自动运行它。在这个方法的名称中,开头
和末尾各有两个下划线,这是一种约定,旨在避免Python默认方
法与普通方法发生名称冲突。务必确保__init__() 的两边都有
两个下划线,否则当你使用类来创建实例时,将不会自动调用这
个方法,进而引发难以发现的错误。
我们将方法__init__() 定义成包含三个形参:self 、name
和age 。在这个方法的定义中,形参self 必不可少,而且必须
位于其他形参的前面。为何必须在方法定义中包含形参self
呢?因为Python调用这个方法来创建Dog 实例时,将自动传入实
参self 。每个与实例相关联的方法调用都自动传递实参self
,它是一个指向实例本身的引用,让实例能够访问类中的属性和
方法。创建Dog 实例时,Python将调用Dog 类的方法
__init__() 。我们将通过实参向Dog() 传递名字和年龄,
self 会自动传递,因此不需要传递它。每当根据Dog 类创建实
例时,都只需给最后两个形参(name 和age )提供值。
❹ 处定义的两个变量都有前缀self 。以self 为前缀的变量可
供类中的所有方法使用,可以通过类的任何实例来访问。
self.name = name 获取与形参name 相关联的值,并将其赋
给变量name ,然后该变量被关联到当前创建的实例。
self.age = age 的作用与此类似。像这样可通过实例访问的
变量称为属 性 。
Dog 类还定义了另外两个方法:sit() 和roll_over() (见
❺ )。这些方法执行时不需要额外的信息,因此它们只有一个形
参self 。我们随后将创建的实例能够访问这些方法,换句话
说,它们都会蹲下和打滚。当前,sit() 和roll_over() 所
<!-- p268 -->
做的有限,只是打印一条消息,指出小狗正在蹲下或打滚。但可
以扩展这些方法以模拟实际情况:如果这个类包含在一个计算机
游戏中,这些方法将包含创建小狗蹲下和打滚动画效果的代码;
如果这个类是用于控制机器狗的,这些方法将让机器狗做出蹲下
和打滚的动作。
9.1.2 根据类创建实例
可将类视为有关如何创建实例的说明。Dog 类是一系列说明,让
Python知道如何创建表示特定小狗的实例。
下面来创建一个表示特定小狗的实例:
class Dog:
--snip--
❶ my_dog = Dog('Willie', 6)
❷ print(f"My dog's name is {my_dog.name}.")
❸ print(f"My dog is {my_dog.age} years old.")
这这里使用的是前一个示例中编写的Dog 类。在 ❶ 处,让Python
创建一条名字为'Willie' 、年龄为6 的小狗。遇到这行代码
时,Python使用实参'Willie' 和6 调用Dog 类的方法
__init__() 。方法__init__() 创建一个表示特定小狗的实
例,并使用提供的值来设置属性name 和age 。接下来,Python
返回一个表示这条小狗的实例,而我们将这个实例赋给了变量
my_dog 。在这里,命名约定很有用:通常可认为首字母大写的
名称(如Dog )指的是类,而小写的名称(如my_dog )指的是
根据类创建的实例。
a. 访 问 属 性
要访问实例的属性,可使用句点表示法。 ❷ 处编写了如下代
码来访问my_dog 的属性name 的值:
<!-- p269 -->
my_dog.name
句点表示法在Python中很常用,这种语法演示了Python如何
获悉属性的值。在这里, Python先找到实例my_dog ,再
查找与该实例相关联的属性name 。在Dog 类中引用这个属
性时,使用的是self.name 。在 ❸ 处,使用同样的方法来
获取属性age 的值。
输出是有关my_dog 的摘要:
My dog's name is Willie.
My dog is 6 years old.
b. 调 用 方 法
根据Dog 类创建实例后,就能使用句点表示法来调用Dog
类中定义的任何方法了。下面来让小狗蹲下和打滚:
class Dog:
--snip--
my_dog = Dog('Willie', 6)
my_dog.sit()
my_dog.roll_over()
要调用方法,可指定实例的名称(这里是my_dog )和要调
用的方法,并用句点分隔。遇到代码my_dog.sit() 时,
Python在类Dog 中查找方法sit() 并运行其代码。Python
以同样的方式解读代码my_dog.roll_over() 。
Willie按我们的命令做了:
<!-- p270 -->
Willie is now sitting.
Willie rolled over!
这种语法很有用。如果给属性和方法指定了合适的描述性名
称,如name 、age 、sit() 和roll_over() ,即便是
从未见过的代码块,我们也能够轻松地推断出它是做什么
的。
c. 创 建 多 个 实 例
可按需求根据类创建任意数量的实例。下面再创建一个名为
your_dog 的小狗实例:
class Dog:
--snip--
my_dog = Dog('Willie', 6)
your_dog = Dog('Lucy', 3)
print(f"My dog's name is {my_dog.name}.")
print(f"My dog is {my_dog.age} years old.")
my_dog.sit()
print(f"\nYour dog's name is {your_dog.name}.")
print(f"Your dog is {your_dog.age} years old.")
your_dog.sit()
在本例中创建了两条小狗,分别名为Willie和Lucy。每条小
狗都是一个独立的实例,有自己的一组属性,能够执行相同
的操作:
My dog's name is Willie.
My dog is 6 years old.
Willie is now sitting.
Your dog's name is Lucy.
Your dog is 3 years old.
Lucy is now sitting.
<!-- p271 -->
即使给第二条小狗指定同样的名字和年龄,Python依然会根
据Dog 类创建另一个实例。你可按需求根据一个类创建任意
数量的实例,条件是将每个实例都存储在不同的变量中,或
者占用列表或字典的不同位置。
动 手 试 一 试
练 习 9 - 1 : 餐 馆 创建一个名为Restaurant 的类,为其
方法__init__() 设置属性restaurant_name 和
cuisine_type 。创建一个名为
describe_restaurant() 的方法和一个名为
open_restaurant() 的方法,前者打印前述两项信息,
而后者打印一条消息,指出餐馆正在营业。
根据这个类创建一个名为restaurant 的实例,分别打印其
两个属性,再调用前述两个方法。
练 习 9 - 2 : 三 家 餐 馆 根据为完成练习9-1而编写的类创建
三个实例,并对每个实例调用方法
describe_restaurant() 。
练 习 9 - 3 : 用 户 创建一个名为User 的类,其中包含属性
first_name 和last_name ,以及用户简介通常会存储的
其他几个属性。在类User 中定义一个名为
describe_user() 的方法,用于打印用户信息摘要。再定
义一个名为greet_user() 的方法,用于向用户发出个性
化的问候。
创建多个表示不同用户的实例,并对每个实例调用上述两个
方法。
<!-- p272 -->
9.2 使用类和实例
可使用类来模拟现实世界中的很多情景。类编写好后,你的大部
分时间将花在根据类创建的实例上。你需要执行的一个重要任务
是修改实例的属性。可以直接修改实例的属性,也可以编写方法
以特定的方式进行修改。
9.2.1 Car 类
下面来编写一个表示汽车的类。它存储了有关汽车的信息,还有
一个汇总这些信息的方法:
car.py
class Car:
""" 一次模拟汽车的简单尝试。 """
❶ def __init__(self, make, model, year):
""" 初始化描述汽车的属性。 """
self.make = make
self.model = model
self.year = year
❷ def get_descriptive_name(self):
""" 返回整洁的描述性信息。 """
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()
❸ my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
在 ❶ 处,定义了方法__init__() 。与前面的Dog 类中一样,
这个方法的第一个形参为self 。该方法还包含另外三个形参:
make 、model 和year 。方法__init__() 接受这些形参的
值,并将它们赋给根据这个类创建的实例的属性。创建新的Car
实例时,需要指定其制造商、型号和生产年份。
<!-- p273 -->
在 ❷ 处,定义了一个名为get_descriptive_name() 的方法。
它使用属性year 、make 和model 创建一个对汽车进行描述的
字符串,让我们无须分别打印每个属性的值。为在这个方法中访
问属性的值,使用了self.make 、self.model 和
self.year 。在 ❸ 处,根据Car 类创建了一个实例,并将其赋
给变量my_new_car 。接下来,调用方法
get_descriptive_name() ,指出我们拥有一辆什么样的汽
车:
2019 Audi A4
为了让这个类更有趣,下面给它添加一个随时间变化的属性,用
于存储汽车的总里程。
9.2.2 给属性指定默认值
创建实例时,有些属性无须通过形参来定义,可在方法
__init__() 中为其指定默认值。
下面来添加一个名为odometer_reading 的属性,其初始值总
是为0。我们还添加了一个名为read_odometer() 的方法,用
于读取汽车的里程表:
class Car:
def __init__(self, make, model, year):
""" 初始化描述汽车的属性。 """
self.make = make
self.model = model
self.year = year
❶ self.odometer_reading = 0
def get_descriptive_name(self):
--snip--
❷ def read_odometer(self):
""" 打印一条指出汽车里程的消息。 """
print(f"This car has {self.odometer_reading} miles
on it.")
<!-- p274 -->
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()
现在,当Python调用方法__init__() 来创建新实例时,将像前
一个示例一样以属性的方式存储制造商、型号和生产年份。接下
来,Python将创建一个名为odometer_reading 的属性,并将
其初始值设置为0(见 ❶ )。在 ❷ 处,定义一个名为
read_odometer() 的方法,让你能够轻松地获悉汽车的里程
。
此处里程的单位为英里(mile),1英里 ≈ 1.6千米。——编者注
一开始汽车的里程为0:
2019 Audi A4
This car has 0 miles on it.
出售时里程表读数为0的汽车不多,因此需要一种方式来修改该属
性的值。
9.2.3 修改属性的值
我们能以三种方式修改属性的值:直接通过实例进行修改,通过
方法进行设置,以及通过方法进行递增(增加特定的值)。下面
依次介绍这些方式。
a. 直 接 修 改 属 性 的 值
要修改属性的值,最简单的方式是通过实例直接访问它。下
面的代码直接将里程表读数设置为23:
class Car:
--snip--
<!-- p275 -->
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
❶ my_new_car.odometer_reading = 23
my_new_car.read_odometer()
在 ❶ 处,使用句点表示法直接访问并设置汽车的属性
odometer_reading 。这行代码让Python在实例
my_new_car 中找到属性odometer_reading ,并将其
值设置为23:
2019 Audi A4
This car has 23 miles on it.
有时候需要像这样直接访问属性,但其他时候需要编写对属
性进行更新的方法。
b. 通 过 方 法 修 改 属 性 的 值
如果有方法能替你更新属性,将大有裨益。这样就无须直接
访问属性,而可将值传递给方法,由它在内部进行更新。
下面的示例演示了一个名为update_odometer() 的方
法:
class Car:
--snip--
❶ def update_odometer(self, mileage):
""" 将里程表读数设置为指定的值。 """
self.odometer_reading = mileage
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
❷ my_new_car.update_odometer(23)
my_new_car.read_odometer()
<!-- p276 -->
对Car 类所做的唯一修改是在 ❶ 处添加了方法
update_odometer() 。这个方法接受一个里程值,并将
其赋给self.odometer_reading 。在 ❷ 处,调用
update_odometer() ,并向它提供了实参23(该实参对
应于方法定义中的形参mileage )。它将里程表读数设置
为23,而方法read_odometer() 打印该读数:
2019 Audi A4
This car has 23 miles on it.
可对方法update_odometer() 进行扩展,使其在修改里
程表读数时做些额外的工作。下面来添加一些逻辑,禁止任
何人将里程表读数往回调:
class Car:
--snip--
def update_odometer(self, mileage):
"""
将里程表读数设置为指定的值。
禁止将里程表读数往回调。
"""
❶ if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
❷ print("You can't roll back an odometer!")
现在,update_odometer() 在修改属性前检查指定的读
数是否合理。如果新指定的里程(mileage )大于或等于
原来的里程(self.odometer_reading ),就将里程表
读数改为新指定的里程(见 ❶ );否则发出警告,指出不能
将里程表往回调(见 ❷ )。
<!-- p277 -->
c. 通 过 方 法 对 属 性 的 值 进 行 递 增
有时候需要将属性值递增特定的量,而不是将其设置为全新
的值。假设我们购买了一辆二手车,且从购买到登记期间增
加了100英里的里程。下面的方法让我们能够传递这个增量,
并相应地增大里程表读数:
class Car:
--snip--
def update_odometer(self, mileage):
--snip--
❶ def increment_odometer(self, miles):
""" 将里程表读数增加指定的量。 """
self.odometer_reading += miles
❷ my_used_car = Car('subaru', 'outback', 2015)
print(my_used_car.get_descriptive_name())
❸ my_used_car.update_odometer(23_500)
my_used_car.read_odometer()
❹ my_used_car.increment_odometer(100)
my_used_car.read_odometer()
在 ❶ 处,新增的方法increment_odometer() 接受一个单
位为英里的数,并将其加入self.odometer_reading
中。在 ❷ 处,创建一辆二手车my_used_car 。在 ❸ 处,调
用方法update_odometer() 并传入23_500 ,将这辆二
手车的里程表读数设置为23 500。在 ❹ 处,调用
increment_odometer() 并传入100 ,以增加从购买到
登记期间行驶的100英里:
2015 Subaru Outback
This car has 23500 miles on it.
This car has 23600 miles on it.
<!-- p278 -->
你可以轻松地修改这个方法,以禁止增量为负值,从而防止
有人利用它来回调里程表。
注 意 你可以使用类似于上面的方法来控制用户修改
属性值(如里程表读数)的方式,但能够访问程序的人
都可以通过直接访问属性来将里程表修改为任何值。要
确保安全,除了进行类似于前面的基本检查外,还需特
别注意细节。
动 手 试 一 试
练 习 9 - 4 : 就 餐 人 数 在为完成练习9-1而编写的程序中,
添加一个名为number_served 的属性,并将其默认值设置
为0。根据这个类创建一个名为restaurant 的实例。打印
有多少人在这家餐馆就餐过,然后修改这个值并再次打印
它。
添加一个名为set_number_served() 的方法,让你能够
设置就餐人数。调用这个方法并向它传递一个值,然后再次
打印这个值。
添加一个名为increment_number_served() 的方法,让
你能够将就餐人数递增。调用这个方法并向它传递一个这样
的值:你认为这家餐馆每天可能接待的就餐人数。
练 习 9 - 5 : 尝 试 登 录 次 数 在为完成练习9-3而编写的User
类中,添加一个名为login_attempts 的属性。编写一个
名为increment_login_attempts() 的方法,将属性
login_attempts 的值加1。再编写一个名为
reset_login_attempts() 的方法,将属性
login_attempts 的值重置为0。
根据User 类创建一个实例,再调用方法
increment_login_attempts() 多次。打印属性
login_attempts 的值,确认它被正确地递增。然后,调
<!-- p279 -->
用方法reset_login_attempts() ,并再次打印属性
login_attempts 的值,确认它被重置为0。
<!-- p280 -->
9.3 继承
编写类时,并非总是要从空白开始。如果要编写的类是另一个现
成类的特殊版本,可使用继 承 。一个类继 承 另一个类时,将自
动获得另一个类的所有属性和方法。原有的类称为父 类 ,而新
类称为子 类 。子类继承了父类的所有属性和方法,同时还可以
定义自己的属性和方法。
9.3.1 子类的方法 __init__()
在既有类的基础上编写新类时,通常要调用父类的方法
__init__() 。这将初始化在父类__init__() 方法中定义的
所有属性,从而让子类包含这些属性。
例如,下面来模拟电动汽车。电动汽车是一种特殊的汽车,因此
可在前面创建的Car 类的基础上创建新类ElectricCar 。这样
就只需为电动汽车特有的属性和行为编写代码。
下面来创建ElectricCar 类的一个简单版本,它具备Car 类的
所有功能:
electric_car.py
❶ class Car:
""" 一次模拟汽车的简单尝试。 """
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()
def read_odometer(self):
print(f"This car has {self.odometer_reading} miles
on it.")
<!-- p281 -->
def update_odometer(self, mileage):
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
self.odometer_reading += miles
❷ class ElectricCar(Car):
""" 电动汽车的独特之处。 """
❸ def __init__(self, make, model, year):
""" 初始化父类的属性。 """
❹ super().__init__(make, model, year)
❺ my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
首先是Car 类的代码(见 ❶ )。创建子类时,父类必须包含在当
前文件中,且位于子类前面。在 ❷ 处,定义了子类ElectricCar
。定义子类时,必须在圆括号内指定父类的名称。方法
__init__() 接受创建Car 实例所需的信息(见 ❸ )。
❹ 处的super() 是一个特殊函数,让你能够调用父类的方法。这
行代码让Python调用Car 类的方法__init__() ,让
ElectricCar 实例包含这个方法中定义的所有属性。父类也称
为超 类 (superclass),名称super 由此而来。
为测试继承能够正确地发挥作用,我们尝试创建一辆电动汽车,
但提供的信息与创建普通汽车时相同。在 ❺ 处,创建
ElectricCar 类的一个实例,并将其赋给变量my_tesla 。这
行代码调用ElectricCar 类中定义的方法__init__() ,后
者让Python调用父类Car 中定义的方法__init__() 。我们提
供了实参'tesla' 、'model s' 和2019 。
除方法__init___() 外,电动汽车没有其他特有的属性和方
法。当前,我们只想确认电动汽车具备普通汽车的行为:
<!-- p282 -->
2019 Tesla Model S
ElectricCar 实例的行为与Car 实例一样,现在可以开始定义
电动汽车特有的属性和方法了。
9.3.2 给子类定义属性和方法
让一个类继承另一个类后,就可以添加区分子类和父类所需的新
属性和新方法了。
下面来添加一个电动汽车特有的属性(电瓶),以及一个描述该
属性的方法。我们将存储电瓶容量,并编写一个打印电瓶描述的
方法:
class Car:
--snip--
class ElectricCar(Car):
""" 电动汽车的独特之处。 """
def __init__(self, make, model, year):
"""
初始化父类的属性。
再初始化电动汽车特有的属性。
"""
super().__init__(make, model, year)
❶ self.battery_size = 75
❷ def describe_battery(self):
""" 打印一条描述电瓶容量的消息。 """
print(f"This car has a {self.battery_size}-kWh
battery.")
my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
my_tesla.describe_battery()
在 ❶ 处,添加了新属性self.battery_size ,并设置其初始值
(75 )。根据ElectricCar 类创建的所有实例都将包含该属
<!-- p283 -->
性,但所有Car 实例都不包含它。在 ❷ 处,还添加了一个名为
describe_battery() 的方法,打印有关电瓶的信息。调用这
个方法时,将看到一条电动汽车特有的描述:
2019 Tesla Model S
This car has a 75-kWh battery.
对于ElectricCar 类的特殊程度没有任何限制。模拟电动汽车
时,可根据所需的准确程度添加任意数量的属性和方法。如果一
个属性或方法是任何汽车都有的,而不是电动汽车特有的,就应
将其加入到Car 类而非ElectricCar 类中。这样,使用Car
类的人将获得相应的功能,而ElectricCar 类只包含处理电动
汽车特有属性和行为的代码。
9.3.3 重写父类的方法
对于父类的方法,只要它不符合子类模拟的实物的行为,都可以
进行重写。为此,可在子类中定义一个与要重写的父类方法同名
的方法。这样,Python将不会考虑这个父类方法,而只关注你在
子类中定义的相应方法。
假设Car 类有一个名为fill_gas_tank() 的方法,它对全电
动汽车来说毫无意义,因此你可能想重写它。下面演示了一种重
写方式:
class ElectricCar(Car):
--snip--
def fill_gas_tank(self):
""" 电动汽车没有油箱。 """
print("This car doesn't need a gas tank!")
现在,如果有人对电动汽车调用方法fill_gas_tank() ,
Python将忽略Car 类中的方法fill_gas_tank() ,转而运行
<!-- p284 -->
上述代码。使用继承时,可让子类保留从父类那里继承而来的精
华,并剔除不需要的糟粕。
9.3.4 将实例用作属性
使用代码模拟实物时,你可能会发现自己给类添加的细节越来越
多:属性和方法清单以及文件都越来越长。在这种情况下,可能
需要将类的一部分提取出来,作为一个独立的类。可以将大型类
拆分成多个协同工作的小类。
例如,不断给ElectricCar 类添加细节时,我们可能发现其中
包含很多专门针对汽车电瓶的属性和方法。在这种情况下,可将
这些属性和方法提取出来,放到一个名为Battery 的类中,并
将一个Battery 实例作为ElectricCar 类的属性:
class Car:
--snip--
❶ class Battery:
""" 一次模拟电动汽车电瓶的简单尝试。 """
❷ def __init__(self, battery_size=75):
""" 初始化电瓶的属性。 """
self.battery_size = battery_size
❸ def describe_battery(self):
""" 打印一条描述电瓶容量的消息。 """
print(f"This car has a {self.battery_size}-kWh
battery.")
class ElectricCar(Car):
""" 电动汽车的独特之处。 """
def __init__(self, make, model, year):
"""
初始化父类的属性。
再初始化电动汽车特有的属性。
"""
super().__init__(make, model, year)
❹ self.battery = Battery()
my_tesla = ElectricCar('tesla', 'model s', 2019)
<!-- p285 -->
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
❶ 处定义一个名为Battery 的新类,它没有继承任何类。 ❷ 处的
方法__init__() 除self 外,还有另一个形参
battery_size 。这个形参是可选的:如果没有给它提供值,
电瓶容量将被设置为75。方法describe_battery() 也移到了
这个类中(见 ❸ )。
在ElectricCar 类中,添加了一个名为self.battery 的属
性(见 ❹ )。这行代码让Python创建一个新的Battery 实例(因
为没有指定容量,所以为默认值75),并将该实例赋给属性
self.battery 。每当方法__init__() 被调用时,都将执行
该操作,因此现在每个ElectricCar 实例都包含一个自动创建
的Battery 实例。
我们创建一辆电动汽车,并将其赋给变量my_tesla 。描述电瓶
时,需要使用电动汽车的属性battery :
my_tesla.battery.describe_battery()
这行代码让Python在实例my_tesla 中查找属性battery ,并
对存储在该属性中的Battery 实例调用方法
describe_battery() 。
输出与你在前面看到的相同:
2019 Tesla Model S
This car has a 75-kWh battery.
这看似做了很多额外的工作,但是现在想多详细地描述电瓶都可
以,且不会导致ElectricCar 类混乱不堪。下面再给Battery
类添加一个方法,它根据电瓶容量报告汽车的续航里程:
<!-- p286 -->
class Car:
--snip--
class Battery:
--snip--
❶ def get_range(self):
""" 打印一条消息,指出电瓶的续航里程。 """
if self.battery_size == 75:
range = 260
elif self.battery_size == 100:
range = 315
print(f"This car can go about {range} miles on a
full charge.")
class ElectricCar(Car):
--snip--
my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
❷ my_tesla.battery.get_range()
❶ 处新增的方法get_range() 做了一些简单的分析:如果电瓶
的容量为75 kWh,就将续航里程设置为260英里;如果容量为100
kWh,就将续航里程设置为315英里,然后报告这个值。为使用这
个方法,也需要通过汽车的属性battery 来调用(见 ❷ )。
输出指出了汽车的续航里程(这取决于电瓶的容量):
2019 Tesla Model S
This car has a 75-kWh battery.
This car can go about 260 miles on a full charge.
9.3.5 模拟实物
模拟较复杂的物件(如电动汽车)时,需要解决一些有趣的问
题。续航里程是电瓶的属性还是汽车的属性呢?如果只描述一辆
<!-- p287 -->
汽车,将方法get_range() 放在Battery 类中也许是合适
的,但如果要描述一家汽车制造商的整个产品线,也许应该将方
法get_range() 移到ElectricCar 类中。在这种情况下,
get_range() 依然根据电瓶容量来确定续航里程,但报告的是
一款汽车的续航里程。也可以这样做:仍将方法get_range()
留在Battery 类中,但向它传递一个参数,如car_model 。
在这种情况下,方法get_range() 将根据电瓶容量和汽车型号
报告续航里程。
这让你进入了程序员的另一个境界:解决上述问题时,从较高的
逻辑层面(而不是语法层面)考虑;考虑的不是Python,而是如
何使用代码来表示实物。达到这种境界后,你会经常发现,对现
实世界的建模方法没有对错之分。有些方法的效率更高,但要找
出效率最高的表示法,需要经过一定的实践。只要代码像你希望
的那样运行,就说明你做得很好!即便发现自己不得不多次尝试
使用不同的方法来重写类,也不必气馁。要编写出高效、准确的
代码,都得经过这样的过程。
动 手 试 一 试
练 习 9 - 6 : 冰 激 凌 小 店 冰激凌小店是一种特殊的餐馆。编
写一个名为IceCreamStand 的类,让它继承为完成练习9-
1或练习9-4而编写的Restaurant 类。这两个版本的
Restaurant 类都可以,挑选你更喜欢的那个即可。添加一
个名为flavors 的属性,用于存储一个由各种口味的冰激
凌组成的列表。编写一个显示这些冰激凌的方法。创建一个
IceCreamStand 实例,并调用这个方法。
练 习 9 - 7 : 管 理 员 管理员是一种特殊的用户。编写一个名
为Admin 的类,让它继承为完成练习9-3或练习9-5而编写的
User 类。添加一个名为privileges 的属性,用于存储一
个由字符串(如"can add post" 、"can delete
post" 、"can ban user" 等)组成的列表。编写一个名
为show_privileges() 的方法,显示管理员的权限。创
建一个Admin 实例,并调用这个方法。
<!-- p288 -->
练 习 9 - 8 : 权 限 编写一个名为Privileges 的类,它只
有一个属性privileges ,其中存储了练习9-7所述的字符
串列表。将方法show_privileges() 移到这个类中。在
Admin 类中,将一个Privileges 实例用作其属性。创建
一个Admin 实例,并使用方法show_privileges() 来显
示其权限。
练 习 9 - 9 : 电 瓶 升 级 在本节最后一个electric_car.py版
本中,给Battery 类添加一个名为upgrade_battery()
的方法。该方法检查电瓶容量,如果不是100,就将其设置为
100。创建一辆电瓶容量为默认值的电动汽车,调用方法
get_range() ,然后对电瓶进行升级,并再次调用
get_range() 。你将看到这辆汽车的续航里程增加了。
<!-- p289 -->
9.4 导入类
随着不断给类添加功能,文件可能变得很长,即便妥善地使用了
继承亦如此。为遵循Python的总体理念,应让文件尽可能整洁。
Python在这方面提供了帮助,允许将类存储在模块中,然后在主
程序中导入所需的模块。
9.4.1 导入单个类
下面来创建一个只包含Car 类的模块。这让我们面临一个微妙的
命名问题:在本章中已经有一个名为car.py的文件,但这个模块
也应命名为car.py,因为它包含表示汽车的代码。我们将这样解
决这个命名问题:将Car 类存储在一个名为car.py的模块中,该
模块将覆盖前面使用的文件car.py。从现在开始,使用该模块的
程序都必须使用更具体的文件名,如my_car.py。下面是模块
car.py,其中只包含Car 类的代码:
car.py
❶ """ 一个可用于表示汽车的类。 """
class Car:
""" 一次模拟汽车的简单尝试。 """
def __init__(self, make, model, year):
""" 初始化描述汽车的属性。 """
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
""" 返回整洁的描述性名称。 """
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()
def read_odometer(self):
""" 打印一条消息,指出汽车的里程。 """
print(f"This car has {self.odometer_reading} miles
on it.")
<!-- p290 -->
def update_odometer(self, mileage):
"""
将里程表读数设置为指定的值。
拒绝将里程表往回调。
"""
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
""" 将里程表读数增加指定的量。 """
self.odometer_reading += miles
❶ 处包含一个模块级文档字符串,对该模块的内容做了简要的描
述。你应为自己创建的每个模块编写文档字符串。
下面来创建另一个文件my_car.py,在其中导入Car 类并创建其
实例:
my_car.py
❶ from car import Car
my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
❶ 处的import 语句让Python打开模块car 并导入其中的Car
类。这样,我们就可以使用Car 类,就像它是在这个文件中定义
的一样。输出与我们在前面看到的一样:
2019 Audi A4
This car has 23 miles on it.
<!-- p291 -->
导入类是一种有效的编程方式。如果这个程序包含整个Class
类,它该有多长啊!通过将这个类移到一个模块中并导入该模
块,依然可以使用其所有功能,但主程序文件变得整洁而易于阅
读了。这还让你能够将大部分逻辑存储在独立的文件中。确定类
像你希望的那样工作后,就可以不管这些文件,而专注于主程序
的高级逻辑了。
9.4.2 在一个模块中存储多个类
虽然同一个模块中的类之间应存在某种相关性,但可根据需要在
一个模块中存储任意数量的类。Battery 类和ElectricCar
类都可帮助模拟汽车,下面将它们都加入模块car.py中:
car.py
""" 一组用于表示燃油汽车和电动汽车的类。 """
class Car:
--snip--
class Battery:
""" 一次模拟电动汽车电瓶的简单尝试。 """
def __init__(self, battery_size=75):
""" 初始化电瓶的属性。 """
self.battery_size = battery_size
def describe_battery(self):
""" 打印一条描述电瓶容量的消息。 """
print(f"This car has a {self.battery_size}-kWh
battery.")
def get_range(self):
""" 打印一条描述电瓶续航里程的消息。 """
if self.battery_size == 75:
range = 260
elif self.battery_size == 100:
range = 315
print(f"This car can go about {range} miles on a full
charge.")
class ElectricCar(Car):
""" 模拟电动汽车的独特之处。 """
<!-- p292 -->
def __init__(self, make, model, year):
"""
初始化父类的属性。
再初始化电动汽车特有的属性。
"""
super().__init__(make, model, year)
self.battery = Battery()
现在,可以新建一个名为my_electric_car.py的文件,导入
ElectricCar 类,并创建一辆电动汽车了:
my_electric_car.py
from car import ElectricCar
my_tesla = ElectricCar('tesla', 'model s', 2019)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
my_tesla.battery.get_range()
输出与我们在前面看到的相同,但大部分逻辑隐藏在一个模块
中:
2019 Tesla Model S
This car has a 75-kWh battery.
This car can go about 260 miles on a full charge.
9.4.3 从一个模块中导入多个类
可根据需要在程序文件中导入任意数量的类。如果要在同一个程
序中创建普通汽车和电动汽车,就需要将Car 类和
ElectricCar 类都导入:
my_cars.py
<!-- p293 -->
❶ from car import Car, ElectricCar
❷ my_beetle = Car('volkswagen', 'beetle', 2019)
print(my_beetle.get_descriptive_name())
❸ my_tesla = ElectricCar('tesla', 'roadster', 2019)
print(my_tesla.get_descriptive_name())
在 ❶ 处从一个模块中导入多个类时,用逗号分隔了各个类。导入
必要的类后,就可根据需要创建每个类的任意数量实例。
在本例中,在 ❷ 处创建了一辆大众甲壳虫普通汽车,并在 ❸ 处创
建了一辆特斯拉Roadster电动汽车:
2019 Volkswagen Beetle
2019 Tesla Roadster
9.4.4 导入整个模块
还可以导入整个模块,再使用句点表示法访问需要的类。这种导
入方式很简单,代码也易于阅读。因为创建类实例的代码都包含
模块名,所以不会与当前文件使用的任何名称发生冲突。
下面的代码导入整个car 模块,并创建一辆普通汽车和一辆电动
汽车:
my_cars.py
❶ import car
❷ my_beetle = car.Car('volkswagen', 'beetle', 2019)
print(my_beetle.get_descriptive_name())
❸ my_tesla = car.ElectricCar('tesla', 'roadster', 2019)
print(my_tesla.get_descriptive_name())
<!-- p294 -->
在 ❶ 处,导入了整个car 模块。接下来,使用语法
module_name.ClassName 访问需要的类。像前面一样,在 ❷
处创建一辆大众甲壳虫汽车,并在 ❸ 处创建一辆特斯拉Roadster
汽车。
9.4.5 导入模块中的所有类
要导入模块中的每个类,可使用下面的语法:
from module_name import *
不推荐使用这种导入方式,原因有二。第一,如果只看文件开头
的import 语句,就能清楚地知道程序使用了哪些类,将大有裨
益。然而这种导入方式没有明确地指出使用了模块中的哪些类。
第二,这种方式还可能引发名称方面的迷惑。如果不小心导入了
一个与程序文件中其他东西同名的类,将引发难以诊断的错误。
这里之所以介绍这种导入方式,是因为虽然不推荐使用,但你可
能在别人编写的代码中见到它。
需要从一个模块中导入很多类时,最好导入整个模块,并使用
module_name.ClassName 语法来访问类。这样做时,虽然文
件开头并没有列出用到的所有类,但你清楚地知道在程序的哪些
地方使用了导入的模块。这也避免了导入模块中的每个类可能引
发的名称冲突。
9 . 4 . 6 在 一 个 模 块 中 导 入 另 一 个 模 块
有时候,需要将类分散到多个模块中,以免模块太大或在同一个
模块中存储不相关的类。将类存储在多个模块中时,你可能会发
现一个模块中的类依赖于另一个模块中的类。在这种情况下,可
在前一个模块中导入必要的类。
下面将Car 类存储在一个模块中,并将ElectricCar 类和
Battery 类存储在另一个模块中。将第二个模块命名为
electric_car.py(这将覆盖前面创建的文件
<!-- p295 -->
electric_car.py),并将Battery 类和ElectricCar 类复制
到这个模块中:
electric_car.py
""" 一组可用于表示电动汽车的类。 """
❶ from car import Car
class Battery:
--snip--
class ElectricCar(Car):
--snip--
ElectricCar 类需要访问其父类Car ,因此在 ❶ 处直接将Car
类导入该模块中。如果忘记了这行代码,Python将在我们试图创
建ElectricCar 实例时引发错误。还需要更新模块car ,使其
只包含Car 类:
car.py
""" 一个可用于表示汽车的类。 """
class Car:
--snip--
现在可以分别从每个模块中导入类,以根据需要创建任何类型的
汽车了:
my_cars.py
❶ from car import Car
from electric_car import ElectricCar
my_beetle = Car('volkswagen', 'beetle', 2019)
print(my_beetle.get_descriptive_name())
<!-- p296 -->
my_tesla = ElectricCar('tesla', 'roadster', 2019)
print(my_tesla.get_descriptive_name())
在 ❶ 处,从模块car 中导入了Car 类,并从模块
electric_car 中导入ElectricCar 类。接下来,创建了一
辆普通汽车和一辆电动汽车。这两种汽车都被正确地创建出来
了:
2019 Volkswagen Beetle
2019 Tesla Roadster
9.4.7 使用别名
第8章说过,使用模块来组织项目代码时,别名大有裨益。导入类
时,也可为其指定别名。
例如,要在程序中创建大量电动汽车实例,需要反复输入
ElectricCar ,非常烦琐。为避免这种烦恼,可在import 语
句中给ElectricCar 指定一个别名:
from electric_car import ElectricCar as EC
现在每当需要创建电动汽车实例时,都可使用这个别名:
my_tesla = EC('tesla', 'roadster', 2019)
9.4.8 自定义工作流程
如你所见,在组织大型项目的代码方面,Python提供了很多选
项。熟悉所有这些选项很重要,这样你才能确定哪种项目组织方
式是最佳的,并能理解别人开发的项目。
<!-- p297 -->
一开始应让代码结构尽可能简单。先尽可能在一个文件中完成所
有的工作,确定一切都能正确运行后,再将类移到独立的模块
中。如果你喜欢模块和文件的交互方式,可在项目开始时就尝试
将类存储到模块中。先找出让你能够编写出可行代码的方式,再
尝试改进代码。
动 手 试 一 试
练 习 9 - 1 0 : 导 入 Restaurant 类 将最新的Restaurant
类存储在一个模块中。在另一个文件中,导入Restaurant
类,创建一个Restaurant 实例并调用Restaurant 的一
个方法,以确认import 语句正确无误。
练 习 9 - 1 1 : 导 入 Admin 类 以为完成练习9-8而做的工作
为基础。将User 类、Privileges 类和Admin 类存储在
一个模块中,再创建一个文件,在其中创建一个Admin 实例
并对其调用方法show_privileges() ,以确认一切都能
正确运行。
练 习 9 - 1 2 : 多 个 模 块 将User 类存储在一个模块中,并
将Privileges 类和Admin 类存储在另一个模块中。再创
建一个文件,在其中创建一个Admin 实例并对其调用方法
show_privileges() ,以确认一切依然能够正确运行。
<!-- p298 -->
9.5 P y t h o n 标准库
P y t h o n 标 准 库 是一组模块,我们安装的Python都包含它。你现
在对函数和类的工作原理已有大致的了解,可以开始使用其他程
序员编写好的模块了。可以使用标准库中的任何函数和类,只需
在程序开头包含一条简单的import 语句即可。下面来看看模块
random ,它在你模拟很多现实情况时很有用。
在这个模块中,一个有趣的函数是randint() 。它将两个整数
作为参数,并随机返回一个位于这两个整数之间(含)的整数。
下面演示了如何生成一个位于1和6之间的随机整数:
>> from random import randint
>> randint(1, 6)
在模块random 中,另一个有用的函数是choice() 。它将一个
列表或元组作为参数,并随机返回其中的一个元素:
>> from random import choice
>> players = ['charles', 'martina', 'michael', 'florence',
'eli']
>> first_up = choice(players)
>> first_up
'florence'
创建与安全相关的应用程序时,请不要使用模块random ,但该
模块可以很好地用于创建众多有趣的项目。
注 意 还可以从其他地方下载外部模块。第二部分的每个
项目都需要使用外部模块,届时你将看到很多此类示例。
<!-- p299 -->
动 手 试 一 试
练 习 9 - 1 3 : 骰 子 创建一个Die 类,它包含一个名为
sides 的属性,该属性的默认值为6。编写一个名为
roll_die() 的方法,它打印位于1和骰子面数之间的随机
数。创建一个6面的骰子再掷10次。
创建一个10面的骰子和一个20面的骰子,再分别掷10次。
练 习 9 - 1 4 : 彩 票 创建一个列表或元组,其中包含10个数
和5个字母。从这个列表或元组中随机选择4个数或字母,并
打印一条消息,指出只要彩票上是这4个数或字母,就中大奖
了。
练 习 9 - 1 5 : 彩 票 分 析 可以使用一个循环来明白前述彩票
大奖有多难中奖。为此,创建一个名为my_ticket 的列表
或元组,再编写一个循环,不断地随机选择数或字母,直到
中大奖为止。请打印一条消息,报告执行循环多少次才中了
大奖。
练 习 9 - 1 6 : P y t h o n M o d u l e o f t h e W e e k 要了解Python
标准库,一个很不错的资源是网站Python Module of the
Week。请访问该网站并查看其中的目录,找一个你感兴趣的
模块进行探索。从模块random 开始可能是个不错的选择。
<!-- p300 -->
9.6 类编码风格
你必须熟悉有些与类相关的编码风格问题,在编写的程序较复杂
时尤其如此。
类名应采用驼 峰 命 名 法 ,即将类名中的每个单词的首字母都大
写,而不使用下划线。实例名和模块名都采用小写格式,并在单
词之间加上下划线。
对于每个类,都应紧跟在类定义后面包含一个文档字符串。这种
文档字符串简要地描述类的功能,并遵循编写函数的文档字符串
时采用的格式约定。每个模块也都应包含一个文档字符串,对其
中的类可用于做什么进行描述。
可使用空行来组织代码,但不要滥用。在类中,可使用一个空行
来分隔方法;而在模块中,可使用两个空行来分隔类。
需要同时导入标准库中的模块和你编写的模块时,先编写导入标
准库模块的import 语句,再添加一个空行,然后编写导入你自
己编写的模块的import 语句。在包含多条import 语句的程序
中,这种做法让人更容易明白程序使用的各个模块都来自何处。
<!-- p301 -->
9.7 小结
在本章中,你学习了:如何编写类;如何使用属性在类中存储信
息,以及如何编写方法,以让类具备所需的行为;如何编写方法
__init__() ,以便根据类创建包含所需属性的实例。你见识了
如何修改实例的属性,包括直接修改以及通过方法进行修改。你
还了解了使用继承可简化相关类的创建工作,以及将一个类的实
例用作另一个类的属性可让类更简洁。
你了解到,通过将类存储在模块中,并在需要使用这些类的文件
中导入它们,可让项目组织有序。你学习了Python标准库,并见
识了一个使用模块random 的示例。最后,你学习了编写类时应
遵循的Python约定。
在第10章中,你将学习如何使用文件,这让你能够保存你在程序
中所做的工作,以及你让用户做的工作。你还将学习异 常 ,这
是一种特殊的Python类,用于帮助你在发生错误时采取相应的措
施。
<!-- p302 -->
第 1 0 章 文 件 和 异 常
至此,你掌握了编写组织有序、易于使用的程
序所需的基本技能,该考虑让程序目标更明确、用途更大
了。在本章中,你将学习处理文件,让程序能够快速地分析
大量数据;你将学习错误处理,避免程序在面对意外情形时
崩溃;你将学习异 常 ,它们是Python创建的特殊对象,用
于管理程序运行时出现的错误;你还将学习模块json ,它
让你能够保存用户数据,以免在程序停止运行后丢失。
学习处理文件和保存数据可让你的程序使用起来更容易:用
户将能够选择输入什么样的数据,以及在什么时候输入;用
户使用你的程序做一些工作后,可将程序关闭,以后再接着
往下做。学习处理异常可帮助你应对文件不存在的情形,以
及处理其他可能导致程序崩溃的问题。这让你的程序在面对
错误的数据时更健壮,不管这些错误数据源自无意的错误,
还是源自破坏程序的恶意企图。你在本章学习的技能可提高
程序的适用性、可用性和稳定性。
<!-- p303 -->
1 0 . 1 从 文 件 中 读 取 数 据
文本文件可存储的数据量多得难以置信:天气数据、交通数据、
社会经济数据、文学作品等。每当需要分析或修改存储在文件中
的信息时,读取文件都很有用,对数据分析应用程序来说尤其如
此。例如,可以编写一个这样的程序:读取一个文本文件的内
容,重新设置这些数据的格式并将其写入文件,让浏览器能够显
示这些内容。
要使用文本文件中的信息,首先需要将信息读取到内存中。为
此,你可以一次性读取文件的全部内容,也可以以每次一行的方
式逐步读取。
1 0 . 1 . 1 读 取 整 个 文 件
要读取文件,需要一个包含几行文本的文件。下面首先创建一个
文件,它包含精确到小数点后30位的圆周率值,且在小数点后每
10位处换行:
pi_digits.txt
3.1415926535
8979323846
2643383279
要动手尝试后续示例,可在编辑器中输入这些数据行,再将文件
保存为pi_digits.txt,也可从本书主页
(ituring.cn/book/2784)下载该文件。请将该文件保存到本章
程序所在的目录。
下面的程序打开并读取这个文件,再将其内容显示到屏幕上:
file_reader.py
<!-- p304 -->
with open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents)
在这个程序中,第一行代码做了大量的工作。我们先来看看函数
open() 。要以任何方式使用文件,那怕仅仅是打印其内容,都
得先打 开 文件,才能访问它。函数open() 接受一个参数:要
打开的文件的名称。Python在当前执行的文件所在的目录中查找
指定的文件。在本例中,当前运行的是file_reader.py,因此
Python在file_reader.py所在的目录中查找pi_digits.txt。函数
open() 返回一个表示文件的对象。在这里,
open('pi_digits.txt') 返回一个表示文件pi_digits.txt
的对象,Python将该对象赋给file_object 供以后使用。
关键字with 在不再需要访问文件后将其关闭。在这个程序中,
注意到我们调用了open() ,但没有调用close() 。也可以调
用open() 和close() 来打开和关闭文件,但这样做时,如果
程序存在bug导致方法close() 未执行,文件将不会关闭。这看
似微不足道,但未妥善关闭文件可能导致数据丢失或受损。如果
在程序中过早调用close() ,你会发现需要使用文件时它已关
闭 (无法访问),这会导致更多的错误。并非在任何情况下都能
轻松确定关闭文件的恰当时机,但通过使用前面所示的结构,可
让Python去确定:你只管打开文件,并在需要时使用它,Python
自会在合适的时候自动将其关闭。
有了表示pi_digits.txt的文件对象后,使用方法read() (前
述程序的第二行)读取这个文件的全部内容,并将其作为一个长
长的字符串赋给变量contents 。这样,通过打印contents
的值,就可将这个文本文件的全部内容显示出来:
3.1415926535
8979323846
2643383279
<!-- p305 -->
相比于原始文件,该输出唯一不同的地方是末尾多了一个空行。
为何会多出这个空行呢?因为read() 到达文件末尾时返回一个
空字符串,而将这个空字符串显示出来时就是一个空行。要删除
多出来的空行,可在函数调用print() 中使用rstrip() :
with open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents.rstrip())
本书前面说过,Python方法rstrip() 删除字符串末尾的空白。
现在,输出与原始文件的内容完全相同:
3.1415926535
8979323846
2643383279
1 0 . 1 . 2 文 件 路 径
将类似于pi_digits.txt的简单文件名传递给函数open() 时,
Python将在当前执行的文件(即.py程序文件)所在的目录中查
找。
根据你组织文件的方式,有时可能要打开不在程序文件所属目录
中的文件。例如,你可能将程序文件存储在了文件夹python_work
中,而该文件夹中有一个名为text_files的文件夹用于存储程序
文件操作的文本文件。虽然文件夹text_files包含在文件夹
python_work中,但仅向open() 传递位于前者中的文件名称也
不可行,因为Python只在文件夹python_work中查找,而不会在其
子文件夹text_files中查找。要让Python打开不与程序文件位于
同一个目录中的文件,需要提供文件路径,让Python到系统的特
定位置去查找。
由于文件夹text_files位于文件夹python_work中,可以使用相对
文件路径来打开其中的文件。相对文件路径让Python到指定的位
<!-- p306 -->
置去查找,而该位置是相对于当前运行的程序所在目录的。例
如,可这样编写代码:
with open('text_files/filename.txt') as file_object:
这行代码让Python到文件夹python_work下的文件夹text_files中
去查找指定的.txt文件。
注 意 显示文件路径时,Windows系统使用反斜杠(\ )而
不是斜杠(/ ),但在代码中依然可以使用斜杠。
还可以将文件在计算机中的准确位置告诉Python,这样就不用关
心当前运行的程序存储在什么地方了。这称为绝 对 文 件 路 径 。
在相对路径行不通时,可使用绝对路径。例如,如果text_files
并不在文件夹python_work中,而在文件夹other_files中,则向
open() 传递路径'text_files/filename .txt' 行不通,
因为Python只在文件夹python_work中查找该位置。为明确指出希
望Python到哪里去查找,需要提供完整的路径。
绝对路径通常比相对路径长,因此将其赋给一个变量,再将该变
量传递给open() 会有所帮助:
file_path =
'/home/ehmatthes/other_files/text_files/filename.txt'
with open(file_path) as file_object:
通过使用绝对路径,可读取系统中任何地方的文件。就目前而
言,最简单的做法是,要么将数据文件存储在程序文件所在的目
录,要么将其存储在程序文件所在目录下的一个文件夹(如
text_files)中。
注 意 如果在文件路径中直接使用反斜杠,将引发错误,
因为反斜杠用于对字符串中的字符进行转义。例如,对于路
径"C:\path\to\file.txt" ,其中的\t 将被解读为制
<!-- p307 -->
表符。如果一定要使用反斜杠,可对路径中的每个反斜杠都
进行转义,如"C:\\path\\to\\file.txt" 。
1 0 . 1 . 3 逐 行 读 取
读取文件时,常常需要检查其中的每一行:可能要在文件中查找
特定的信息,或者要以某种方式修改文件中的文本。例如,你可
能要遍历一个包含天气数据的文件,并使用天气描述中包含sunny
字样的行。在新闻报道中,你可能会查找包含标签<headline>
的行,并按特定的格式设置它。
要以每次一行的方式检查文件,可对文件对象使用for 循环:
file_reader.py
❶ filename = 'pi_digits.txt'
❷ with open(filename) as file_object:
❸ for line in file_object:
print(line)
在 ❶ 处,将要读取的文件的名称赋给变量filename 。这是使用
文件时的一种常见做法。变量filename 表示的并非实际文件
——它只是一个让Python知道到哪里去查找文件的字符串,因此
可以轻松地将'pi_digits.txt' 替换为要使用的另一个文件的
名称。调用open() 后,将一个表示文件及其内容的对象赋给了
变量file_object (见 ❷ )。这里也使用了关键字with ,让
Python负责妥善地打开和关闭文件。为查看文件的内容,通过对
文件对象执行循环来遍历文件中的每一行(见 ❸ )。
打印每一行时,发现空白行更多了:
3.1415926535
8979323846
2643383279
<!-- p308 -->
为何会出现这些空白行呢?因为在这个文件中,每行的末尾都有
一个看不见的换行符,而函数调用print() 也会加上一个换行
符,因此每行末尾都有两个换行符:一个来自文件,另一个来自
函数调用print() 。要消除这些多余的空白行,可在函数调用
print() 中使用rstrip() :
filename = 'pi_digits.txt'
with open(filename) as file_object:
for line in file_object:
print(line.rstrip())
现在,输出又与文件内容完全相同了:
3.1415926535
8979323846
2643383279
1 0 . 1 . 4 创 建 一 个 包 含 文 件 各 行 内 容 的 列 表
使用关键字with 时,open() 返回的文件对象只在with 代码
块内可用。如果要在with 代码块外访问文件的内容,可在with
代码块内将文件的各行存储在一个列表中,并在with 代码块外
使用该列表:可以立即处理文件的各个部分,也可以推迟到程序
后面再处理。
下面的示例在with 代码块中将文件pi_digits.txt的各行存储在
一个列表中,再在with 代码块外打印:
filename = 'pi_digits.txt'
with open(filename) as file_object:
❶ lines = file_object.readlines()
<!-- p309 -->
❷ for line in lines:
print(line.rstrip())
❶ 处的方法readlines() 从文件中读取每一行,并将其存储在
一个列表中。接下来,该列表被赋给变量lines 。在with 代码
块外,依然可使用这个变量。在 ❷ 处,使用一个简单的for 循环
来打印lines 中的各行。因为列表lines 的每个元素都对应于
文件中的一行,所以输出与文件内容完全一致。
1 0 . 1 . 5 使 用 文 件 的 内 容
将文件读取到内存中后,就能以任何方式使用这些数据了。下面
以简单的方式使用圆周率的值。首先,创建一个字符串,它包含
文件中存储的所有数字,且没有任何空格:
pi_string.py
filename = 'pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
❶ pi_string = ''
❷ for line in lines:
pi_string += line.rstrip()
❸ print(pi_string)
print(len(pi_string))
像前一个示例一样,首先打开文件,并将其中所有的行都存储在
一个列表中。在 ❶ 处,创建了一个变量pi_string ,用于指向
圆周率的值。接下来,使用一个循环将各行加入pi_string ,
并删除每行末尾的换行符(见 ❷ )。在 ❸ 处,打印这个字符串及
其长度:
<!-- p310 -->
3.1415926535 8979323846 2643383279
变量pi_string 指向的字符串包含原来位于每行左边的空格,
为删除这些空格,可使用strip() 而非rstrip() :
--snip--
for line in lines:
pi_string += line.strip()
print(pi_string)
print(len(pi_string))
这样就获得了一个字符串,其中包含准确到30位小数的圆周率
值。这个字符串长32字符,因为它还包含整数部分的3和小数点:
3.141592653589793238462643383279
注 意 读取文本文件时,Python将其中的所有文本都解读
为字符串。如果读取的是数,并要将其作为数值使用,就必
须使用函数int() 将其转换为整数或使用函数float() 将
其转换为浮点数。
1 0 . 1 . 6 包 含 一 百 万 位 的 大 型 文 件
前面分析的都是一个只有三行的文本文件,但这些代码示例也可
处理大得多的文件。如果我们有一个文本文件,其中包含精确到
小数点后1 000 000位而不是30位的圆周率值,也可创建一个包含
所有这些数字的字符串。为此,无须对前面的程序做任何修改,
只要将这个文件传递给它即可。在这里,只打印到小数点后50
位,以免终端为显示全部1 000 000位而不断滚动:
pi_string.py
<!-- p311 -->
filename = 'pi_million_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
for line in lines:
pi_string += line.strip()
print(f"{pi_string[:52]}...")
print(len(pi_string))
输出表明,创建的字符串确实包含精确到小数点后1 000 000位的
圆周率值:
3.14159265358979323846264338327950288419716939937510...
1000002
对于可处理的数据量,Python没有任何限制。只要系统的内存足
够多,你想处理多少数据都可以。
注 意 要运行这个程序(以及后面的众多示例),需要从
http://ituring.cn/book/2784 下载相关的资源。
1 0 . 1 . 7 圆 周 率 值 中 包 含 你 的 生 日 吗
我一直想知道自己的生日是否包含在圆周率值中。下面来扩展刚
才编写的程序,以确定某个人的生日是否包含在圆周率值的前1
000 000位中。为此,可将生日表示为一个由数字组成的字符串,
再检查这个字符串是否包含在pi_string 中:
--snip--
for line in lines:
pi_string += line.strip()
❶ birthday = input("Enter your birthday, in the form mmddyy:
")
❷ if birthday in pi_string:
print("Your birthday appears in the first million digits
<!-- p312 -->
of pi!")
else:
print("Your birthday does not appear in the first
million digits of pi.")
在 ❶ 处,提示用户输入生日。在 ❷ 处,检查这个字符串是否包含
在pi_string 中。下面来运行一下这个程序:
Enter your birthdate, in the form mmddyy: 120372
Your birthday appears in the first million digits of pi!
我的生日确实出现在了圆周率值中!读取文件的内容后,能以你
能想到的任何方式对其进行分析。
动 手 试 一 试
练 习 1 0 - 1 : P y t h o n 学 习 笔 记 在文本编辑器中新建一个文
件,写几句话来总结一下你至此学到的Python知识,其中每
一行都以“In Python you can”打头。将这个文件命名为
learning_python.txt,并存储到为完成本章练习而编写的程
序所在的目录中。编写一个程序,它读取这个文件,并将你
所写的内容打印三次:第一次打印时读取整个文件;第二次
打印时遍历文件对象;第三次打印时将各行存储在一个列表
中,再在with 代码块外打印它们。
练 习 1 0 - 2 : C 语 言 学 习 笔 记 可使用方法replace() 将字
符串中的特定单词都替换为另一个单词。下面是一个简单的
示例,演示了如何将句子中的'dog' 替换为'cat' :
>> message = "I really like dogs."
>> message.replace('dog', 'cat')
'I really like cats.'
<!-- p313 -->
读取你刚创建的文件learning_python.txt中的每一行,将其
中的Python都替换为另一门语言的名称,比如C。将修改后的
各行都打印到屏幕上。
<!-- p314 -->
1 0 . 2 写 入 文 件
保存数据的最简单的方式之一是将其写入文件中。通过将输出写
入文件,即便关闭包含程序输出的终端窗口,这些输出也依然存
在:可以在程序结束运行后查看这些输出,可以与别人分享输出
文件,还可以编写程序来将这些输出读取到内存中并进行处理。
1 0 . 2 . 1 写 入 空 文 件
要将文本写入文件,你在调用open() 时需要提供另一个实参,
告诉Python你要写入打开的文件。为明白其中的工作原理,我们
来将一条简单的消息存储到文件中,而不是将其打印到屏幕上:
write_message.py
filename = 'programming.txt'
❶ with open(filename, 'w') as file_object:
❷ file_object.write("I love programming.")
在本例中,调用open() 时提供了两个实参(见 ❶ )。第一个实
参也是要打开的文件的名称。第二个实参('w' )告诉Python,
要以写 入 模 式 打开这个文件。打开文件时,可指定读 取 模 式
('r' )、写 入 模 式 ('w' )、附 加 模 式 ('a' )或读 写 模
式 ('r+' )。如果省略了模式实参,Python将以默认的只读模
式打开文件。
如果要写入的文件不存在,函数open() 将自动创建它。然而,
以写入模式('w' )打开文件时千万要小心,因为如果指定的文
件已经存在,Python将在返回文件对象前清空该文件的内容。
在 ❷ 处,使用文件对象的方法write() 将一个字符串写入文件。
这个程序没有终端输出,但如果打开文件programming.txt,将看
到其中包含如下一行内容:
<!-- p315 -->
programming.txt
I love programming.
相比于计算机中的其他文件,这个文件没有什么不同。你可以打
开它、在其中输入新文本、复制其内容、将内容粘贴到其中,等
等。
注 意 Python只能将字符串写入文本文件。要将数值数据
存储到文本文件中,必须先使用函数str() 将其转换为字符
串格式。
1 0 . 2 . 2 写 入 多 行
函数write() 不会在写入的文本末尾添加换行符,因此如果写
入多行时没有指定换行符,文件看起来可能不是你希望的那样:
filename = 'programming.txt'
with open(filename, 'w') as file_object:
file_object.write("I love programming.")
file_object.write("I love creating new games.")
如果你打开programming.txt,将发现两行内容挤在一起:
I love programming.I love creating new games.
要让每个字符串都单独占一行,需要在方法调用write() 中包
含换行符:
filename = 'programming.txt'
with open(filename, 'w') as file_object:
file_object.write("I love programming.\n")
file_object.write("I love creating new games.\n")
<!-- p316 -->
现在,输出出现在不同的行中:
I love programming.
I love creating new games.
像显示到终端的输出一样,还可以使用空格、制表符和空行来设
置这些输出的格式。
1 0 . 2 . 3 附 加 到 文 件
如果要给文件添加内容,而不是覆盖原有的内容,可以以附 加 模
式 打开文件。以附加模式打开文件时,Python不会在返回文件对
象前清空文件的内容,而是将写入文件的行添加到文件末尾。如
果指定的文件不存在,Python将为你创建一个空文件。
下面来修改write_message.py,在既有文件programming.txt中再
添加一些你酷爱编程的原因:
write_message.py
filename = 'programming.txt'
❶ with open(filename, 'a') as file_object:
❷ file_object.write("I also love finding meaning in large
datasets.\n")
file_object.write("I love creating apps that can run in
a browser.\n")
在 ❶ 处,打开文件时指定了实参'a' ,以便将内容附加到文件末
尾,而不是覆盖文件原来的内容。在 ❷ 处,又写入了两行,它们
被添加到文件programming.txt末尾:
programming.txt
<!-- p317 -->
I love programming.
I love creating new games.
I also love finding meaning in large datasets.
I love creating apps that can run in a browser.
最终的结果是,文件原来的内容还在,后面则是刚添加的内容。
动 手 试 一 试
练 习 1 0 - 3 : 访 客 编写一个程序,提示用户输入名字。用
户做出响应后,将其名字写入文件guest.txt中。
练 习 1 0 - 4 : 访 客 名 单 编写一个while 循环,提示用户输
入名字。用户输入名字后,在屏幕上打印一句问候语,并将
一条到访记录添加到文件guest_book.txt中。确保这个文件
中的每条记录都独占一行。
练 习 1 0 - 5 : 调 查 编写一个while 循环,询问用户为何喜
欢编程。每当用户输入一个原因后,都将其添加到一个存储
所有原因的文件中。
<!-- p318 -->
1 0 . 3 异 常
Python使用称为异 常 的特殊对象来管理程序执行期间发生的错
误。每当发生让Python不知所措的错误时,它都会创建一个异常
对象。如果你编写了处理该异常的代码,程序将继续运行;如果
未对异常进行处理,程序将停止并显示traceback,其中包含有关
异常的报告。
异常是使用try-except 代码块处理的。try-except 代码块
让Python执行指定的操作,同时告诉Python发生异常时怎么办。
使用try-except 代码块时,即便出现异常,程序也将继续运
行:显示你编写的友好的错误消息,而不是令用户迷惑的
traceback。
1 0 . 3 . 1 处 理 ZeroDivisionError 异 常
下面来看一种导致Python引发异常的简单错误。你可能知道,不
能用数除以0,但还是让Python这样做:
division_calculator.py
print(5/0)
显然,Python无法这样做,因此你将看到一个traceback:
Traceback (most recent call last):
File "division_calculator.py", line 1, in <module>
print(5/0)
❶ ZeroDivisionError: division by zero
在上述traceback中, ❶ 处指出的错误ZeroDivisionError 是
个异常对象。Python无法按你的要求做时,就会创建这种对象。
<!-- p319 -->
在这种情况下,Python将停止运行程序,并指出引发了哪种异
常,而我们可根据这些信息对程序进行修改。下面来告诉
Python,发生这种错误时怎么办。这样,如果再次发生此类错
误,我们就有备无患了。
1 0 . 3 . 2 使 用 try-except 代 码 块
当你认为可能会发生错误时,可编写一个try-except 代码块来
处理可能引发的异常。你让Python尝试运行一些代码,并告诉它
如果这些代码引发了指定的异常该怎么办。
处理ZeroDivisionError 异常的try-except 代码块类似于
下面这样:
try:
print(5/0)
except ZeroDivisionError:
print("You can't divide by zero!")
将导致错误的代码行print(5/0) 放在一个try 代码块中。如
果try 代码块中的代码运行起来没有问题,Python将跳过
except 代码块;如果try 代码块中的代码导致了错误,Python
将查找与之匹配的except 代码块并运行其中的代码。
在本例中,try 代码块中的代码引发了ZeroDivisionError
异常,因此Python查找指出了该怎么办的except 代码块,并运
行其中的代码。这样,用户看到的是一条友好的错误消息,而不
是traceback:
You can't divide by zero!
如果try-except 代码块后面还有其他代码,程序将接着运行,
因为已经告诉了Python如何处理这种错误。下面来看一个捕获错
误后程序继续运行的示例。
<!-- p320 -->
1 0 . 3 . 3 使 用 异 常 避 免 崩 溃
发生错误时,如果程序还有工作尚未完成,妥善地处理错误就尤
其重要。这种情况经常会出现在要求用户提供输入的程序中;如
果程序能够妥善地处理无效输入,就能再提示用户提供有效输
入,而不至于崩溃。
下面来创建一个只执行除法运算的简单计算器:
division_calculator.py
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")
while True:
❶ first_number = input("\nFirst number: ")
if first_number == 'q':
break
❷ second_number = input("Second number: ")
if second_number == 'q':
break
❸ answer = int(first_number) / int(second_number)
print(answer)
在 ❶ 处,程序提示用户输入一个数,并将其赋给变量
first_number 。如果用户输入的不是表示退出的q ,就再提
示用户输入一个数,并将其赋给变量second_number (见
❷ )。接下来,计算这两个数的商(见 ❸ )。该程序没有采取任
何处理错误的措施,因此在执行除数为0的除法运算时,它将崩
溃:
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
Traceback (most recent call last):
File "division_calculator.py", line 9, in <module>
answer = int(first_number) / int(second_number)
ZeroDivisionError: division by zero
<!-- p321 -->
程序崩溃可不好,但让用户看到traceback也不是个好主意。不懂
技术的用户会被搞糊涂,怀有恶意的用户还会通过traceback获悉
你不想他知道的信息。例如,他将知道你的程序文件的名称,还
将看到部分不能正确运行的代码。有时候,训练有素的攻击者可
根据这些信息判断出可对你的代码发起什么样的攻击。
1 0 . 3 . 4 else 代 码 块
通过将可能引发错误的代码放在try-except 代码块中,可提高
程序抵御错误的能力。错误是执行除法运算的代码行导致的,因
此需要将它放到try-except 代码块中。这个示例还包含一个
else 代码块。依赖try 代码块成功执行的代码都应放到else
代码块中:
--snip--
while True:
--snip--
if second_number == 'q':
break
❶ try:
answer = int(first_number) / int(second_number)
❷ except ZeroDivisionError:
print("You can't divide by 0!")
❸ else:
print(answer)
让Python尝试执行try 代码块中的除法运算(见 ❶ ),这个代码
块只包含可能导致错误的代码。依赖try 代码块成功执行的代码
都放在else 代码块中。在本例中,如果除法运算成功,就使用
else 代码块来打印结果(见 ❸ )。
except 代码块告诉Python,出现ZeroDivisionError 异常
时该如何办(见 ❷ )。如果try 代码块因除零错误而失败,就打
<!-- p322 -->
印一条友好的消息,告诉用户如何避免这种错误。程序继续运
行,用户根本看不到traceback:
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
You can't divide by 0!
First number: 5
Second number: 2
2.5
First number: q
try-except-else 代码块的工作原理大致如下。Python尝试执
行try 代码块中的代码,只有可能引发异常的代码才需要放在
try 语句中。有时候,有一些仅在try 代码块成功执行时才需
要运行的代码,这些代码应放在else 代码块中。except 代码
块告诉Python,如果尝试运行try 代码块中的代码时引发了指定
的异常该怎么办。
通过预测可能发生错误的代码,可编写健壮的程序。它们即便面
临无效数据或缺少资源,也能继续运行,从而抵御无意的用户错
误和恶意的攻击。
1 0 . 3 . 5 处 理 FileNotFoundError 异 常
使用文件时,一种常见的问题是找不到文件:查找的文件可能在
其他地方,文件名可能不正确,或者这个文件根本就不存在。对
于所有这些情形,都可使用try-except 代码块以直观的方式处
理。
我们来尝试读取一个不存在的文件。下面的程序尝试读取文件
alice.txt的内容,但该文件没有存储在alice.py所在的目录中:
alice.py
<!-- p323 -->
filename = 'alice.txt'
with open(filename, encoding='utf-8') as f:
contents = f.read()
相比于本章前面的文件打开方式,这里有两个不同之处。一是使
用变量f 来表示文件对象,这是一种常见的做法。二是给参数
encoding 指定了值,在系统的默认编码与要读取文件使用的编
码不一致时,必须这样做。
Python无法读取不存在的文件,因此它引发一个异常:
Traceback (most recent call last):
File "alice.py", line 3, in <module>
with open(filename, encoding='utf-8') as f:
FileNotFoundError: [Errno 2] No such file or directory:
'alice.txt'
上述traceback的最后一行报告了FileNotFoundError 异常,
这是Python找不到要打开的文件时创建的异常。在本例中,这个
错误是函数open() 导致的。因此,要处理这个错误,必须将
try 语句放在包含open() 的代码行之前:
filename = 'alice.txt'
try:
with open(filename, encoding='utf-8') as f:
contents = f.read()
except FileNotFoundError:
print(f"Sorry, the file {filename} does not exist.")
在本例中,try 代码块引发了FileNotFoundError 异常,因
此Python找到与该错误匹配的except 代码块,并运行其中的代
码。最终的结果是显示一条友好的错误消息,而不是traceback:
<!-- p324 -->
Sorry, the file alice.txt does not exist.
如果文件不存在,这个程序就什么都做不了,错误处理代码也意
义不大。下面来扩展这个示例,看看在你使用多个文件时,异常
处理可提供什么样的帮助。
1 0 . 3 . 6 分 析 文 本
你可以分析包含整本书的文本文件。很多经典文学作品都是简单
以文本文件的形式提供的,因为它们不受版权限制。本节使用的
文本来自古登堡计划,该计划提供了一系列不受版权限制的文学
作品。如果你要在编程项目中使用文学文本,这是一个很不错的
资源。
下面来提取童话《爱丽丝漫游奇境记》(Alice in Wonderland
)的文本,并尝试计算它包含多少个单词。我们将使用方法
split() ,它能根据一个字符串创建一个单词列表。下面是对
只包含童话名"Alice in Wonderland" 的字符串调用方法
split() 的结果:
>> title = "Alice in Wonderland"
>> title.split()
['Alice', 'in', 'Wonderland']
方法split() 以空格为分隔符将字符串分拆成多个部分,并将
这些部分都存储到一个列表中。结果是一个包含字符串中所有单
词的列表,虽然有些单词可能包含标点。为计算《爱丽丝漫游奇
境记》包含多少个单词,我们将对整篇小说调用split() ,再
计算得到的列表包含多少个元素,从而确定整篇童话大致包含多
少个单词:
filename = 'alice.txt'
try:
with open(filename, encoding='utf-8') as f:
<!-- p325 -->
contents = f.read()
except FileNotFoundError:
print(f"Sorry, the file {filename} does not exist.")
else:
计算该文件大致包含多少个单词。
❶ words = contents.split()
❷ num_words = len(words)
❸ print(f"The file {filename} has about {num_words}
words.")
我们将文件alice.txt移到了正确的目录中,让try 代码块能够
成功执行。在 ❶ 处,对变量contents (它现在是一个长长的字
符串,包含童话《爱丽丝漫游奇境记》的全部文本)调用方法
split() ,以生成一个列表,其中包含这部童话中的所有单
词。使用len() 来确定这个列表的长度时,就能知道原始字符串
大致包含多少个单词了(见 ❷ )。在 ❸ 处,打印一条消息,指出
文件包含多少个单词。这些代码都放在else 代码块中,因为仅
当try 代码块成功执行时才执行它们。输出指出了文件
alice.txt包含多少个单词:
The file alice.txt has about 29465 words.
这个数稍大一点,因为使用的文本文件包含出版商提供的额外信
息,但还是成功估算出了童话《爱丽丝漫游奇境记》的篇幅。
1 0 . 3 . 7 使 用 多 个 文 件
下面多分析几本书。这此之前,先将这个程序的大部分代码移到
一个名为count_words() 的函数中。这样,对多本书进行分析
时将更容易:
word_count.py
def count_words(filename):
❶ """ 计算一个文件大致包含多少个单词。 """
try:
<!-- p326 -->
with open(filename, encoding='utf-8') as f:
contents = f.read()
except FileNotFoundError:
print(f"Sorry, the file {filename} does not exist.")
else:
words = contents.split()
num_words = len(words)
print(f"The file {filename} has about {num_words}
words.")
filename = 'alice.txt'
count_words(filename)
这些代码大多与原来一样,只是移到了函数count_words()
中,并增加了缩进量。修改程序的同时更新注释是个不错的习
惯,因此我们将注释改成文档字符串,并稍微调整了一下措辞
(见 ❶ )。
现在可以编写一个简单的循环,计算要分析的任何文本包含多少
个单词了。为此,将要分析的文件的名称存储在一个列表中,然
后对列表中的每个文件调用count_words() 。我们将尝试计算
《爱丽丝漫游奇境记》《悉达多》(Siddhartha )、《白鲸》
(Moby Dick )和《小妇人》(Little Women )分别包含多少个
单词,它们都不受版权限制。我故意没有将siddhartha.txt放到
word_count.py所在的目录中,从而展示该程序在文件不存在时应
对得有多出色:
def count_words(filename):
--snip--
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt',
'little_women.txt']
for filename in filenames:
count_words(filename)
文件siddhartha.txt不存在,但这丝毫不影响该程序处理其他文
件:
<!-- p327 -->
The file alice.txt has about 29465 words.
Sorry, the file siddhartha.txt does not exist.
The file moby_dick.txt has about 215830 words.
The file little_women.txt has about 189079 words.
在本例中,使用try-except 代码块提供了两个重要的优点:避
免用户看到traceback,以及让程序继续分析能够找到的其他文
件。如果不捕获因找不到siddhartha.txt而引发的
FileNotFoundError 异常,用户将看到完整的traceback,而
程序将在尝试分析《悉达多》后停止运行。它根本不会分析《白
鲸》和《小妇人》。
1 0 . 3 . 8 静 默 失 败
在前一个示例中,我们告诉用户有一个文件找不到。但并非每次
捕获到异常都需要告诉用户,有时候你希望程序在发生异常时保
持静默,就像什么都没有发生一样继续运行。要让程序静默失
败,可像通常那样编写try 代码块,但在except 代码块中明确
地告诉Python什么都不要做。Python有一个pass 语句,可用于
让Python在代码块中什么都不要做:
def count_words(filename):
""" 计算一个文件大致包含多少个单词。 """
try:
--snip--
except FileNotFoundError:
❶ pass
else:
--snip--
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt',
'little_women.txt']
for filename in filenames:
count_words(filename)
相比于前一个程序,这个程序唯一的不同之处是 ❶ 处的pass 语
句。现在,出现FileNotFoundError 异常时,将执行except
<!-- p328 -->
代码块中的代码,但什么都不会发生。这种错误发生时,不会出
现traceback,也没有任何输出。用户将看到存在的每个文件包含
多少个单词,但没有任何迹象表明有一个文件未找到:
The file alice.txt has about 29465 words.
The file moby_dick.txt has about 215830 words.
The file little_women.txt has about 189079 words.
pass 语句还充当了占位符,提醒你在程序的某个地方什么都没
有做,并且以后也许要在这里做些什么。例如,在这个程序中,
我们可能决定将找不到的文件的名称写入文件missing_files.txt
中。用户看不到这个文件,但我们可以读取它,进而处理所有找
不到文件的问题。
1 0 . 3 . 9 决 定 报 告 哪 些 错 误
该在什么情况下向用户报告错误?又该在什么情况下静默失败
呢?如果用户知道要分析哪些文件,他们可能希望在有文件却没
有分析时出现一条消息来告知原因。如果用户只想看到结果,并
不知道要分析哪些文件,可能就无须在有些文件不存在时告知他
们。向用户显示他不想看到的信息可能会降低程序的可用性。
Python的错误处理结构让你能够细致地控制与用户分享错误信息
的程度,要分享多少信息由你决定。
编写得很好且经过详尽测试的代码不容易出现内部错误,如语法
或逻辑错误,但只要程序依赖于外部因素,如用户输入、存在指
定的文件、有网络链接,就有可能出现异常。凭借经验可判断该
在程序的什么地方包含异常处理块,以及出现错误时该向用户提
供多少相关的信息。
动 手 试 一 试
练 习 1 0 - 6 : 加 法 运 算 提示用户提供数值输入时,常出现
的一个问题是,用户提供的是文本而不是数。在此情况下,
当你尝试将输入转换为整数时,将引发ValueError 异常。
编写一个程序,提示用户输入两个数,再将其相加并打印结
<!-- p329 -->
果。在用户输入的任何一个值不是数时都捕获ValueError
异常,并打印一条友好的错误消息。对你编写的程序进行测
试:先输入两个数,再输入一些文本而不是数。
练 习 1 0 - 7 : 加 法 计 算 器 将为完成练习10-6而编写的代码
放在一个while 循环中,让用户犯错(输入的是文本而不是
数)后能够继续输入数。
练 习 1 0 - 8 : 猫 和 狗 创建文件cats.txt和dogs.txt,在第
一个文件中至少存储三只猫的名字,在第二个文件中至少存
储三条狗的名字。编写一个程序,尝试读取这些文件,并将
其内容打印到屏幕上。将这些代码放在一个try-except 代
码块中,以便在文件不存在时捕获FileNotFound 错误,
并显示一条友好的消息。将任意一个文件移到另一个地方,
并确认except 代码块中的代码将正确执行。
练 习 1 0 - 9 : 静 默 的 猫 和 狗 修改你在练习10-8中编写的
except 代码块,让程序在任意文件不存在时静默失败。
练 习 1 0 - 1 0 : 常 见 单 词 访问古登堡计划,找一些你想分析
的图书。下载这些作品的文本文件或将浏览器中的原始文本
复制到文本文件中。
可以使用方法count() 来确定特定的单词或短语在字符串
中出现了多少次。例如,下面的代码计算'row' 在一个字符
串中出现了多少次:
>> line = "Row, row, row your boat"
>> line.count('row')
>> line.lower().count('row')
请注意,通过使用lower() 将字符串转换为小写,可捕捉
要查找单词的所有格式,而不管其大小写如何。
<!-- p330 -->
编写一个程序,它读取你在古登堡计划中获取的文件,并计
算单词'the' 在每个文件中分别出现了多少次。这里计算得
到的结果并不准确,因为将诸如'then' 和'there' 等单
词也计算在内了。请尝试计算'the ' (包含空格)出现的
次数,看看结果相差多少。
<!-- p331 -->
1 0 . 4 存 储 数 据
很多程序都要求用户输入某种信息,如让用户存储游戏首选项或
提供要可视化的数据。不管关注点是什么,程序都把用户提供的
信息存储在列表和字典等数据结构中。用户关闭程序时,几乎总
是要保存他们提供的信息。一种简单的方式是使用模块json 来
存储数据。
模块json 让你能够将简单的Python数据结构转储到文件中,并
在程序再次运行时加载该文件中的数据。你还可以使用json 在
Python程序之间分享数据。更重要的是,JSON数据格式并非
Python专用的,这让你能够将以JSON格式存储的数据与使用其他
编程语言的人分享。这是一种轻便而有用的格式,也易于学习。
注 意 JSON(JavaScript Object Notation)格式最初是
为JavaScript开发的,但随后成了一种常见格式,被包括
Python在内的众多语言采用。
1 0 . 4 . 1 使 用 json.dump() 和 json.load()
我们来编写一个存储一组数的简短程序,再编写一个将这些数读
取到内存中的程序。第一个程序将使用json.dump() 来存储这
组数,而第二个程序将使用json.load() 。
函数json.dump() 接受两个实参:要存储的数据,以及可用于
存储数据的文件对象。下面演示了如何使用json.dump() 来存
储数字列表:
number_writer.py
import json
numbers = [2, 3, 5, 7, 11, 13]
❶ filename = 'numbers.json'
❷ with open(filename, 'w') as f:
<!-- p332 -->
❸ json.dump(numbers, f)
先导入模块json ,再创建一个数字列表。在 ❶ 处,指定了要将
该数字列表存储到哪个文件中。通常使用文件扩展名.json来指出
文件存储的数据为JSON格式。接下来,以写入模式打开这个文
件,让json 能够将数据写入其中(见 ❷ )。在 ❸ 处,使用函数
json.dump() 将数字列表存储到文件numbers.json中。
这个程序没有输出,但可以打开文件numbers.json来看看内容。
数据的存储格式与Python中一样:
[2, 3, 5, 7, 11, 13]
下面再编写一个程序,使用json.load() 将列表读取到内存
中:
number_reader.py
import json
❶ filename = 'numbers.json'
❷ with open(filename) as f:
❸ numbers = json.load(f)
print(numbers)
在 ❶ 处,确保读取的是前面写入的文件。这次以读取方式打开该
文件,因为Python只需要读取它(见 ❷ )。在 ❸ 处,使用函数
json.load() 加载存储在numbers.json中的信息,并将其赋给
变量numbers 。最后,打印恢复的数字列表,看看是否与
number_writer.py中创建的数字列表相同:
[2, 3, 5, 7, 11, 13]
<!-- p333 -->
这是一种在程序之间共享数据的简单方式。
1 0 . 4 . 2 保 存 和 读 取 用 户 生 成 的 数 据
使用json 保存用户生成的数据大有裨益,因为如果不以某种方
式存储,用户的信息会在程序停止运行时丢失。下面来看一个这
样的例子:提示用户首次运行程序时输入自己的名字,并在再次
运行程序时记住他。
先来存储用户的名字:
remember_me.py
import json
❶ username = input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f:
❷ json.dump(username, f)
❸ print(f"We'll remember you when you come back,
{username}!")
在 ❶ 处,提示输入用户名并将其赋给一个变量。接下来,调用
json.dump() ,并将用户名和一个文件对象传递给它,从而将
用户名存储到文件中(见 ❷ )。然后,打印一条消息,指出存储
了用户输入的信息(见 ❸ ):
What is your name? Eric
We'll remember you when you come back, Eric!
现在再编写一个程序,向已存储了名字的用户发出问候:
greet_user.py
<!-- p334 -->
import json
filename = 'username.json'
with open(filename) as f:
❶ username = json.load(f)
❷ print(f"Welcome back, {username}!")
在 ❶ 处,使用json.load() 将存储在username.json中的信息读
取到变量username 中。恢复用户名后,就可以欢迎用户回来了
(见 ❷ ):
Welcome back, Eric!
需要将这两个程序合并到一个程序(remember_me.py)中。这个
程序运行时,将尝试从文件username.json中获取用户名。因此,
首先编写一个尝试恢复用户名的try 代码块。如果这个文件不存
在,就在except 代码块中提示用户输入用户名,并将其存储到
username.json中,以便程序再次运行时能够获取:
remember_me.py
import json
如果以前存储了用户名,就加载它。
否则,提示用户输入用户名并存储它。
filename = 'username.json'
try:
❶ with open(filename) as f:
❷ username = json.load(f)
❸ except FileNotFoundError:
❹ username = input("What is your name? ")
❺ with open(filename, 'w') as f:
json.dump(username, f)
print(f"We'll remember you when you come back,
{username}!")
else:
print(f"Welcome back, {username}!")
<!-- p335 -->
这里没有任何新代码,只是将前两个示例的代码合并到了一个程
序中。在 ❶ 处,尝试打开文件username.json。如果该文件存在,
就将其中的用户名读取到内存中(见 ❷ ),再执行else 代码
块,打印一条欢迎用户回来的消息。用户首次运行该程序时,文
件username.json不存在,将引发FileNotFoundError 异常
(见 ❸ )。因此Python将执行except 代码块,提示用户输入用
户名(见 ❹ ),再使用json.dump() 存储该用户名并打印一句
问候语(见 ❺ )。
无论执行的是except 还是else 代码块,都将显示用户名和合
适的问候语。如果这个程序是首次运行,输出将如下:
What is your name? Eric
We'll remember you when you come back, Eric!
否则,输出将如下:
Welcome back, Eric!
这是程序之前至少运行了一次时的输出。
1 0 . 4 . 3 重 构
你经常会遇到这样的情况:代码能够正确地运行,但通过将其划
分为一系列完成具体工作的函数,还可以改进。这样的过程称为
重 构 。重构让代码更清晰、更易于理解、更容易扩展。
要重构remember_me.py,可将其大部分逻辑放到一个或多个函数
中。remember_me.py的重点是问候用户,因此将其所有代码都放
到一个名为greet_user() 的函数中:
remember_me.py
<!-- p336 -->
import json
def greet_user():
❶ """ 问候用户,并指出其名字。 """
filename = 'username.json'
try:
with open(filename) as f:
username = json.load(f)
except FileNotFoundError:
username = input("What is your name? ")
with open(filename, 'w') as f:
json.dump(username, f)
print(f"We'll remember you when you come back,
{username}!")
else:
print(f"Welcome back, {username}!")
greet_user()
考虑到现在使用了一个函数,我们删除原注释,转而使用一个文
档字符串来指出程序的作用(见 ❶ )。这个程序更加清晰,但函
数greet_user() 所做的不仅仅是问候用户,还在存储了用户
名时获取它、在没有存储用户名时提示用户输入。
下面来重构greet_user() ,减少其任务。为此,首先将获取
已存储用户名的代码移到另一个函数中:
import json
def get_stored_username():
❶ """ 如果存储了用户名,就获取它。 """
filename = 'username.json'
try:
with open(filename) as f:
username = json.load(f)
except FileNotFoundError:
❷ return None
else:
return username
def greet_user():
""" 问候用户,并指出其名字。 """
username = get_stored_username()
❸ if username:
print(f"Welcome back, {username}!")
<!-- p337 -->
else:
username = input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f:
json.dump(username, f)
print(f"We'll remember you when you come back,
{username}!")
greet_user()
新增的函数get_stored_username() 目标明确, ❶ 处的文档
字符串指出了这一点。如果存储了用户名,该函数就获取并返回
它;如果文件username.json不存在,该函数就返回None (见
❷ )。这是一种不错的做法:函数要么返回预期的值,要么返回
None 。这让我们能够使用函数的返回值做简单的测试。在 ❸
处,如果成功地获取了用户名,就打印一条欢迎用户回来的消
息,否则提示用户输入用户名。
还需要重构greet_user() 中的另一个代码块,将没有存储用
户名时提示用户输入的代码放在一个独立的函数中:
import json
def get_stored_username():
""" 如果存储了用户名,就获取它。 """
--snip--
def get_new_username():
""" 提示用户输入用户名。 """
username = input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f:
json.dump(username, f)
return username
def greet_user():
""" 问候用户,并指出其名字。 """
username = get_stored_username()
if username:
print(f"Welcome back, {username}!")
else:
username = get_new_username()
print(f"We'll remember you when you come back,
<!-- p338 -->
{username}!")
greet_user()
在remember_me.py的这个最终版本中,每个函数都执行单一而清
晰的任务。我们调用greet_user() ,它打印一条合适的消
息:要么欢迎老用户回来,要么问候新用户。为此,它首先调用
get_stored_username() ,该函数只负责获取已存储的用户
名(如果存储了的话)。最后在必要时调用
get_new_username() ,该函数只负责获取并存储新用户的用
户名。要编写出清晰而易于维护和扩展的代码,这种划分必不可
少。
动 手 试 一 试
练 习 1 0 - 1 1 : 喜 欢 的 数 编写一个程序,提示用户输入喜欢
的数,并使用json.dump() 将这个数存储到文件中。再编
写一个程序,从文件中读取这个值,并打印如下所示的消
息。
I know your favorite number! It's _____.
练 习 1 0 - 1 2 : 记 住 喜 欢 的 数 将练习10-11中的程序合二为
一。如果存储了用户喜欢的数,就向用户显示它,否则提示
用户输入喜欢的数并将其存储到文件中。运行这个程序两
次,看看它能否像预期的那样工作。
练 习 1 0 - 1 3 : 验 证 用 户 最后一个remember_me.py版本假设
用户要么已输入用户名,要么是首次运行该程序。我们应该
修改这个程序,以防当前用户并非上次运行该程序的用户。
为此,在greet_user() 中打印欢迎用户回来的消息前,
询问他用户名是否正确。如果不对,就调用
get_new_username() 让用户输入正确的用户名。
<!-- p339 -->
1 0 . 5 小 结
在本章中,你学习了:如何使用文件;如何一次性读取整个文
件,以及如何以每次一行的方式读取文件的内容;如何写入文
件,以及如何将文本附加到文件末尾;什么是异常以及如何处理
程序可能引发的异常;如何存储Python数据结构,以保存用户提
供的信息,避免用户每次运行程序时都需要重新提供。
在第11章中,你将学习高效的代码测试方式。这可帮助你确定代
码正确无误,以及发现扩展现有程序时可能引入的bug。
<!-- p340 -->
第 1 1 章 测 试 代 码
编写函数或类时,还可为其编写测试。通过测
试,可确定代码面对各种输入都能够按要求的那样工作。测
试让你深信,即便有更多人使用你的程序,它也能正确地工
作。在程序中添加新代码时,也可以对其进行测试,确认不
会破坏程序既有的行为。程序员都会犯错,因此每个程序员
都必须经常测试其代码,在用户发现问题前找出它们。
在本章中,你将学习如何使用Python模块unittest 中的工
具来测试代码,还将学习编写测试用例,核实一系列输入都
将得到预期的输出。你将看到测试通过了是什么样子,测试
未通过又是什么样子,还将知道测试未通过如何有助于改进
代码。你将学习如何测试函数和类,并将知道该为项目编写
多少个测试。
<!-- p341 -->
1 1 . 1 测 试 函 数
要学习测试,必须有要测试的代码。下面是一个简单的函数,它
接受名和姓并返回整洁的姓名:
name_function.py
def get_formatted_name(first, last):
""" 生成整洁的姓名。 """
full_name = f"{first} {last}"
return full_name.title()
函数get_formatted_name() 将名和姓合并成姓名:在名和姓
之间加上一个空格并将其首字母大写,再返回结果。为核实
get_formatted_name() 像期望的那样工作,我们来编写一个
使用该函数的程序。程序names.py让用户输入名和姓,并显示整
洁的姓名:
names.py
from name_function import get_formatted_name
print("Enter 'q' at any time to quit.")
while True:
first = input("\nPlease give me a first name: ")
if first == 'q':
break
last = input("Please give me a last name: ")
if last == 'q':
break
formatted_name = get_formatted_name(first, last)
print(f"\tNeatly formatted name: {formatted_name}.")
这个程序从name_function.py中导入get_formatted_name()
。用户可输入一系列名和姓,并看到格式整洁的姓名:
<!-- p342 -->
Enter 'q' at any time to quit.
Please give me a first name: janis
Please give me a last name: joplin
Neatly formatted name: Janis Joplin.
Please give me a first name: bob
Please give me a last name: dylan
Neatly formatted name: Bob Dylan.
Please give me a first name: q
从上述输出可知,合并得到的姓名正确无误。现在假设要修改
get_formatted_name() ,使其还能够处理中间名。这样做
时,要确保不破坏这个函数处理只含有名和姓的姓名的方式。为
此,可在每次修改get_formatted_name() 后都进行测试:运
行程序names.py,并输入像Janis Joplin这样的姓名。不过这太
烦琐了。所幸Python提供了一种自动测试函数输出的高效方式。
倘若对get_formatted_name() 进行自动测试,就能始终确信
当提供测试过的姓名时,该函数都能正确工作。
1 1 . 1 . 1 单 元 测 试 和 测 试 用 例
Python标准库中的模块unittest 提供了代码测试工具。单 元 测
试 用于核实函数的某个方面没有问题。测 试 用 例 是一组单元测
试,它们一道核实函数在各种情形下的行为都符合要求。良好的
测试用例考虑到了函数可能收到的各种输入,包含针对所有这些
情形的测试。全 覆 盖 的测试用例包含一整套单元测试,涵盖了
各种可能的函数使用方式。对于大型项目,要进行全覆盖测试可
能很难。通常,最初只要针对代码的重要行为编写测试即可,等
项目被广泛使用时再考虑全覆盖。
1 1 . 1 . 2 可 通 过 的 测 试
你需要一段时间才能习惯创建测试用例的语法,但创建测试用例
之后,再添加针对函数的单元测试就很简单了。要为函数编写测
试用例,可先导入模块unittest 和要测试的函数,再创建一个
<!-- p343 -->
继承unittest.TestCase 的类,并编写一系列方法对函数行
为的不同方面进行测试。
下面的测试用例只包含一个方法,它检查函数
get_formatted_name() 在给定名和姓时能否正确工作:
test_name_function.py
import unittest
from name_function import get_formatted_name
❶ class NamesTestCase(unittest.TestCase):
""" 测试 name_function.py 。 """
def test_first_last_name(self):
""" 能够正确地处理像 Janis Joplin 这样的姓名吗? """
❷ formatted_name = get_formatted_name('janis',
'joplin')
❸ self.assertEqual(formatted_name, 'Janis Joplin')
❹ if __name__ == '__main__':
unittest.main()
首先,导入了模块unittest 和要测试的函数
get_formatted_name() 。在 ❶ 处,创建了一个名为
NamesTestCase 的类,用于包含一系列针对
get_formatted_name() 的单元测试。这个类可以随意命名,
但最好让它看起来与要测试的函数相关并包含Test 字样。这个
类必须继承unittest.TestCase 类,这样Python才知道如何
运行你编写的测试。
NamesTestCase 只包含一个方法,用于测试
get_formatted_name() 的一个方面。将该方法命名为
test_first_last_name() ,因为要核实的是只有名和姓的
姓名能否被正确格式化。运行test_name_function.py时,所有以
test_ 打头的方法都将自动运行。在这个方法中,调用了要测试
的函数。在本例中,使用实参'janis' 和'joplin' 调用
<!-- p344 -->
get_formatted_name() ,并将结果赋给变量
formatted_name (见 ❷ )。
在 ❸ 处,使用了unittest 类最有用的功能之一:断 言 方法。
断言方法核实得到的结果是否与期望的结果一致。在这里,我们
知道get_formatted_name() 应返回名和姓首字母大写且之间
有一个空格的姓名,因此期望formatted_name 的值为Janis
Joplin 。为检查是否确实如此,我们调用unittest 的方法
assertEqual() ,并向它传递formatted_name 和'Janis
Joplin' 。代码行
self.assertEqual(formatted_name, 'Janis Joplin')
的意思是:“将formatted_name 的值与字符串'Janis
Joplin' 比较。如果它们相等,那么万事大吉;如果它们不相
等,就告诉我一声!”
我们将直接运行这个文件,但需要指出的是,很多测试框架都会
先导入测试文件再运行。导入文件时,解释器将在导入的同时执
行它。 ❹ 处的if 代码块检查特殊变量__name__ ,这个变量是
在程序执行时设置的。如果这个文件作为主程序执行,变量
__name__ 将被设置为'__main__' 。在这里,调用
unittest.main() 来运行测试用例。如果这个文件被测试框架
导入,变量__name__ 的值将不是'__main__' ,因此不会调
用unittest.main() 。
运行test_name_function.py时,得到的输出如下:
.
--------------------------------------------------------------
--------
Ran 1 test in 0.000s
OK
<!-- p345 -->
第一行的句点表明有一个测试通过了。接下来的一行指出Python
运行了一个测试,消耗的时间不到0.001秒。最后的OK 表明该测
试用例中的所有单元测试都通过了。
上述输出表明,给定包含名和姓的姓名时,函数
get_formatted_name() 总是能正确地处理。修改
get_formatted_name() 后,可再次运行这个测试用例。如果
它通过了,就表明给定Janis Joplin这样的姓名时,该函数依然
能够正确地处理。
1 1 . 1 . 3 未 通 过 的 测 试
测试未通过时结果是什么样的呢?我们来修改
get_formatted_name() ,使其能够处理中间名,但同时故意
让该函数无法正确处理像Janis Joplin这样只有名和姓的姓名。
下面是函数get_formatted_name() 的新版本,它要求通过一
个实参指定中间名:
name_function.py
def get_formatted_name(first, middle, last):
""" 生成整洁的姓名。 """
full_name = f"{first} {middle} {last}"
return full_name.title()
这个版本应该能够正确处理包含中间名的姓名,但对其进行测试
时,我们发现它不再能正确处理只有名和姓的姓名。这次运行程
序test_name_function.py时,输出如下:
❶ E
==============================================================
========
❷ ERROR: test_first_last_name (__main__.NamesTestCase)
------------------------------------------------------------
----------
❸ Traceback (most recent call last):
<!-- p346 -->
File "test_name_function.py", line 8, in
test_first_last_name
formatted_name = get_formatted_name('janis', 'joplin')
TypeError: get_formatted_name() missing 1 required
positional argument: 'last'
------------------------------------------------------------
----------
❹ Ran 1 test in 0.000s
❺ FAILED (errors=1)
里面包含很多信息,因为测试未通过时,需要让你知道的事情可
能有很多。第一行输出只有一个字母E (见 ❶ ),指出测试用例
中有一个单元测试导致了错误。接下来,我们看到
NamesTestCase 中的test_first_last_name() 导致了错
误(见 ❷ )。测试用例包含众多单元测试时,知道哪个测试未通
过至关重要。在 ❸ 处,我们看到了一个标准的traceback,指出函
数调用get_formatted_name ('janis', 'joplin') 有问
题,因为缺少一个必不可少的位置实参。
我们还看到运行了一个单元测试(见 ❹ )。最后是一条消息,指
出整个测试用例未通过,因为运行该测试用例时发生了一个错误
(见 ❺ )。这条消息位于输出末尾,让你一眼就能看到。你可不
希望为获悉有多少测试未通过而翻阅长长的输出。
1 1 . 1 . 4 测 试 未 通 过 时 怎 么 办
测试未通过时怎么办呢?如果你检查的条件没错,测试通过意味
着函数的行为是对的,而测试未通过意味着编写的新代码有错。
因此,测试未通过时,不要修改测试,而应修复导致测试不能通
过的代码:检查刚刚对函数所做的修改,找出导致函数行为不符
合预期的修改。
在本例中,get_formatted_name() 以前只需要名和姓两个实
参,但现在要求提供名、中间名和姓。新增的中间名参数是必不
可少的,这导致get_formatted_name() 的行为不符合预期。
<!-- p347 -->
就这里而言,最佳的选择是让中间名变为可选的。这样做后,使
用类似于Janis Joplin的姓名进行测试时,测试就又能通过了,
而且也可以接受中间名。下面来修改get_formatted_name()
,将中间名设置为可选的,然后再次运行这个测试用例。如果通
过了,就接着确认该函数能够妥善地处理中间名。
要将中间名设置为可选的,可在函数定义中将形参middle 移到
形参列表末尾,并将其默认值指定为一个空字符串。还需要添加
一个if 测试,以便根据是否提供了中间名相应地创建姓名:
name_function.py
def get_formatted_name(first, last, middle=''):
""" 生成整洁的姓名。 """
if middle:
full_name = f"{first} {middle} {last}"
else:
full_name = f"{first} {last}"
return full_name.title()
在get_formatted_name() 的这个新版本中,中间名是可选
的。如果向该函数传递了中间名,姓名将包含名、中间名和姓,
否则姓名将只包含名和姓。现在,对于两种不同的姓名,这个函
数都应该能够正确地处理。为确定这个函数依然能够正确处理像
Janis Joplin这样的姓名,我们再次运行
test_name_function.py:
.
--------------------------------------------------------------
--------
Ran 1 test in 0.000s
OK
现在,测试用例通过了。太好了,这意味着这个函数又能正确处
理像Janis Joplin这样的姓名了,而且我们无须手工测试这个函
<!-- p348 -->
数。这个函数之所以很容易修复,是因为未通过的测试让我们得
知新代码破坏了函数原来的行为。
1 1 . 1 . 5 添 加 新 测 试
确定get_formatted_name() 又能正确处理简单的姓名后,我
们再编写一个测试,用于测试包含中间名的姓名。为此,在
NamesTestCase 类中再添加一个方法:
test_name_function.py
--snip--
class NamesTestCase(unittest.TestCase):
""" 测试 name_function.py 。 """
def test_first_last_name(self):
--snip--
def test_first_last_middle_name(self):
""" 能够正确地处理像 Wolfgang Amadeus Mozart 这样的姓名
吗? """
❶ formatted_name = get_formatted_name(
'wolfgang', 'mozart', 'amadeus')
self.assertEqual(formatted_name, 'Wolfgang Amadeus
Mozart')
if __name__ == '__main__':
unittest.main()
将该方法命名为test_first_last_middle_name() 。方法
名必须以test_ 打头,这样它才会在我们运行
test_name_function.py时自动运行。这个方法名清楚地指出了它
测试的是get_formatted_name() 的哪个行为。这样,如果该
测试未通过,我们就能马上知道受影响的是哪种类型的姓名。可
以在TestCase 类中使用很长的方法名,而且这些方法名必须是
描述性的,这样你才能看懂测试未通过时的输出。这些方法由
Python自动调用,你根本不用编写调用它们的代码。
<!-- p349 -->
为测试函数get_formatted_name() ,我们使用名、姓和中间
名调用它(见 ❶ ),再使用assertEqual() 检查返回的姓名是
否与预期的姓名(名、中间名和姓)一致。再次运行
test_name_function.py时,两个测试都通过了:
..
--------------------------------------------------------------
--------
Ran 2 tests in 0.000s
OK
太好了!现在我们知道,这个函数又能正确地处理像Janis
Joplin这样的姓名了,而且深信它也能够正确地处理像Wolfgang
Amadeus Mozart这样的姓名。
动 手 试 一 试
练 习 1 1 - 1 : 城 市 和 国 家 编写一个函数,它接受两个形
参:一个城市名和一个国家名。这个函数返回一个格式为
City , Country 的字符串,如Santiago, Chile 。将
这个函数存储在一个名为city_functions.py的模块中。
创建一个名为test_cities.py的程序,对刚才编写的函数进
行测试(别忘了,需要导入模块unittest 和要测试的函
数)。编写一个名为test_city_country() 的方法,核
实使用类似于'santiago' 和'chile' 这样的值来调用前
述函数时,得到的字符串是正确的。运行test_cities.py,
确认测试test_city_country() 通过了。
练 习 1 1 - 2 : 人 口 数 量 修改前面的函数,加上第三个必不
可少的形参population ,并返回一个格式为 City ,
Country – population xxx 的字符串,如Santiago,
Chile – population 5000000 。运行
test_cities.py,确认测试test_city_country() 未通
过。
<!-- p350 -->
修改上述函数,将形参population 设置为可选的。再次运
行test_cities.py,确认测试test_city_country() 又
通过了。
再编写一个名为test_city_country_population() 的
测试,核实可以使用类似于'santiago' 、'chile'
和'population=5000000' 这样的值来调用这个函数。再
次运行test_cities.py,确认测试
test_city_country_population() 通过了。
<!-- p351 -->
1 1 . 2 测 试 类
在本章前半部分,你编写了针对单个函数的测试,下面来编写针
对类的测试。很多程序中都会用到类,因此证明你的类能够正确
工作大有裨益。如果针对类的测试通过了,你就能确信对类所做
的改进没有意外地破坏其原有的行为。
1 1 . 2 . 1 各 种 断 言 方 法
Python在unittest.TestCase 类中提供了很多断言方法。前
面说过,断言方法检查你认为应该满足的条件是否确实满足。如
果该条件确实满足,你对程序行为的假设就得到了确认,可以确
信其中没有错误。如果你认为应该满足的条件实际上并不满足,
Python将引发异常。
表11-1描述了6个常用的断言方法。使用这些方法可核实返回的值
等于或不等于预期的值,返回的值为True 或False ,以及返回
的值在列表中或不在列表中。只能在继承unittest.TestCase
的类中使用这些方法,随后来看看如何在测试类时使用其中之
一。
表 1 1 - 1 u n i t t e s t 模 块 中 的 断 言 方 法
方 法 用 途
assertEqual(a, b) 核实a == b
assertNotEqual(a, b) 核实a != b
assertTrue(x) 核实x 为True
assertFalse(x) 核实x 为False
<!-- p352 -->
方 法 用 途
assertIn(item , list ) 核实 item 在 list 中
assertNotIn(item , list ) 核实 item 不在 list 中
1 1 . 2 . 2 一 个 要 测 试 的 类
类的测试与函数的测试相似,你所做的大部分工作是测试类中方
法的行为。不过还是存在一些不同之处,下面编写一个要测试的
类。来看一个帮助管理匿名调查的类:
survey.py
class AnonymousSurvey:
""" 收集匿名调查问卷的答案。 """
❶ def __init__(self, question):
""" 存储一个问题,并为存储答案做准备。 """
self.question = question
self.responses = []
❷ def show_question(self):
""" 显示调查问卷。 """
print(self.question)
❸ def store_response(self, new_response):
""" 存储单份调查答卷。 """
self.responses.append(new_response)
❹ def show_results(self):
""" 显示收集到的所有答卷。 """
print("Survey results:")
for response in self.responses:
print(f"- {response}")
<!-- p353 -->
这个类首先存储了一个调查问题(见 ❶ ),并创建了一个空列
表,用于存储答案。这个类包含打印调查问题的方法(见 ❷ ),
在答案列表中添加新答案的方法(见 ❸ ),以及将存储在列表中
的答案都打印出来的方法(见 ❹ )。要创建该类的实例,只需提
供一个问题即可。有了表示调查的实例后,就可使用
show_question() 来显示其中的问题,使用
store_response() 来存储答案并使用show_results() 来
显示调查结果。
为证明AnonymousSurvey 类能够正确工作,编写一个使用它的
程序:
language_survey.py
from survey import AnonymousSurvey
定义一个问题,并创建一个调查。
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
显示问题并存储答案。
my_survey.show_question()
print("Enter 'q' at any time to quit.\n")
while True:
response = input("Language: ")
if response == 'q':
break
my_survey.store_response(response)
显示调查结果。
print("\nThank you to everyone who participated in the
survey!")
my_survey.show_results()
这个程序定义了一个问题("What language did you
first learn to speak? " ),并使用该问题创建了一个
AnonymousSurvey 对象。接下来,这个程序调用
show_question() 来显示问题,并提示用户输入答案。在收到
<!-- p354 -->
每个答案的同时将其存储起来。用户输入所有答案(输入q 要求
退出)后,调用show_results() 来打印调查结果:
What language did you first learn to speak?
Enter 'q' at any time to quit.
Language: English
Language: Spanish
Language: English
Language: Mandarin
Language: q
Thank you to everyone who participated in the survey!
Survey results:
- English
- Spanish
- English
- Mandarin
AnonymousSurvey 类可用于进行简单的匿名调查。假设我们将
它放在了模块survey 中,并想进行改进:让每位用户都可输入
多个答案;编写一个方法,只列出不同的答案并指出每个答案出
现了多少次;再编写一个类,用于管理非匿名调查。
进行上述修改存在风险,可能影响AnonymousSurvey 类的当前
行为。例如,允许每位用户输入多个答案时,可能会不小心修改
处理单个答案的方式。要确认在开发这个模块时没有破坏既有行
为,可以编写针对这个类的测试。
1 1 . 2 . 3 测 试 AnonymousSurvey 类
下面来编写一个测试,对AnonymousSurvey 类的行为的一个方
面进行验证:如果用户面对调查问题只提供一个答案,这个答案
也能被妥善地存储。为此,我们将在这个答案被存储后,使用方
法assertIn() 来核实它确实在答案列表中:
test_survey.py
<!-- p355 -->
import unittest
from survey import AnonymousSurvey
❶ class TestAnonymousSurvey(unittest.TestCase):
""" 针对 AnonymousSurvey 类的测试。 """
❷ def test_store_single_response(self):
""" 测试单个答案会被妥善地存储。 """
question = "What language did you first learn to
speak?"
❸ my_survey = AnonymousSurvey(question)
my_survey.store_response('English')
❹ self.assertIn('English', my_survey.responses)
if __name__ == '__main__':
unittest.main()
首先导入模块unittest 和要测试的类AnonymousSurvey 。
将测试用例命名为TestAnonymousSurvey ,它也继承了
unittest.TestCase (见 ❶ )。第一个测试方法验证:调查问
题的单个答案被存储后,会包含在调查结果列表中。对于这个方
法,一个不错的描述性名称是
test_store_single_response() (见 ❷ )。如果这个测试
未通过,我们就能通过输出中的方法名得知,在存储单个调查答
案方面存在问题。
要测试类的行为,需要创建其实例。在 ❸ 处,使用问题"What
language did you first learn to speak?" 创建一个
名为my_survey 的实例,然后使用方法store_response()
存储单个答案English 。接下来,检查English 是否包含在列
表my_survey.responses 中,以核实这个答案是否被妥善地
存储了(见 ❹ )。
当我们运行test_survey.py时,测试通过了:
.
--------------------------------------------------------------
--------
Ran 1 test in 0.001s
<!-- p356 -->
OK
这很好,但只能收集一个答案的调查用途不大。下面来核实当用
户提供三个答案时,它们也将被妥善地存储。为此,在
TestAnonymousSurvey 中再添加一个方法:
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
""" 针对 AnonymousSurvey 类的测试。 """
def test_store_single_response(self):
--snip--
def test_store_three_responses(self):
""" 测试三个答案会被妥善地存储。 """
question = "What language did you first learn to
speak?"
my_survey = AnonymousSurvey(question)
❶ responses = ['English', 'Spanish', 'Mandarin']
for response in responses:
my_survey.store_response(response)
❷ for response in responses:
self.assertIn(response, my_survey.responses)
if __name__ == '__main__':
unittest.main()
我们将该方法命名为test_store_three_responses() ,并
像对test_store_single_response() 所做的一样,在其中
创建一个调查对象。定义一个包含三个不同答案的列表(见
❶ ),再对其中每个答案调用store_response() 。存储这些
答案后,使用一个循环来确认每个答案都包含在
my_survey.responses 中(见 ❷ )。
再次运行test_survey.py时,两个测试(针对单个答案的测试和
针对三个答案的测试)都通过了:
<!-- p357 -->
..
--------------------------------------------------------------
--------
Ran 2 tests in 0.000s
OK
前述做法的效果很好,但这些测试有些重复的地方。下面使用
unittest 的另一项功能来提高其效率。
1 1 . 2 . 4 方 法 setUp()
在前面的test_survey.py中,我们在每个测试方法中都创建了一
个AnonymousSurvey 实例,并在每个方法中都创建了答案。
unittest.TestCase 类包含的方法setUp() 让我们只需创建
这些对象一次,就能在每个测试方法中使用。如果在TestCase
类中包含了方法setUp() ,Python将先运行它,再运行各个以
test_ 打头的方法。这样,在你编写的每个测试方法中,都可使
用在方法setUp() 中创建的对象。
下面使用setUp() 来创建一个调查对象和一组答案,供方法
test_store_single_response() 和
test_store_three_responses() 使用:
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
""" 针对 AnonymousSurvey 类的测试。 """
def setUp(self):
"""
创建一个调查对象和一组答案,供使用的测试方法使用。
"""
question = "What language did you first learn to
speak?"
❶ self.my_survey = AnonymousSurvey(question)
❷ self.responses = ['English', 'Spanish', 'Mandarin']
def test_store_single_response(self):
<!-- p358 -->
""" 测试单个答案会被妥善地存储。 """
self.my_survey.store_response(self.responses[0])
self.assertIn(self.responses[0],
self.my_survey.responses)
def test_store_three_responses(self):
""" 测试三个答案会被妥善地存储。 """
for response in self.responses:
self.my_survey.store_response(response)
for response in self.responses:
self.assertIn(response,
self.my_survey.responses)
if __name__ == '__main__':
unittest.main()
方法setUp() 做了两件事情:创建一个调查对象(见 ❶ ),以及
创建一个答案列表(见 ❷ )。存储这两样东西的变量名包含前缀
self (即存储在属性中),因此可在这个类的任何地方使用。
这让两个测试方法都更简单,因为它们都不用创建调查对象和答
案了。方法test_store_single_response() 核实
self.responses 中的第一个答案self.responses[0] 被
妥善地存储,而方法test_store_three_response() 核实
self.responses 中的全部三个答案都被妥善地存储。
再次运行test_survey.py时,这两个测试也都通过了。如果要扩
展AnonymousSurvey ,使其允许每位用户输入多个答案,这些
测试将很有用。修改代码以接受多个答案后,可运行这些测试,
确认存储单个答案或一系列答案的行为未受影响。
测试自己编写的类时,方法setUp() 让测试方法编写起来更容
易:可在setUp() 方法中创建一系列实例并设置其属性,再在
测试方法中直接使用这些实例。相比于在每个测试方法中都创建
实例并设置其属性,这要容易得多。
注 意 运行测试用例时,每完成一个单元测试,Python都
打印一个字符:测试通过时打印一个句点,测试引发错误时
打印一个E ,而测试导致断言失败时则打印一个F 。这就是
你运行测试用例时,在输出的第一行中看到的句点和字符数
<!-- p359 -->
量各不相同的原因。如果测试用例包含很多单元测试,需要
运行很长时间,就可通过观察这些结果来获悉有多少个测试
通过了。
动 手 试 一 试
练 习 1 1 - 3 : 雇 员 编写一个名为Employee 的类,其方法
__init__() 接受名、姓和年薪,并将它们存储在属性中。
编写一个名为give_raise() 的方法,它默认将年薪增加
5000美元,但也能够接受其他的年薪增加量。
为Employee 编写一个测试用例,其中包含两个测试方法:
test_give_default_raise() 和
test_give_custom_raise() 。使用方法setUp() ,
以免在每个测试方法中都新建雇员实例。运行这个测试用
例,确认两个测试都通过了。