Lecture 3. Control
Python 中的打印与求值
本部分讨论了在Python中打印与表达式求值之间的区别,以及如何理解特殊值 None
的显示行为。
表达式求值与打印
在Python交互式解释器中,直接输入一个表达式,如数字 -2
或字符串 'go bears'
,解释器会显示表达式的值。这种情况下,字符串显示时会包括引号,显示的是字符串字面量的确切形式。相反,使用 print
函数打印同一个字符串时,输出结果中不包含引号,显示的是字符串的实际内容。
None
的特殊行为
None
在Python中用来表示空或无值的情况。如果直接在解释器中求值 None
,由于它代表“无”,所以默认不会显示任何东西。但是,如果使用 print
函数打印 None
,则会在控制台上显示 “None” 字样。这揭示了 print
和直接求值之间的一个关键区别:print
能够显式地显示 None
,而直接求值时 None
被视为不显示。
函数返回 None
在Python中,如果一个函数没有显式地返回一个值,它会默认返回 None
。例如,一个只计算但不返回值的函数,如计算 x 的平方但不使用 return
语句,将返回 None
。这种情况下,虽然操作被执行了(如计算了 x 的平方),但由于没有返回值,所以调用此函数的结果是 None
。
打印与副作用
print
函数不仅可以打印多个值(包括 None
),而且还支持连续打印操作。例如,嵌套 print
调用将展示如何在函数调用中处理 None
。当 print
被嵌套调用时,外部的 print
尝试打印内部 print
的结果,因为内部的 print
返回 None
,所以最终输出的是 “None None”。
纯函数与非纯函数
在Python中,函数分为纯函数和非纯函数。纯函数(如 abs
和 pow
)仅基于输入计算并返回值,没有副作用。非纯函数(如 print
),除了可能有返回值外(通常是 None
),还有副作用,如输出到控制台。这种副作用是函数的一部分行为,不是返回的值,而是由函数调用引起的行为改变。
Python中的嵌套打印调用和执行环境
本部分将探讨Python中嵌套打印调用的行为及其对表达式求值的影响,并解释用户定义函数的执行环境。
嵌套打印调用的解析
在Python中执行嵌套的 print
调用如 print(print(1), print(2))
时,会出现一系列的输出和求值步骤:
- 第一个
print(1)
调用:执行时,首先打印数字1
(这是一个副作用),然后返回None
。 - 第二个
print(2)
调用:同样打印数字2
,返回None
。 - 外层的
print(print(1), print(2))
调用:接收到两个None
作为参数,导致打印出 “None None”。
这个过程说明了 print
函数的副作用(即打印输出)和它的返回值(始终是 None
)。解释器不会在最终的表达式求值结果为 None
时自动打印它。
用户定义函数与执行环境
讨论用户定义函数时,理解Python的执行环境非常关键。以下是创建和调用用户定义函数的步骤:
- 定义 (
def
) 语句:通过def
语句定义函数,指定函数名和形参。函数体包含在定义之后的缩进块中。 - 函数调用:调用一个函数时,Python首先评估调用表达式中的操作数和操作符,然后将实际参数传递给函数。
- 环境框架:每次函数调用时,Python都会创建一个新的环境框架,其中形参被绑定到传入的实际参数上。
- 函数体执行:在新的环境中,执行函数体,返回计算结果。
例如,定义一个名为 square
的函数,计算输入值的平方,然后通过传入参数(如 2+2
)调用此函数,最终返回计算结果 16
。
函数调用与值的传递
在Python中,函数的调用涉及到值的传递,即将表达式的结果(如 2+2
的结果 4
)传递到函数中。这种机制支持了函数的封装性和独立性,允许函数在隔离的环境中执行操作,而不影响全局环境。
Python中函数调用和执行环境的复杂例子
本部分探讨了如何通过嵌套函数调用来理解Python中的执行环境和作用域。
嵌套函数调用过程
在Python中,我们可以通过嵌套调用同一个函数来进行更复杂的计算,例如计算 square(square(3))
。这里的流程包括:
- 第一层函数调用:首先计算
square(3)
。这涉及到:- 创建一个新的执行环境(帧)。
- 将形式参数
x
绑定到实际参数3
。 - 执行函数体,计算
x * x
得到9
作为返回值。
- 第二层函数调用:使用第一次调用的结果
9
来调用square(9)
,重复上述过程,得到最终结果81
。
执行环境和帧
在每次函数调用中,Python都会创建一个新的帧来存储局部变量和参数。每个帧都是独立的,并根据传入的参数进行不同的处理:
- 帧标识:每个帧都有一个独特的标识(例如 F1, F2),以区分不同的函数调用实例。
- 参数绑定:每次调用都会将形式参数绑定到实际传入的参数上,这决定了函数体内部的计算方式。
多环境结构
Python中的执行环境可以由多个帧组成,其中包括:
- 全局帧:存储全局变量和函数定义。
- 局部帧:每次函数调用时创建,存储局部变量和参数。
在执行嵌套函数调用时,Python会维护一个由多个帧构成的环境链,每个帧都可以访问到它之前的帧中定义的变量和函数。
名称解析和作用域
在这种多环境结构中,变量名的解析遵循从内到外的顺序,即先从当前帧查找,如果未找到,则继续在上一层帧中查找,直至全局帧。这一过程确保了名称在各自的作用域内有明确的绑定和解析,从而支持了Python的作用域规则和变量隐藏(shadowing)现象。
Python 函数执行和环境的复杂示例
本部分进一步探讨了Python中的函数调用、执行环境和名称解析。
名称在不同环境中的含义
在Python中,相同的名称在不同的环境中可以有不同的含义。这是因为每个执行帧可以为相同的名称绑定不同的值。例如,函数的调用表达式和函数体被调用时,都在不同的环境中评估。
函数参数与局部变量的区别
在一个示例中,函数名称 square
被用作函数本身的名称及其形式参数。尽管这样做不推荐,但这有效地展示了当函数被调用时,参数名称如何在局部帧中覆盖全局定义。例如,调用 square(4)
实际上会在函数的局部环境中将 square
绑定到数字 4
,而不是函数本身。
函数执行的环境切换
当 square(4)
被调用时,首先在全局环境中解析函数名称 square
,然后创建一个新的局部帧执行函数体。在这个局部帧中,形式参数 square
被绑定到实际参数 4
。当函数体内部的 square
被求值时,它指向的是参数 4
而不是全局的函数。
多环境结构的重要性
Python中的多环境结构允许函数调用在隔离的环境中执行,保证了函数执行的独立性和变量作用域的控制。每次函数调用都可能创建一个新的局部环境,这些环境形成了一个包含全局环境的环境链。
环境链和名称解析
每个环境由一系列帧组成,其中每个帧可能包含变量的绑定。解析一个名称时,Python从当前帧开始,向上通过环境链查找,直到找到名称的绑定或达到全局环境。这确保了在最接近的环境中找到最具体的绑定。
通过理解Python中的函数调用、执行环境、名称解析和环境链,开发者可以更好地管理和预测程序的行为,尤其是在涉及复杂作用域和多函数调用的情况下。这有助于编写更为可靠和可维护的代码,同时避免常见的编程错误。
条件语句及其结构
本部分讨论了Python中的条件语句的使用和其基本结构,这对于编写根据不同条件执行不同操作的函数至关重要。
语句和复合语句
在Python中,语句是解释器执行的一段代码,用于执行某些操作,如赋值、定义函数等。复合语句包括一个或多个子语句(称为“套件”),这些子语句依据特定的控制结构来执行。每个复合语句都以一个头部开始,后跟一个或多个由缩进的套件组成的序列。
条件语句的构造
条件语句允许程序根据表达式的真值来选择性地执行代码块。基本的条件语句由以下部分组成:
if
子句:这是条件语句的开始,包含一个条件表达式。elif
子句(可选):代表“else if”,用于链式的条件判断。else
子句(可选):在所有先前条件均不满足时执行。
执行条件语句的规则
条件语句的执行基于以下规则:
- 依次评估每个子句的头部表达式。
- 如果表达式的值为真,则执行相应的套件,并跳过剩余的子句。
- 如果表达式为假,则继续评估下一个条件表达式(如果有的话)。
布尔环境和表达式评估
条件语句中的表达式被视为布尔环境,这意味着表达式的值只用于判断真或假。Python中定义了哪些值为假(如False
、0
、空字符串''
、None
等),而所有其他的值都被视为真。
条件语句的应用实例
通过定义一个计算绝对值的函数,展示了如何使用条件语句。这个函数检查一个数字是否小于、等于或大于零,并根据这些条件返回相应的值。这不仅是对条件语句功能的展示,也是对如何在Python中执行基本算术和逻辑操作的基础教程。
条件语句的重要性
理解并能正确使用条件语句是编程中的一个基本技能,它允许程序根据不同的运行时条件做出反应,从而执行更复杂的任务和决策。通过掌握这些结构,程序员可以编写更灵活和强大的代码,更好地控制程序的行为。
布尔运算符(Boolean operators)在编程中至关重要,因为它们允许我们对逻辑条件进行评估和决策。以下是对布尔运算符及其实际应用的深入讲解,不仅限于 Python 语言。
布尔运算符
在编程中,布尔运算符主要包括 and
、or
和 not
。这些运算符的行为如下:
and
运算符:- 表达式
True and False
结果为False
。 and
运算符在左侧表达式为False
时,会短路(short-circuit),即不会计算右侧表达式,因为结果已经确定为False
。
求解过程:
<left> and <right> #1. 计算 <left>。 #2. 如果 <left> 为假值,则表达式结果为 <left>。 #3. 否则,表达式结果为 <right>。
- 表达式
or
运算符:- 表达式
True or False
结果为True
。 or
运算符在左侧表达式为True
时,会短路,即不会计算右侧表达式,因为结果已经确定为True
。
求解过程:
<left> or <right> #1. 计算 <left>。 #2. 如果 <left> 为真值,则表达式结果为 <left>。 #3. 否则,表达式结果为 <right>。
- 表达式
not
运算符:- 表达式
not False
结果为True
。 not
运算符用于反转布尔值。
求解过程:
not <exp> #1. 计算 <exp>。 #2. 如果 <exp> 为假值,则结果为 `True`。 #3. 否则,结果为 `False`。
- 表达式
实际应用
条件判断
布尔运算符广泛应用于条件判断中,用于控制程序流。例如:
# Python 示例
age = 18
if age >= 18 and age < 21:
print("You are an adult, but not old enough to drink in some countries.")
在上述示例中,and
运算符确保两个条件都为真时才执行 print
语句。
循环控制
布尔运算符在循环控制中也很常见,例如在某些条件下提前终止循环:
# Python 示例
numbers = [1, 2, 3, 4, 5]
for number in numbers:
if number % 2 == 0 or number == 5:
print(f"{number} is either even or equals to 5.")
break # 短路
or
运算符在 number % 2 == 0
为真时会短路,不再检查 number == 5
。
逻辑判断
在某些复杂逻辑判断中,使用 not
运算符反转条件:
# Python 示例
is_valid = False
if not is_valid:
print("The input is not valid.")
短路(Short-circuiting)
短路是一种优化,它避免了不必要的计算,从而提高了程序的效率。例如:
# Python 示例
def expensive_operation():
print("This is an expensive operation.")
return True
# 短路示例
result = False and expensive_operation() # 不会调用 expensive_operation
在这个示例中,由于 False and ...
已经确定结果为 False
,所以 expensive_operation
不会被调用,从而节省了计算资源。
常用布尔函数
在实际编程中,常用的布尔函数通常以 is
开头,例如:
isfinite(x)
:判断x
是否为有限数。isdigit()
:判断字符串是否仅包含数字。isinstance(obj, class)
:判断obj
是否是class
的实例。
这些函数帮助我们进行具体的布尔判断,从而结合布尔运算符实现复杂的逻辑控制。
布尔运算符及其短路特性在编程中具有广泛的应用,从简单的条件判断到复杂的逻辑控制。理解它们的求值过程和实际应用,对于编写高效和可读的代码至关重要。
1.5.5 迭代 - Python中的迭代和while语句
本部分详细讨论了Python中使用 while
语句进行迭代计算的过程和规则。
迭代的基本概念
迭代指的是重复执行某个过程多次,直到满足特定条件。在编程中,迭代是一种基本的构建块,用于执行重复任务。
使用while语句进行迭代
while
语句是Python中实现迭代的一种方法。它是一个复合语句,包含一个条件表达式和一个或多个需要重复执行的语句(称为循环体)。
while语句的结构和执行规则
以下是while
语句的基本结构和执行步骤:
- 条件判断:
while
语句以一个条件表达式开始。如果条件为真(true),则执行循环体内的语句。 - 执行循环体:循环体内的语句被执行。
- 重复执行:执行完循环体后,解释器返回到条件表达式进行再次判断。如果条件仍为真,则重复执行循环体。这个过程一直持续到条件表达式为假(false)。
示例:计算数字总和
通过一个简单的示例来说明while
语句的用法:
- 初始设置变量
i
和total
都为0。 - 使用
while
语句累加数字1到3。条件表达式为i < 3
。 - 在每次迭代中,
i
递增1,total
增加当前的i
值。
循环过程详解
在上述示例中,每次循环迭代都遵循以下步骤:
- 评估条件:检查
i < 3
是否成立。 - 执行循环体:如果条件为真,增加
i
的值,更新total
的值。 - 回到条件评估:完成循环体后,再次检查条件,如果为真,则继续执行循环体。如果为假,结束循环。
循环的结束
当 i
增加到3时,条件 i < 3
不再成立,循环结束。此时,i
的值为3,total
的值为6(1+2+3的总和)。
使用While语句计算素数分解
本部分讲解了如何利用 while
语句在Python中找到任何正整数的素数分解,这是一个更复杂的例子,展示了 while
语句在解决实际问题中的应用。
素数分解的定义
素数分解指的是将一个正整数 ( n ) 分解为几个素数的乘积,这些素数的乘积等于原始的数 ( n )。每个正整数都有一组唯一的素数分解(忽略因数的顺序)。
素数分解的计算方法
为了找到一个数的素数分解,可以使用以下步骤:
- 寻找最小的素数因子:从最小的素数开始,找到可以整除给定数字 ( n ) 的素数。
- 除以素数因子:将 ( n ) 除以这个素数因子,得到的商作为新的 ( n )。
- 重复过程:重复以上两步,直到新的 ( n ) 为 1。
使用While语句实现
通过 while
语句实现上述过程:
- 初始化 ( n ) 为需要分解的数。
- 使用
while
语句重复查找最小的素数因子,直到 ( n ) 减小到 1。 - 在每次迭代中,打印出找到的素数因子,并更新 ( n )。
函数定义和实现
prime_factor 函数
这个函数的目的是打印出一个整数 n
的所有素数因子,直到所有的因子都被找出来。函数采用了一个 while
循环,持续进行直到 n
降至 1。
def prime_factor(n):
"""Print the prime factor of n in non_decreasing order
>>> prime_factor(8)
2
2
2
>>> prime_factor(11)
11
>>> prime_factor(858)
2
3
11
13
"""
while n > 1:
k = smallest_prime_factor(n) # 调用 smallest_prime_factor 函数获取最小素数因子
print(k) # 打印这个素数因子
n //= k # 更新 n,除以当前的素数因子,进行下一轮循环
- 功能:通过持续找出并打印整数
n
的最小素数因子,直到n
降至 1。 - 过程:使用
while
循环检查n > 1
条件,然后找到n
的最小素数因子(通过smallest_prime_factor
函数),打印该因子,并用这个因子去除n
更新其值。
smallest_prime_factor 函数
此函数用于找出给定整数 n
的最小素数因子。它通过从 2 开始逐一尝试直到找到可以整除 n
的最小整数。
def smallest_prime_factor(n):
i = 2 # 从最小的素数开始
while n % i != 0:
i += 1 # 如果当前 i 不能整除 n,增加 i
return i # 返回可以整除 n 的最小整数 i
- 功能:找到并返回整数
n
的最小素数因子。 - 过程:从 2 开始,逐一检查每个整数是否能整除
n
,一旦找到能整除的整数,立即返回。
整合与测试
将上述代码整合到Python脚本中,并进行测试,以确保它可以正确地为各种整数找到素数分解。可以通过实际的输入和预期输出来验证函数的正确性。
测试一个函数就是去验证函数的行为是否符合预期。随着函数变得越来越复杂,测试变得尤为重要。测试是通过系统地执行验证机制来确保函数的功能实现正确。以下是对测试内容的详细讲解:
什么是测试?
测试是一种系统地执行验证的机制。通常,测试采用函数的形式,包含对被测试函数的多个调用样例,并根据预期结果验证其返回值。测试不仅验证函数的正确性,还可以作为文档来演示如何调用函数以及选择合适的参数值。
断言(Assertions)
程序员使用 assert
语句来验证被测试函数的输出是否符合预期。assert
语句包含一个布尔表达式和一个错误消息字符串,如果表达式的结果为假值,assert
会导致错误,使程序停止执行。
示例:
def fib(n):
# 假设这是计算第 n 个斐波那契数的函数
pass
# 断言示例
assert fib(8) == 13, '第八个斐波那契数应该是 13'
在上例中,如果 fib(8)
的返回值不是 13
,程序将抛出错误,并显示消息 '第八个斐波那契数应该是 13'
。
测试函数
一个测试函数应该包含对被测试函数的多个调用,并使用断言来验证结果。例如:
def fib_test():
assert fib(2) == 1, '第二个斐波那契数应该是 1'
assert fib(3) == 1, '第三个斐波那契数应该是 1'
assert fib(50) == 7778742049, '第五十个斐波那契数发生错误'
文档测试(Doctests)
Python 提供了一种在函数的文档字符串中嵌入测试的方便方法。这些测试可以通过 doctest
模块自动运行。
示例:
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
运行 Doctests
可以通过 doctest
模块来验证文档字符串中的交互式示例:
from doctest import testmod
testmod()
这将运行所有嵌入在文档字符串中的测试,并报告结果。例如:
TestResults(failed=0, attempted=2)
运行单个函数的 Doctest
为了验证单个函数的文档测试,可以使用 run_docstring_examples
函数:
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
命令行运行 Doctests
当在文件中编写 Python 代码时,可以通过命令行运行所有的 doctest:
python3 -m doctest <python_source_file>
单元测试(Unit Tests)
单元测试是指对单个函数进行测试。详尽的单元测试是良好程序设计的标志。有效测试的关键是在实现新功能后立即编写(并运行)测试。在实现之前编写一些测试也是一种良好的做法,这样可以在你的脑海中有一些示例输入和输出。
总结起来,测试不仅是验证代码正确性的重要手段,也是文档和示例的良好补充。通过断言、文档测试和单元测试,可以系统地确保代码行为符合预期。