资讯专栏INFORMATION COLUMN

SICP Python 描述 1.5 控制

mingzhong / 1007人阅读

摘要:函数体由表达式组成。我们说头部控制语句组。于是,函数体内的赋值语句不会影响全局帧。包含了多种假值,包括和布尔值。布尔值表示了逻辑表达式中的真值。执行测试以及返回布尔值的函数通常以开头,并不带下划线例如等等。返回值之后会和预期结果进行比对。

1.5 控制

来源:1.5 Control

译者:飞龙

协议:CC BY-NC-SA 4.0

我们现在可以定义的函数能力有限,因为我们还不知道一种方法来进行测试,并且根据测试结果来执行不同的操作。控制语句可以让我们完成这件事。它们不像严格的求值子表达式那样从左向右编写,并且可以从它们控制解释器下一步做什么当中得到它们的名称。这可能基于表达式的值。

1.5.1 语句

目前为止,我们已经初步思考了如何求出表达式。然而,我们已经看到了三种语句:赋值、defreturn语句。这些 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 的代码是语句的序列。一条简单的语句是一行不以分号结束的代码。复合语句之所以这么命名,因为它是其它(简单或复合)语句的复合。复合语句一般占据多行,并且以一行以冒号结尾的头部开始,它标识了语句的类型。同时,一个头部和一组缩进的代码叫做子句(或从句)。复合语句由一个或多个子句组成。

: ... : ... ...

我们可以这样理解我们已经见到的语句:

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

def语句是复合语句。def头部之后的组定义了函数体。

为每种头部特化的求值规则指导了组内的语句什么时候以及是否会被执行。我们说头部控制语句组。例如,在def语句的例子中,我们看到返回表达式并不会立即求值,而是储存起来用于以后的使用,当所定义的函数最终调用时就会求值。

我们现在也能理解多行的程序了。

执行语句序列需要执行第一条语句。如果这个语句不是重定向控制,之后执行语句序列的剩余部分,如果存在的话。

这个定义揭示出递归定义“序列”的基本结构:一个序列可以划分为它的第一个元素和其余元素。语句序列的“剩余”部分也是一个语句序列。所以我们可以递归应用这个执行规则。这个序列作为递归数据结构的看法会在随后的章节中再次出现。

这一规则的重要结果就是语句顺序执行,但是随后的语句可能永远不会执行到,因为有重定向控制。

实践指南:在缩进代码组时,所有行必须以相同数量以及相同方式缩进(空格而不是Tab)。任何缩进的变动都会导致错误。

1.5.3 定义函数 II:局部赋值

一开始我们说,用户定义函数的函数体只由带有一个返回表达式的一个返回语句组成。实际上,函数可以定义为操作的序列,不仅仅是一条表达式。Python 复合语句的结构自然让我们将函数体的概念扩展为多个语句。

无论用户定义的函数何时被调用,定义中的子句序列在局部环境内执行。return语句会重定向控制:无论什么时候执行return语句,函数调用的流程都会中止,返回表达式的值会作为被调用函数的返回值。

于是,赋值语句现在可以出现在函数体中。例如,这个函数以第一个数的百分数形式,返回两个数量的绝对值,并使用了两步运算:

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

赋值语句的效果是在当前环境的第一个帧上,将名字绑定到值上。于是,函数体内的赋值语句不会影响全局帧。函数只能操作局部作用域的现象是创建模块化程序的关键,其中纯函数只通过它们接受和返回的值与外界交互。

当然,percent_difference函数也可以写成一个表达式,就像下面这样,但是返回表达式会更加复杂:

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

目前为止,局部赋值并不会增加函数定义的表现力。当它和控制语句组合时,才会这样。此外,局部赋值也可以将名称赋为间接量,在理清复杂表达式的含义时起到关键作用。

新的环境特性:局部赋值。

1.5.4 条件语句

Python 拥有内建的绝对值函数:

>>> abs(-2)
2

我们希望自己能够实现这个函数,但是我们当前不能直接定义函数来执行测试并做出选择。我们希望表达出,如果x是正的,abs(x)返回x,如果x是 0,abx(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
            
>>> absolute_value(-2) == abs(-2)
True

absolute_value的实现展示了一些重要的事情:

条件语句。Python 中的条件语句包含一系列的头部和语句组:一个必要的if子句,可选的elif子句序列,和最后可选的else子句:

if :
    
elif :
    
else:
    

当执行条件语句时,每个子句都按顺序处理:

求出头部中的表达式。

如果它为真,执行语句组。之后,跳过条件语句中随后的所有子句。

如果能到达else子句(仅当所有ifelif表达式值为假时),它的语句组才会被执行。

布尔上下文。上面过程的执行提到了“假值”和“真值”。条件块头部语句中的表达式也叫作布尔上下文:它们值的真假对控制流很重要,但在另一方面,它们的值永远不会被赋值或返回。Python 包含了多种假值,包括 0、None和布尔值False。所有其他数值都是真值。在第二章中,我们就会看到每个 Python 中的原始数据类型都是真值或假值。

布尔值。Python 有两种布尔值,叫做TrueFalse。布尔值表示了逻辑表达式中的真值。内建的比较运算符,><>=<===!=,返回这些值。

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

逻辑表达式拥有对应的求值过程。这些过程揭示了逻辑表达式的真值有时可以不执行全部子表达式而确定,这个特性叫做短路。

为了求出表达式 and

求出子表达式

如果结果v是假值,那么表达式求值为v

否则表达式的值为子表达式

为了求出表达式 or

求出子表达式

如果结果v是真值,那么表达式求值为v

否则表达式的值为子表达式

为了求出表达式not

求出,如果值是True那么返回值是假值,如果为False则反之。

这些值、规则和运算符向我们提供了一种组合测试结果的方式。执行测试以及返回布尔值的函数通常以is开头,并不带下划线(例如isfiniteisdigitisinstance等等)。

1.5.5 迭代

除了选择要执行的语句,控制语句还用于表达重复操作。如果我们编写的每一行代码都只执行一次,程序会变得非常没有生产力。只有通过语句的重复执行,我们才可以释放计算机的潜力,使我们更加强大。我们已经看到了重复的一种形式:一个函数可以多次调用,虽然它只定义一次。迭代控制结构是另一种将相同语句执行多次的机制。

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

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

每个值都通过重复使用“前两个值的和”的规则构造。为了构造第 n 个值,我们需要跟踪我们创建了多少个值(k),以及第 k 个值(curr)和它的上一个值(pred),像这样:

>>> def fib(n):
        """Compute the nth Fibonacci number, for n >= 2."""
        pred, curr = 0, 1   # Fibonacci numbers
        k = 2               # Position of curr in the sequence
        while k < n:
            pred, curr = curr, pred + curr  # Re-bind pred and curr
            k = k + 1                       # Re-bind k
        return curr
>>> fib(8)
13

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

pred, curr = curr, pred + curr

具有将curr的值重新绑定到名称pred上,以及将pred + curr的值重新绑定到curr上的效果。所有=右边的表达式会在绑定发生之前求出来。

while子句包含一个头部表达式,之后是语句组:

while :
    

为了执行while子句:

求出头部表达式。

如果它为真,执行语句组,之后返回到步骤 1。

在步骤 2 中,整个while子句的语句组在头部表达式再次求值之前被执行。

为了防止while子句的语句组无限执行,它应该总是在每次通过时修改环境的状态。

不终止的while语句叫做无限循环。按下-C可以强制让 Python 停止循环。

1.5.6 实践指南:测试

函数的测试是验证函数的行为是否符合预期的操作。我们的函数现在已经足够复杂了,我们需要开始测试我们的实现。

测试是系统化执行这个验证的机制。测试通常写为另一个函数,这个函数包含一个或多个被测函数的样例调用。返回值之后会和预期结果进行比对。不像大多数通用的函数,测试涉及到挑选特殊的参数值,并使用它来验证调用。测试也可作为文档:它们展示了如何调用函数,以及什么参数值是合理的。

要注意我们也将“测试”这个词用于ifwhile语句的头部中作为一种技术术语。当我们将“测试”这个词用作表达式,或者用作一种验证机制时,它应该在语境中十分明显。

断言。程序员使用assert语句来验证预期,例如测试函数的输出。assert语句在布尔上下文中只有一个表达式,后面是带引号的一行文本(单引号或双引号都可以,但是要一致)如果表达式求值为假,它就会显示。

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

当被断言的表达式求值为真时,断言语句的执行没有任何效果。当它是假时,asset会造成执行中断。

fib编写的test函数测试了几个参数,包含n的极限值:

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

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

Doctest。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 run_docstring_examples
>>> run_docstring_examples(sum_naturals, globals())

在文件中编写 Python 时,可以通过以下面的命令行选项启动 Python 来运行一个文档中的所有 doctest。

python3 -m doctest 

高效测试的关键是在实现新的函数之后(甚至是之前)立即编写(以及执行)测试。只调用一个函数的测试叫做单元测试。详尽的单元测试是良好程序设计的标志。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/45498.html

相关文章

  • SICP Python 描述 第二章 使用对象构建抽象 2.1 引言

    摘要:对象表示信息,但是同时和它们所表示的抽象概念行为一致。通过绑定行为和信息,对象提供了可靠独立的日期抽象。名称来源于实数在中表示的方式浮点表示。另一方面,对象可以表示很大范围内的分数,但是不能表示所有有理数。 2.1 引言 来源:2.1 Introduction 译者:飞龙 协议:CC BY-NC-SA 4.0 在第一章中,我们专注于计算过程,以及程序设计中函数的作用。我们看到了...

    phoenixsky 评论0 收藏0
  • SICP Python 描述 第三章 计算机程序的构造和解释 3.1 引言

    摘要:为通用语言设计解释器的想法可能令人畏惧。但是,典型的解释器拥有简洁的通用结构两个可变的递归函数,第一个求解环境中的表达式,第二个在参数上调用函数。这一章接下来的两节专注于递归函数和数据结构,它们是理解解释器设计的基础。 3.1 引言 来源:3.1 Introduction 译者:飞龙 协议:CC BY-NC-SA 4.0 第一章和第二章描述了编程的两个基本元素:数据和函数之间的...

    v1 评论0 收藏0
  • SICP Python 描述 2.7 泛用方法

    摘要:使用消息传递,我们就能使抽象数据类型直接拥有行为。构造器以类似的方式实现它在参数上调用了叫做的方法。抽象数据类型允许我们在数据表示和用于操作数据的函数之间构造界限。 2.7 泛用方法 来源:2.7 Generic Operations 译者:飞龙 协议:CC BY-NC-SA 4.0 这一章中我们引入了复合数据类型,以及由构造器和选择器实现的数据抽象机制。使用消息传递,我们就能...

    leanote 评论0 收藏0
  • SICP Python 描述 3.4 异常

    摘要:的最常见的作用是构造异常实例并抛出它。子句组只在执行过程中的异常产生时执行。每个子句指定了需要处理的异常的特定类。将强制转为字符串会得到由返回的人类可读的字符串。 3.4 异常 来源:3.4 Exceptions 译者:飞龙 协议:CC BY-NC-SA 4.0 程序员必须总是留意程序中可能出现的错误。例子数不胜数:一个函数可能不会收到它预期的信息,必需的资源可能会丢失,或者网...

    pkhope 评论0 收藏0
  • SICP Python描述 1.1 引言

    摘要:另一个赋值语句将名称关联到出现在莎士比亚剧本中的所有去重词汇的集合,总计个。表达式是一个复合表达式,计算出正序或倒序出现的莎士比亚词汇集合。在意图上并没有按照莎士比亚或者回文来设计,但是它极大的灵活性让我们用极少的代码处理大量文本。 1.1 引言 来源:1.1 Introduction 译者:飞龙 协议:CC BY-NC-SA 4.0 计算机科学是一个极其宽泛的学科。全球的分布...

    xumenger 评论0 收藏0

发表评论

0条评论

mingzhong

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<