1.5 控制

此时,我们可以定义的函数的表达能力非常有限,因为我们还没有引入一种方法来进行比较,并根据比较的结果执行不同的操作。控制语句将赋予我们这种能力。它们是基于逻辑比较的结果控制程序执行流的语句。

控制语句与我们到目前为止学过的表达式有根本的不同。它们没有值。执行控制语句来决定解释器下一步应该做什么,不是计算什么。

1.5.1 声明

到目前为止,我们主要考虑了如何对表达式求值。但是,我们已经看到了三种语句:赋值、def和返回语句。这些Python代码行本身不是表达式,尽管它们都包含表达式作为组件。

不是求值,而是执行语句。每条语句描述解释器状态的一些变化,执行一条语句应用这些变化。正如我们在return和赋值语句中看到的,执行语句可能涉及对包含在语句中的子表达式求值。

表达式也可以作为语句执行,在这种情况下,它们被求值,但是它们的值被丢弃。执行纯函数没有效果,但执行非纯函数会导致函数应用程序的结果。

>>> def square(x):
        mul(x, x) # Watch out! This call doesn't return a value.

这个例子是有效的Python,但可能不是我们想要的。函数体由一个表达式组成。表达式本身是有效的语句,但语句的效果是调用mul函数,并丢弃结果。如果你想对一个表达式的结果做一些事情,你需要这样说:你可以用赋值语句存储它,或者用return语句返回它:

>>> def square(x):
        return mul(x, x)

有时,当调用print等非纯函数时,使用函数体为表达式的函数是有意义的。

>>> def print_square(x):
        print(square(x))

在其最高级别,Python解释器的工作是执行由语句组成的程序。然而,许多有趣的计算工作来自对表达式求值。语句控制程序中不同表达式之间的关系以及它们的结果。

1.5.2 复合语句

通常,Python代码是一个语句序列。简单的语句是不以冒号结束的单行语句。之所以称为复合语句,是因为它是由其他语句(简单语句和复合语句)组成的。复合语句通常跨多行,并以以冒号结尾的单行标题开头,冒号标识语句的类型。标题和缩进的语句集合一起称为子句。复合语句由一个或多个子句组成:

<header>:
    <statement>
    <statement>
    ...
<separating header>:
    <statement>
    <statement>
    ...
...

我们可以理解我们在这些术语中已经介绍的陈述。

  • 表达式、返回语句和赋值语句都是简单语句。

  • def语句是一个复合语句。def头文件后面的套件定义了函数体。

针对每种头文件的专门化求值规则规定何时以及是否执行其套件中的语句。我们说头文件控制它的套件。例如,在def语句中,我们看到返回表达式不是立即求值,而是存储起来,以便在最终调用定义的函数时使用。

我们现在也可以理解多行程序了。

  • 要执行一个语句序列,执行第一个语句。如果该语句没有重定向控制,则继续执行语句序列的其余部分(如果还有的话)。

这个定义揭示了递归定义序列的基本结构:可以将序列分解为其第一个元素和其他元素。 一个语句序列的“rest”本身就是一个语句序列!因此,我们可以递归地应用这个执行规则。 序列作为递归数据结构的观点将在后面的章节中再次出现。

该规则的重要结果是,语句按顺序执行,但由于重定向控制,以后的语句可能永远无法到达。

实际指导。 缩进一个套件时,所有行必须缩进相同的数量和方式(使用空格,而不是制表符)。 缩进的任何变化都会导致错误。

1.5.3 定义函数II:局部赋值

最初,我们声明了用户定义函数的函数体仅由一个return语句和一个return表达式组成。实际上,函数可以定义扩展到单个表达式之外的操作序列。

每当应用了用户定义函数时,其定义集合中的子句序列就会在一个本地环境中执行——这个环境以一个通过调用该函数创建的本地框架开始。return语句重定向控制:函数应用程序的进程在执行第一个return语句时终止,并且return表达式的值就是应用函数的返回值。

赋值语句可以出现在函数体中。例如,该函数使用两步计算,返回两个量之间的绝对差,作为第一个量的百分比:

赋值语句的作用是将名称绑定到当前环境的第一个框架中的值。因此,函数体中的赋值语句不能影响全局框架。函数只能操作其局部环境这一事实对于创建模块化程序至关重要,在模块化程序中,纯函数仅通过它们获取和返回的值进行交互。

当然,percent_difference函数可以写成一个表达式,如下所示,但是返回表达式更复杂。

>>> def percent_difference(x, y):
        return 100 * abs(x-y) / x
>>> percent_difference(40, 50)
25.0

到目前为止,局部赋值并没有增强函数定义的表达能力。当与其他控制语句结合时,它就会这样做。此外,局部赋值通过给中间量赋名,在澄清复杂表达式的含义方面也起着关键作用。

1.5.4 条件语句

Python有一个用于计算绝对值的内置函数。

>>> abs(-2)
2

我们希望能够自己实现这样一个函数,但是我们没有明显的方法来定义一个具有比较和选择的函数。我们想表达的是,如果x是正数,abs(x)返回x。而且,如果x是0,abs(x)返回0。否则,abs(x)返回-x。在Python中,我们可以用条件语句来表示这个选择

absolute_value的这种实现引发了几个重要的问题:

条件语句。Python中的条件语句由一系列头文件和套件组成:一个必需的if子句,一个可选的elif子句序列,以及最后一个可选的else子句:

if <expression>:
    <suite>
elif <expression>:
    <suite>
else:
    <suite>

当执行一个条件语句时,每个子句都是按顺序考虑的。接下来是执行条件子句的计算过程。

  1. 求头文件的表达式。

  2. 如果该值为真,则执行该套件。然后,跳过条件语句中所有后面的子句。

如果到达else子句(只有当所有If和elif表达式的值都为false时才会发生),则执行它的套件。

布尔上下文。上面的执行过程提到了“一个假值”和“一个真值”。条件块的头语句中的表达式被称为布尔上下文:它们的真值对控制流很重要,但否则它们的值不会被赋值或返回。Python包含几个假值,包括0、None和布尔值false。所有其他数字都是真值。在第二章中,我们将看到Python中每种内置数据都同时具有真值和假值。

布尔值。Python有两个布尔值,分别为True和False。布尔值表示逻辑表达式中的真值。内置的比较操作>,<,>=,<=,==,!=,返回这些值。

>>> 4 < 2
False
>>> 5 >= 5
True

他的第二个例子读作“5大于等于5”,对应于operator模块中的函数ge。

>>> 0 == -0
True

最后一个例子读取“0 = -0”,对应于operator模块中的eq。请注意,Python区分了赋值(=)和相等比较(==),这是许多编程语言共享的约定。

布尔操作符。Python还内置了三个基本的逻辑运算符:

>>> True and False
False
>>> True or False
True
>>> not False
True

逻辑表达式有相应的求值过程。这些过程利用这样一个事实:有时可以在不计算逻辑表达式的所有子表达式的情况下确定逻辑表达式的真值,这是一种称为短路的特性。

要计算<left>和<right>的表达式:

  1. 计算子表达式<left>。

  2. 如果结果为假值v,则表达式计算为v。

  3. 否则,表达式计算为子表达式<right>的值。

求表达式的值not <exp> :

  1. 求表达式<exp>的值 ;如果结果为false则为True,否则为False。

这些值、规则和操作符为我们提供了一种组合比较结果的方法。 执行比较并返回布尔值的函数通常以is开头,而不是后跟下划线(例如,isfinite, isdigit, isinstance等)。

1.5.5 迭代

除了选择要执行的语句之外,控制语句还用于表示重复。如果我们编写的每一行代码只执行一次,那么编程将是一个非常低效的练习。只有通过重复执行语句,我们才能释放计算机的全部潜力。我们已经看到了一种形式的重复:一个函数可以被多次应用,尽管它只定义了一次。迭代控制结构是多次执行相同语句的另一种机制。

考虑斐波那契数列,其中每个数都是前两个数的和:

0, 1, 1, 2, 3, 5, 8, 13, 21, ...

每个值都是通过反复应用前两项求和规则来构造的。第一个和第二个固定为0和1。例如,第8个斐波那契数是13。

我们可以使用while语句来枚举n个斐波那契数列。我们需要跟踪我们创建了多少个值(k),以及第k个值(curr)及其前身(pred)。逐步查看这个函数并观察斐波那契数列是如何一个接一个地演变到curr的。

记住,在赋值语句中,用逗号分隔多个名称和值。这一行:

pred, curr = curr, pred + curr

具有将名称pred与curr值重新绑定,同时将curr与pred + curr值重新绑定的效果。在进行任何重新绑定之前,=右边的所有表达式都会被求值。

事件的顺序——在更新左边的任何绑定之前评估=右边的所有内容——对于这个函数的正确性至关重要。

while子句包含一个头表达式,后面跟着一个套件:

while <expression>:
    <suite>

执行while子句:

  1. 求头文件的表达式。

  2. 如果该值为真,则执行该套件,然后返回到步骤1。

在步骤2中,在再次计算头表达式之前执行整个while子句。

为了防止a while子句的suite被无限期地执行,该suite应该总是在每次传递时更改一些绑定。

没有终止的while语句称为无限循环。按-C强制Python停止循环。

1.5.6 测试

测试一个函数就是验证函数的行为是否符合期望。 我们的函数语言现在已经足够复杂了,我们需要开始测试我们的实现。

测试是一种系统地执行这种验证的机制。 测试通常采用另一个函数的形式,该函数包含对被测试函数的一个或多个示例调用。 然后根据预期的结果验证返回值。 与大多数通用函数不同,测试涉及选择和验证带有特定参数值的调用。 测试还充当文档的作用:它们演示如何调用函数以及哪些参数值是合适的。

断言。程序员使用assert语句来验证期望,例如被测试函数的输出。assert语句在布尔上下文中有一个表达式,后面跟着带引号的文本行(单引号或双引号都可以,但要一致),如果表达式的计算结果为false,则会显示该文本行。

>>> assert fib(8) == 13, 'The 8th Fibonacci number should be 13'

当断言的表达式计算为真值时,执行assert语句不起作用。当它是一个假值时,assert会导致一个错误,从而停止执行。

fib的测试函数应该测试几个参数,包括n的极值。

>>> def fib_test():
        assert fib(2) == 1, 'The 2nd Fibonacci number should be 1'
        assert fib(3) == 1, 'The 3rd Fibonacci number should be 1'
        assert fib(50) == 7778742049, 'Error at the 50th Fibonacci number'

当在文件中编写Python时,不是直接写入解释器中,测试通常是在同一个文件或后缀为_test.py的相邻文件中编写的。

文档测试。Python提供了一种方便的方法,可以在函数的文档字符串中直接放置简单的测试。文档字符串的第一行应该包含对函数的单行描述,然后是空行。对争论和行为的详细描述可能会随之而来。此外,文档字符串可能包括一个调用该函数的示例交互式会话:

>>> def sum_naturals(n):
        """Return the sum of the first n natural numbers.

        >>> sum_naturals(10)
        55
        >>> sum_naturals(100)
        5050
        """
        total, k = 0, 1
        while k <= n:
            total, k = total + k, k + 1
        return total

然后,可以通过doctest模块验证交互。下面,globals函数返回全局环境的表示,解释器需要它来计算表达式。

>>> from doctest import testmod
>>> testmod()
TestResults(failed=0, attempted=2)

为了只验证单个函数的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

当函数的返回值与预期结果不匹配时,run_docstring_examples函数将把这个问题报告为测试失败。

当在文件中编写Python时,可以通过doctest命令行选项启动Python来运行文件中的所有doctest:

python3 -m doctest <python_source_file>

有效测试的关键是在实现新功能后立即编写(并运行)测试。为了在头脑中有一些示例输入和输出,在实现之前编写一些测试也是一种很好的做法。应用单个函数的测试称为单元测试。详尽的单元测试是良好程序设计的标志。

Last updated

Was this helpful?