# 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。
01. 安 装 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

02. 在 终 端 会 话 中 运 行 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() 。

03. 安 装 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，并确保其配置正确无误。
01. 检 查 是 否 安 装 了 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的版本，请按下一小节的
说明安装最新版本。

02. 安 装 最 新 的 P y t h o n 版 本
要下载Python安装程序，可访问Python网站主页。将鼠标指向
Download链接，你将看到一个用于下载最新版本Python的按钮。
单击该按钮，这将根据你的系统自动下载正确的安装程序。下载
安装程序后，运行它。
运行安装程序后，在终端提示符下执行如下命令：
$  python3 --version
Python 3.7.2


输出应该类似于上面这样。如果确实如此，就可以开始尝试使用
Python了，但请务必将本书中的每个命令python 都替换为

<!-- p50 -->

python3 。

03. 在 终 端 会 话 中 运 行 P y t h o n 代 码
现在可以打开终端窗口并执行命令python3 ，再尝试运行
Python代码片段。请在终端会话中输入如下代码行并按回车：
>>> print("Hello Python interpreter!")
Hello Python interpreter!
>>>


消息将直接输出到当前终端窗口中。别忘了，要关闭Python解释
器，可按Ctrl + D或执行命令exit() 。

04. 安 装 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的人认为，你很可能会使用这种系统进行编
程，他们也鼓励你这样做。因此，要在这种系统中编程，你几乎不用
安装什么软件，只需要修改一些设置。
01. 检 查 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，了解如何安装
最新版。
02. 在 终 端 会 话 中 运 行 P y t h o n 代 码
现在可打开终端窗口并执行命令python3 ，再尝试运行Python
代码片段。检查Python版本时，你就这样做过。下面再次这样
做，然后在终端会话中输入如下代码并按回车：
>>> print("Hello Python interpreter!")
Hello Python interpreter!
>>>


消息将直接打印到当前终端窗口中。别忘了，要关闭Python解释
器，可按Ctrl + D或执行命令exit() 。
03. 安 装 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提供了多种在既有列表中添加新数据的方式。
01. 在 列 表 末 尾 添 加 元 素
在列表中添加新元素时，最简单的方式是将元素附 加
（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']


这种创建列表的方式极其常见，因为经常要等程序运行后，你才
知道用户要在程序中存储哪些数据。为控制用户，可首先创建一
个空列表，用于存储用户将要输入的值，然后将用户提供的每个
新值附加到列表中。

02. 在 列 表 中 插 入 元 素
使用方法insert() 可在列表的任何位置添加新元素。为此，你
需要指定新元素的索引和值。
  motorcycles = ['honda', 'yamaha', 'suzuki']

❶  motorcycles.insert(0, 'ducati')
  print(motorcycles)


在这个示例中，值'ducati' 被插入到了列表开头（见 ❶ ）。方
法insert() 在索引0 处添加空间，并将值'ducati' 存储到
这个地方。这种操作将列表中既有的每个元素都右移一个位置：
['ducati', 'honda', 'yamaha', 'suzuki']


#### 3.2.3 从列表中删除元素

你经常需要从列表中删除一个或多个元素。例如，玩家将空中的一个
外星人射杀后，你很可能要将其从存活的外星人列表中删除；当用户

<!-- p95 -->

在你创建的Web应用中注销账户时，你就需要将该用户从活动用户列表
中删除。你可以根据位置或值来删除列表中的元素。
01. 使 用 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 语句将值从列表中删除后，你就无法
再访问它了。
02. 使 用 方 法 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.


03. 弹 出 列 表 中 任 何 位 置 处 的 元 素
实际上，可以使用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() 。

04. 根 据 值 删 除 元 素
有时候，你不知道要从列表中删除的值所处的位置。如果只知道
要删除的元素的值，可使用方法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() ，
以免在每个测试方法中都新建雇员实例。运行这个测试用
例，确认两个测试都通过了。