一、纯函数和非纯函数
纯函数(Pure functions):函数有一些输入(参数)并返回一些输出(调用返回结果)。
>>> abs(-2)
2
可以将内置函数 abs
描述为接受输入并产生输出的小型机器。
abs
就是纯函数,纯函数在调用时除了返回值外不会造成其他任何影响,而且在使用相同的参数调用纯函数时总是会返回相同的值。
非纯函数(Non-pure functions):除了返回值外,调用一个非纯函数还会产生其他改变解释器和计算机的状态的副作用(side effect)。一个常见的副作用就是使用 print
函数产生(非返回值的)额外输出。
我们在python解释器上键入-2和用print(-2)打印出的结果是一样的,输入字符串也会得到同样的结果,这是为什么呢?
而我们通过两种方式打印None会有不同的效果,因为python有自动显示键入的任何表达式的规则,None是特殊的,但是我们可以用print(None)来让它出现。
我们还可以打印多个用空格分割的值。
>>> print(1, 2, 3)
1 2 3
虽然 print
和 abs
在这些例子中看起来很相似,但它们的工作方式基本不同。print
返回的值始终为 None
,这是一个不代表任何内容的特殊 Python 值。而交互式 Python 解释器并不会自动打印 None
值,所以 print
函数的额外输出就是它的副作用。
下面这个调用 print
的嵌套表达式就展示了非纯函数的特征。
>>> print(print(1), print(2))
1
2
None None
如果你发现这个输出结果出乎你的意料,可以画一个表达式树来解释求解该表达式会产生特殊输出的原因。
小心使用 print
函数!它返回 None
意味着它不应该用于赋值语句。
>>> two = print(2)
2
>>> print(two)
None
纯函数不能有副作用,或是随着时间推移的改变的限制,但是对其施加这些限制会产生巨大的好处。首先,纯函数可以更可靠地组成复合调用表达式。在上面的示例中可以看到在操作数表达式中使用非纯函数 print
并不能返回有用的结果,但另一方面,我们已经看到 max, pow, sqrt
等函数可以在嵌套表达式中有效使用。
第二,纯函数往往更易于测试。相同的参数列表会返回相同的值,我们可以将其与预期的返回值进行比较。本章后面将更详细地讨论测试。
二、运算符
数学运算符(例如 + 和 -)为我们提供了组合方法的第一个示例,但我们尚未给包含这些运算符的表达式定义求值过程。
带有中缀运算符的 Python 表达式都有自己的求值过程,但你通常可以将它们视为调用表达式的简写形式。当你看到
>>> 2 + 3
5
可以认为简单地将它理解为以下代码
>>> add(2, 3)
5
中缀表示法可以嵌套,就像调用表达式一样。Python 运算符优先级采用了正常数学规则,它规定了如何求解具有多个运算符的复合表达式。
>>> 2 + 3 * 4 + 5
19
它和以下表达式的求解结果完全相同
>>> add(add(2, mul(3, 4)), 5)
19
调用表达式中的嵌套比运算符版本更加明显,但也更难以阅读。Python 还允许使用括号对子表达式进行分组,用以覆盖正常的优先级规则,或使表达式的嵌套结构更加明显。
>>> (2 + 3) * (4 + 5)
45
它和以下表达式的求解结果完全相同
>>> mul(add(2, 3), add(4, 5))
45
对于除法,Python 提供了两个中缀运算符:/
和 //
。前者是常规除法,因此即使除数可以整除被除数,它也会产生 浮点数(十进制小数):
>>> 5 / 4
1.25
>>> 8 / 4
2.0
而后一个运算符 //
会将结果向下舍入到一个整数:
>>> 5 // 4
1
>>> -5 // 4
-2
这两个运算符算是 truediv
和 floordiv
函数的简写。
>>> from operator import truediv, floordiv
>>> truediv(5, 4)
1.25
>>> floordiv(5, 4)
1
你可以在程序中随意使用中缀运算符和圆括号。对于简单的数学运算,Python 惯例上更喜欢使用运算符而不是调用表达式。
python还可以从一个函数中返回多个值。假设我们想要商和余数。
我们还可以将python代码写入文件中便于永久保存。我们用VScode演示。
或者我们可以不在代码中写输出语句,而在交互式python中运行来看我们的输出。
三、文档
函数定义通常包括描述函数的文档,称为“文档字符串 docstring”,它必须在函数体中缩进。文档字符串通常使用三个引号,第一行描述函数的任务,随后的几行可以描述参数并解释函数的意图:
>>> def pressure(v, t, n):"""计算理想气体的压力,单位为帕斯卡使用理想气体定律:http://en.wikipedia.org/wiki/Ideal_gas_lawv -- 气体体积,单位为立方米t -- 绝对温度,单位为开尔文n -- 气体粒子"""k = 1.38e-23 # 玻尔兹曼常数return n * k * t / v
当你使用函数名称作为参数调用 help
时,你会看到它的文档字符串(键入 q 以退出 Python help)。
>>> help(pressure)
编写 Python 程序时,除了最简单的函数之外,都要包含文档字符串。要记住,虽然代码只编写一次,但是会在之后阅读多次。Python 文档包含了 文档字符串准则,它会在不同的 Python 项目中保持一致。
注释:Python 中的注释可以附加到 #
号后的行尾。例如,上面代码中的注释 玻尔兹曼常数
描述了 k
变量的含义。这些注释不会出现在 Python 的 help
中,而且会被解释器忽略,它们只为人类而存在。
四、参数默认值
定义通用函数的结果是引入了额外的参数。具有许多参数的函数可能调用起来很麻烦并且难以阅读。
在 Python 中,我们可以为函数的参数提供默认值。当调用该函数时,具有默认值的参数是可选的。如果未提供,则将默认值绑定到形参上。例如,如果程序通常用于计算“一摩尔”粒子的压力,则可以提供此值作为默认值:
>>> def pressure(v, t, n=6.022e23):"""计算理想气体的压力,单位为帕斯卡使用理想气体定律:http://en.wikipedia.org/wiki/Ideal_gas_lawv -- 气体体积,单位为立方米t -- 绝对温度,单位为开尔文n -- 气体粒子,默认为一摩尔"""k = 1.38e-23 # 玻尔兹曼常数return n * k * t / v
=
符号在此示例中表示两种不同的含义,具体取决于使用它的上下文。在 def 语句中,=
不执行赋值,而是指示调用 pressure
函数时使用的默认值。相比之下,函数体中对 k
的赋值语句中将名称 k
与玻尔兹曼常数的近似值进行了绑定。
>>> pressure(1, 273.15)
2269.974834
>>> pressure(1, 273.15, 3 * 6.022e23)
6809.924502
pressure
函数的定义接收三个参数,但上面的第一个调用表达式中只提供了两个。在这种情况下,n
的值取自 def
语句中的默认值。如果提供了第三个参数,默认值将被忽略。
作为准则,函数主体中使用的大多数数据值都应该表示为具名参数(named arguments)的默认值,这样会使它们更易于检查,并且可以被函数调用者更改。一些永远不会改变的值,例如基本常量 k
可以绑定在函数体或全局帧中。
五、控制
5.1、语句
到目前为止,我们虽然主要思考的是如何计算求解表达式,但我们已经见过了三种语句:赋值(assignment)、 def
和 return
语句。尽管这些 Python 代码都包含表达式作为它们的一部分,但它们本身并不是表达式。
语句不会被求解,而会被执行。每个语句都描述了对解释器状态的一些更改,并且执行语句就会应用该更改。正如我们在 return
和赋值语句中看到的那样,执行语句可能涉及求解其包含的子表达式。
表达式也可以作为语句执行,在这种情况下,它们会被求值,但它们的值会被丢弃。执行纯函数没有效果,但执行非纯函数会因为调用函数而产生效果。
思考一下,例如:
>>> def square(x):mul(x, x) # 小心!此调用不返回值。
这个例子是有效的 Python 代码,但可能不能达到预期。函数体由一个表达式组成。表达式本身是一个有效的语句,但语句的效果是调用 mul
函数,然后把结果丢弃。如果你想对表达式的结果做些什么,你需要用赋值语句存储它或用 return
语句返回它:
>>> def square(x):return mul(x, x)
有时,在调用 print
等非纯函数时,拥有一个主体为表达式的函数确实有意义。
>>> def print_square(x):print(square(x))
在最高层级上,Python 解释器的工作是执行由语句组成的程序。然而,很多有趣的计算工作都来自对表达式的求值。语句用来管理程序中不同表达式之间的关系,以及它们产生的结果。
5.2、复合语句
通常,Python 代码是一系列语句。简单语句是不以冒号结尾的单行,而由其他语句(简单语句和复合语句)组成被称为复合语句。复合语句通常跨越多行,以单行头部(header)开始,并以冒号结尾,其中冒号标识语句的类型。头部和缩进的句体(suite)一起称为子句。复合语句由一个或多个子句组成:
<header>:<statement><statement>...
<separating header>:<statement><statement>...
...
我们可以用这些术语来理解我们之前介绍过的语句。
- 表达式、返回语句和赋值语句都是简单语句。
def
语句是复合语句,def
头后面的句体定义了函数体。
对每类 header 都有专门的求值规则来规定其何时执行以及是否执行其句体中的语句。我们说“the header controls its suite”,例如,在 def
语句中,return
表达式不会立即求值,而是存储起来供以后调用该函数时使用。
我们现在也可以理解多行程序了。
- 要执行一系列语句,会先执行第一个语句。如果该语句不重定向控制,则继续执行语句序列的其余部分(如果还有的话)。
这个定义揭示了递归定义序列(sequence)的基本结构:一个序列可以分解成它的第一个元素和其余元素。语句序列的“其余部分”本身也是语句序列!因此,我们可以递归地应用这个执行规则。这种将序列视为递归的数据结构的观点将在后面的章节中再次出现。
此规则的重要结论是语句会按顺序执行,但由于重定向控制(redirected control),后面的语句可能永远不会被执行到。
实践指南:缩进句体时,所有行必须以相同的方式缩进相同的量(使用空格,而不是制表符)。缩进的任何变化都会导致错误。
5.3、条件语句
Python 有一个用于计算绝对值的内置函数。
>>> abs(-2)
2
我们希望能够自己实现这样一个函数,但是没有清晰的方法来定义一个具有比较和选择的函数。我们想表达的是,如果 x
为正,则 abs(x)
返回 x
;此外,如果 x
为 0,则 abs(x)
返回 0;否则,abs(x)
返回 -x
。在 Python 中,我们可以用条件语句来表达这种选择。
这个 absolute_value
函数的实现提出了几个重要的问题:
条件语句(Conditional statement):Python 中的条件语句由一系列头部和句体组成:必需的 if
子句、可选的 elif
子句序列,最后是可选的 else
子句:
if <expression>:<suite>
elif <expression>:<suite>
else:<suite>
执行条件语句时,每个子句都会按顺序被考虑。执行条件子句的计算过程如下。
- 求解头部的表达式
- 如果它是真值,则执行该句体。然后,跳过条件语句中的所有后续子句。
如果到达 else
子句(仅当所有 if
和 elif
表达式的计算结果为假值时才会发生),则执行其句体。
布尔上下文(Boolean contexts):上面,执行过程提到了“假值 a false value”和“真值 a true value”。条件块头部语句内的表达式被称为布尔上下文:它们值的真假对控制流很重要,另外,它们的值不会被赋值或返回。Python 包含多个假值,包括 0、 None
和布尔值 False
,所有其他数字都是真值。在第二章中,我们将看到 Python 中的每种内置数据都具有真值和假值。
布尔值(Boolean values):Python 有两个布尔值,分别叫做 True
和 False
。布尔值表示逻辑表达式中的真值。内置的比较运算符 >, <, > =, <=, ==, != 会返回这些值。
>>> 4 < 2
False
>>> 5 >= 5
True
第二个例子读作“5 大于或等于 5”,对应于 operator
模块中的函数 ge
。
>>> 0 == -0
True
最后一个示例读作“0 等于 -0”,对应于 operator
模块中的 eq
。请注意,Python 会区分赋值符号 =
与相等比较符号 ==
,这也是许多编程语言共享的约定。
布尔运算符(Boolean operators):Python 中还内置了三个基本的逻辑运算符:
>>> True and False
False
>>> True or False
True
>>> not False
True
逻辑表达式具有相应的求值过程。而这些过程利用了这样一个理论 --> 有时,逻辑表达式的真值可以在不对其所有子表达式求值的情况下确定,这一特性称为短路(short-circuiting)。
求解表达式 <left> and <right>
的步骤如下:
- 求解子表达式
<left>
。 - 如果左边的结果为假值 v,则表达式的计算结果就是 v。
- 否则,表达式的计算结果为子表达式
<right>
的值。
求解表达式 <left> or <right>
的步骤如下:
- 求解子表达式
<left>
。 - 如果左边的结果为真值 v,则表达式的计算结果就是 v。
- 否则,表达式的计算结果为子表达式
<right>
的值。
求解表达式 not <exp>
的步骤如下:
- 求解
<exp>
,如果结果为假值,则值为True
,否则为False
。
这些值、规则和运算符为我们提供了一种组合比较结果的方法。执行比较并返回布尔值的函数通常以 is
开头,后面不跟下划线(例如 isfinite, isdigit, isinstance
等)。
5.4、迭代
除了选择要执行的语句外,控制语句还用于重复。如果我们编写的每一行代码只执行一次,那么编程将是一项非常低效的工作。只有通过重复执行语句,我们才能释放计算机的全部潜力。我们之前已经见过了一种重复形式:一个函数只用定义一次,就可以被多次调用。迭代控制(Iterative control)结构是另一种多次执行相同语句的机制。
思考斐波那契数列,其中每个数都是前两个数的和:
0,1,1,2,3,5,8,13,21,⋯
每个值都是通过重复应用 sum-previous-two
的规则构建的,第一个和第二个值固定为 0 和 1。
我们可以使用 while
语句来枚举 n 项斐波那契数列。我们需要跟踪已经创建了多少个值(k
),和第 k 个值(curr
)及其前身(pred
)。单步执行此函数并观察斐波那契数如何一个一个地演化,并绑定到 curr。
请记住,单行赋值语句可以用逗号分隔多个名称和值同时赋值。该行:
pred, curr = curr, pred + curr
将名称 pred
重新绑定到 curr
的值,同时将 curr
重新绑定到 pred + curr
的值。所有 =
右侧的所有表达式都会在绑定之前计算出来。
在更新左侧的绑定之前求出所有 =
右侧的内容 --> 这种事件顺序对于此函数的正确性至关重要。
while
子句包含一个头部表达式,后跟一个句体:
while <expression>:<suite>
要执行 while
子句:
- 求解头部的表达式。
- 如果是真值,则执行后面的句体,然后返回第 1 步。
在第 2 步中,while
子句的整个句体在再次计算头部表达式之前执行。
为了防止 while
子句的句体无限期地执行,句体应该总是在每次循环中更改一些绑定。
不会终止的 while
语句被称为无限循环(infinite loop)。按 <Control>-C
可以强制 Python 停止循环。
接下来我们可以用迭代来做一个小测试:求任意正整数的素数因式分解(分解质因数:每个正整数n都有一组素因子,正整数可以由它们的乘积表示,关键在于如何找到一组质数,将它们相乘得到该正整数) - 求n的最小素因子,除以它,再对n除以质因数得到的数再做一次。
我们还可以将代码修改一下,利用循环嵌套,在一个大循环里嵌套循环就可以运行我们的程序,或者说让我们的程序都在一个函数定义里运行。