Control Flow 控制流
基本上是这章的翻译,有能力建议去看原文。
到目前为止,我们所能定义的函数的表达能力非常有限,因为我们还没有引入进行比较和根据比较结果执行不同操作的方法。控制语句将为我们提供这种能力。它们是根据逻辑比较结果控制程序执行流程的语句。 控制语句与我们迄今为止学习过的表达式有着本质区别。它们没有价值。执行控制语句并不计算什么,而是决定解释器下一步应该做什么。
控制语句
到目前为止,我们主要考虑了如何评估表达式。然而,我们已经看到了三种语句:赋值、def 和 return 语句。这些 Python 代码行本身并不是表达式,尽管它们都包含表达式作为组件。 控制语句不是被求值,而是被执行。每条语句都描述了解释器状态的某种变化,执行语句就会应用这种变化。正如我们在返回语句和赋值语句中所看到的,执行语句可能涉及对语句中包含的子表达式进行求值。 表达式也可以作为语句执行,在这种情况下,表达式会被求值,但其值会被丢弃。执行纯粹的函数不会产生任何影响,但执行非纯粹的函数却会因函数的应用而产生影响。例如,考虑下面的代码:
>>> def square(x):
mul(x, x) # 注意!此调用不会返回值。
2
这个示例是有效(符合语法)的 Python 函数,但可能与编写者的原意不符。函数的主体是一个表达式。表达式本身是一个有效的语句,但语句的效果是调用 mul 函数,并丢弃结果。如果要对表达式的结果进行处理,你则需要说明:你应该用赋值语句将其存储,或用 return 语句将其返回:
>>> def square(x):
return mul(x, x)
2
有时,当调用像 print 这样的非纯函数时,函数体是一个表达式确实是是有意义的。
>>> def print_square(x):
print(square(x))
2
在最高层次上,Python 解释器的工作是执行由语句组成的程序。然而,计算中许多有趣的工作都来自于对表达式的求值。语句控制着程序中不同表达式之间的关系以及它们的结果。
复合语句
一般来说,Python 代码是一系列语句。简单语句是不以冒号结尾的单行语句。之所以称为复合语句,是因为它由其他语句(简单语句和复合语句)组成。复合语句通常跨越多行,并以冒号结束的单行标题开始,冒号用于标识语句类型。标题和缩进的语句组合称为子句。复合语句由一个或多个子句组成:
<header>:
<statement>
<statement>
...
<separating header>:
<statement>
<statement>
...
...
2
3
4
5
6
7
8
9
我们可以用这些术语来理解我们已经介绍过的语句。
- 表达式、返回语句和赋值语句是简单语句。
- def 语句是复合语句。def 函数头之后的套语定义了函数体。
每种函数头(header)都有专门的评估规则,规定何时以及是否执行其语句组中的语句。 我们说函数头控制着它的语句组。 例如,在 def 语句中,我们看到返回表达式不会立即被求值,而是被存储起来,以便在最终调用所定义的函数时使用。 我们现在还能理解多行程序。
- 要执行一系列语句,先执行第一条语句。如果该语句没有重定向控制,则继续执行语句序列的其余语句(如果还有的话)。
这个定义揭示了递归定义序列的基本结构:一个序列可以分解为它的第一个元素和其余元素。语句序列的 "其余部分" 本身就是一个语句序列!因此,我们可以递归地应用这一执行规则。这种将序列视为递归数据结构的观点将在以后的章节中再次出现。 这一规则的重要结果是,语句按顺序执行,但由于控制的重定向,后面的语句可能永远不会被执行。 实用指南:在缩进一个语句块时,所有行的缩进量和缩进方式必须相同(使用空格,而不是制表符)。缩进的任何变化都会导致错误。 我们可以用这些术语来理解我们已经引入的语句。
函数定义 II:局部赋值
最初,我们指出用户定义函数的主体只包含一个返回语句和一个返回表达式。事实上,函数定义的操作序列可以超出单个表达式的范围。 每次执行一个用户定义的函数时,其定义的语句块中的一系列子句就会在本地环境中执行 —— 这是一个从调用该函数创建的局部框架开始的环境。一个 return 语句重定向控制流::只要第一个返回语句被执行,函数应用过程就会终止,返回表达式的值就是所应用函数的返回值。 赋值语句可以出现在函数体中。例如,该函数通过两步计算,将两个量之间的绝对差返回为第一个量的百分比:
def percent_difference(x, y):
difference = abs(x-y)
return 100 * difference / x
result = percent_difference(40, 50)
2
3
4
赋值语句的效果是在当前环境的第一个框架中将名称绑定到值。因此,函数体内的赋值语句不能影响全局框架。函数只能操作其局部环境这一事实对于创建模块化程序至关重要,在模块化程序中,纯函数仅通过其取值和返回值进行交互。 当然,percent_difference 函数可以写成一个单一表达式,如下所示,但返回表达式更为复杂。
>>> def percent_difference(x, y):
return 100 * abs(x-y) / x
>>> percent_difference(40, 50)
25.0
2
3
4
到目前为止,局部赋值并没有增加我们的函数定义的表达能力。它将在与其他控制语句结合时做到这一点。此外,局部赋值在通过将名称分配给中间量来澄清复杂表达式的意义方面也起着关键作用。
条件语句
Python 有一个用于计算绝对值的内置函数。
>>> abs(-2)
2
2
我们希望能够自己实现这样的函数,但我们没有显而易见的方法来定义具有比较和选择的函数。我们希望表达如果 x 为正数, abs(x)
返回 x。此外,如果 x 为 0,abs (x) 返回 0。否则, abs(x)
返回 -x。在 Python 中,我们可以使用条件语句来表达这种选择。
def absolute_value(x):
"""Compute abs(x)."""
if x > 0:
return x
elif x == 0:
return 0
else:
return -x
result = absolute_value(-2)
2
3
4
5
6
7
8
9
10
这种 absolute_value 的实现方式引发了几个值得讨论的话题: 条件语句:Python 中的条件语句由一系列标题和套语组成:必选的 if 子句,可选的 elif 子句序列,最后是可选的 else 子句:
if <expression>:
<suite>
elif <expression>:
<suite>
else:
<suite>
2
3
4
5
6
在执行条件语句时,每个子句都是按顺序考虑的。执行条件子句的计算过程如下。
- 给第一个表达式求值。
- 如果它是一个真值,执行语句块。然后,跳过条件语句中的所有后续子句。
- 如果到达 else 子句(仅在所有 if 和 elif 表达式求值均为假时发生),则执行其语句块。
布尔上下文。上面的执行程序提到了 "一个假值" 和 "一个真值"。条件代码块头部语句中的表达式被称为布尔上下文:它们的真值对控制流很重要,但除此之外,它们的值不会被赋值或返回。Python 包含几个假值,包括 0、None 和布尔值 False。所有其它数字都是真值。在第 2 章中,我们将看到 Python 中的每种内置数据都有真值和假值。
布尔值。Python 有两个布尔值,分别称为 True 和 False。布尔值表示逻辑表达式中的真值。内置的比较操作,>,<,>=,<=,==,!=,返回这些值。
>>> 4 < 2
False
>>> 5 >= 5
True
2
3
4
第二个示例的含义是 “5 大于或等于 5”,对应于 operator 模块中的函数 ge。
>>> 0 == -0
True
2
最后一个示例的含义是 “0 等于 -0”,对应于 operator 模块中的函数 eq。请注意,Python 区分赋值(=)和相等比较(==),这是许多编程语言共享的惯例。
布尔运算符。Python 中还内置了三个基本逻辑运算符:
>>> True and False
False
>>> True or False
True
>>> not False
True
2
3
4
5
6
逻辑表达式有相应的求值程序。这些程序利用了一个事实,即逻辑表达式的真值有时可以在不求值其所有子表达式的情况下确定,这一特性称为短路,下面是几个例子:
>>> True and False
False
>>> True and False or True
True
>>> False and True or False
False
>>> True or True and False
True
2
3
4
5
6
7
8
评估表达式 <左表达式>
and <右表达式>
:
- 评估子表达式
<左表达式>
。 - 如果
<左表达式>
的结果为 False,则表达式求值为 False。 - 否则(即
<左表达式>
的结果为 True),表达式的值为<右表达式>
的值。
评估表达式 <left>
or <right>
:
- 评估子表达式
<left>
。 <左表达式>
为 True,则表达式求值为 True。- 否则(即
<左表达式>
为 False),表达式将求值为子表达式<右表达式>
的值。
对表达式 not <exp>
进行求值: 1. 对 <exp>
进行求值;如果结果为假值,则值为 True,否则为 False。
这些值、规则和运算符为我们提供了组合比较结果的方法。执行比较并返回布尔值的函数通常以 is 开头,后面不加下划线(例如,isfinite、isdigit、isinstance 等)。
迭代
考虑一下斐波那契数列,其中每个数字都是前两个数字的和: 0,1,1,2,3,5,8,13,21,...... 每个值都是通过重复应用和 - 前两个规则构造出来的。例如,第八个斐波那契数字是 13。我们可以使用 while 语句来枚举 n 个斐波那契数。我们需要跟踪我们创建了多少个值(k),以及第 k 个值(curr)和它的前一个值(pred)。逐步浏览该函数,观察斐波那契数如何在 curr 的约束下逐一演变。
def fib(n):
"""Compute the nth Fibonacci number, for n >= 2."""
pred, curr = 0, 1 # Fibonacci numbers 1 and 2
k = 2 # Which Fib number is curr?
while k < n:
pred, curr = curr, pred + curr
k = k + 1
return curr
result = fib(8)
2
3
4
5
6
7
8
9
10
请记住,逗号分隔赋值语句中的多个名称和数值。这一行 pred, curr = curr, pred + curr
其效果是将名称 pred
重新绑定到 curr
的值,同时将 curr
重新绑定到 pred + curr
的值。在重新绑定之前,要对 = 右边的所有表达式进行运算。在更新左侧的绑定之前,先对 = 右侧的所有表达式进行运算,这样的运算顺序对函数的正确性至关重要。while 子句包含一个头表达式,后面跟着一个子句:
一个 while 子句包含一个标题表达式,后跟一个语句块:
while <expression>:
<suite>
2
执行 while 子句:
- 评估标题表达式。
- 如果是真值,则执行该子句,然后返回步骤 1。
在第 2 步中,在再次评估标题表达式之前,将执行 while 子句的整个子句集。为了防止 while 子句的整套语句被无限期执行,整套语句在每次执行时都应改变某些绑定。不终止的 while 语句称为无限循环。按 Ctrl+C 强制 Python 停止循环。
测试
测试函数就是验证函数的行为是否符合预期。现在,我们的函数语言已经足够复杂,我们需要开始测试我们的实现。 测试是一种系统地进行验证的机制。测试通常采用另一个函数的形式,其中包含对被测函数的一个或多个样本调用。然后将返回值与预期结果进行验证。与大多数旨在通用的函数不同,测试涉及选择和验证带有特定参数值的调用。测试也是一种文档:它们展示了如何调用函数以及哪些参数值是合适的。 断言(Assertions)。程序员使用断言语句来验证预期,例如被测试函数的输出。断言语句在布尔上下文中包含一个表达式,后面是一行带引号的文本(单引号或双引号都可以,但要保持一致),如果表达式的值为假,就会显示该文本。
>>> assert fib(8) == 13, '第8个斐波那契数应该是13'
当断言的表达式评估为真值时,执行 assert 语句没有效果。当它是假值时,assert 会导致一个错误,停止执行。 一个用于 fib 的测试函数应该测试几个参数,包括 n 的极值。
>>> def fib_test():
assert fib(2) == 1, '第2个斐波那契数应该是1'
assert fib(3) == 1, '第3个斐波那契数应该是1'
assert fib(50) == 7778742049, '第50个斐波那契数错误'
2
3
4
当在文件中编写 Python 而不是直接在解释器中编写时,测试通常写在同一个文件或后缀为 _test.py 的邻近文件中。 文档测试(Doctest)。Python 提供了一种方便的方法,可以将简单的测试直接放在函数的 docstring 中。docstring 的第一行应包含对函数的一行描述,然后是一行空行。随后可以是对参数和行为的详细描述。此外,docstring 还可以包含一个调用函数的交互会话示例:
>>> def sum_naturals(n):
"""返回前 n 个自然数的和。
>>> sum_naturals(10)
55
>>> sum_naturals(100)
5050
"""
total, k = 0, 1
while k <= n:
total, k = total + k, k + 1
return total
2
3
4
5
6
7
8
9
10
11
12
然后,可以通过 doctest 模块验证交互。下面,globals 函数返回全局环境的表示,解释器需要用它来评估表达式
>>> from doctest import testmod
>>> testmod()
TestResults(failed=0, attempted=2)
2
3
为了验证 doctest 仅对单个函数的交互作用,我们调用了名为 run_docstring_examples
,这个函数是 doctest 模块下的一个函数。不幸的是,这个函数的调用有点复杂。它的第一个参数是要测试的函数。第二个参数应始终是表达式 globals()
的结果,这是一个返回全局环境的内置函数。第三个参数是 True
,表示我们需要 "详细" 输出:所有测试运行的目录。
>>> from doctest import run_docstring_examples
>>> run_docstring_examples(sum_naturals, globals(), True)
Finding tests in NoName
Trying:
sum_naturals(10)
Expecting:
55
ok
Trying:
sum_naturals(100)
Expecting:
5050
ok
2
3
4
5
6
7
8
9
10
11
12
13
当函数的返回值与预期结果不一致时, run_docstring_examples
函数将报告此问题为测试失败。
在文件中编写 Python 时,可以通过启动 Python 并带有 doctest
命令行选项运行文件中的所有文档测试:
python3 -m doctest <python_source_file>
有效测试的关键是在实现新功能后立即编写(并运行)测试。甚至在实现之前编写一些测试,以便在脑海中形成一些输入和输出示例,也是一种很好的做法。应用单个函数的测试称为单元测试。详尽的单元测试是良好程序设计的标志。