1.2 编程要素
INFO
编程语言不只是指挥计算机干活的工具。它还是一个框架,让我们把关于计算过程的想法组织起来。程序也是在编程社区里传达这些想法的载体。所以,程序首先是写给人读的,让机器执行只是顺带。
描述一门语言时,要特别留意它提供了哪些机制,能把简单的想法组合成更复杂的想法。凡是强大的语言,都有三种这样的机制:
- 基本表达式和语句(primitive expressions and statements):语言提供的最简单的构建模块
- 组合方法(means of combination):把简单元素搭成复合元素
- 抽象方法(means of abstraction):给复合元素命名,把它们当成一个整体来用
编程里我们打交道的元素只有两种:函数和数据(很快我们就会发现,它们其实没那么泾渭分明)。简单说:数据是我们想操作的材料,函数则是操作数据的「规则」。所以,任何强大的编程语言,都得能描述基本的数据和函数,还要有组合、抽象它们的手段。
1.2.1 表达式
上一节里,我们试用了完整的 Python 解释器;现在,我们从头开始,按部就班地一个一个讲 Python 语言的要素。如果例子看着太简单,请耐心点——更精彩的内容马上就来。
我们从基本表达式讲起。有一种基本表达式就是数字。更准确地说,你输入的表达式,是由表示这个数的十进制数字组成的。
>>> 42
42表示数字的表达式可以和数学运算符结合,构成复合表达式,解释器会求它的值:
>>> -1 - -1
0
>>> 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 + 1/128
0.9921875这些数学表达式用的是中缀表示法(infix notation):运算符(operator,比如 +、-、*、/)夹在操作数(operand,也就是数字)中间。Python 构造复合表达式的方式很多。与其一下子全列出来,不如边学边引入新的表达式形式,以及它们对应的语言特性。
1.2.2 调用表达式
最重要的复合表达式是调用表达式(call expression),它把函数作用到一些参数上。回想代数里的定义:函数就是把若干输入参数映射成一个输出值。比如 max 函数,把多个输入映射成其中最大的那个输出。Python 表达函数调用的方式,和传统数学一样。
>>> max(7.5, 9.5)
9.5这个调用表达式包含子表达式(subexpressions):运算符是一个表达式,位于括号前面;括号里是一串用逗号隔开的操作数表达式(operand expressions)。

运算符指定了一个函数。求这个调用表达式的值时,我们就说:用参数 7.5 和 9.5 调用函数 max,它返回 9.5。
参数在调用表达式中的顺序很重要。比如 pow 函数,以第二个参数为指数,对第一个参数求幂:
>>> pow(100, 2)
10000
>>> pow(2, 100)
1267650600228229401496703205376相比传统的中缀表示法,函数表示法有三个主要优点。首先,函数可以接受任意数量的参数:
>>> max(1, -2, 3, -4)
3不会产生歧义,因为函数名永远排在参数前面。
其次,函数表示法能直接扩展到嵌套表达式(nested expressions)——元素本身也是复合表达式。和中缀复合表达式不同,嵌套调用表达式的结构,在括号里一目了然。
>>> max(min(1, -2), min(pow(3, 5), -4))
-2原则上,嵌套可以有多深、表达式能有多复杂,都没有上限,Python 解释器都能求值。但人很快就会被多层嵌套绕晕。作为程序员,你的一个重要任务,就是把表达式的结构组织好,让自己、同伴,以及以后可能读到你表达式的人都能看懂。
第三,数学符号花样太多:乘法写在两项之间,指数写成上标,除法用横杠,平方根像带斜边的屋顶,有些符号还特别难打!但这些花样,都能用调用表达式统一起来。Python 虽然支持用中缀写常见运算符(比如 +、-),但任何运算符都能写成一个有名字的函数。
1.2.3 导入库函数
Python 定义了大量函数,包括上一节提到的运算符函数,但默认不会把所有函数名都直接给你用。它把知道的函数和其他东西整理进模块(module),这些模块合起来就是 Python 标准库。要用的时候,得先把它们导入(import)。比如 math 模块提供了各种常见的数学函数:
>>> from math import sqrt
>>> sqrt(256)
16.0而 operator 模块提供了中缀运算符对应的函数:
>>> from operator import add, sub, mul
>>> add(14, 28)
42
>>> sub(100, mul(7, add(8, 4)))
16import 语句先指定模块名(比如 operator 或 math),再列出要从这个模块导入的命名属性(比如 sqrt)。函数一旦导入,就能反复调用。
用运算符函数(比如 add)和直接用运算符符号(比如 +),没有区别。按惯例,简单的算术运算,多数程序员还是用符号和中缀表示法。
Python 3 库文档 列出了每个模块定义的函数,比如 math 模块。不过,这份文档是写给熟悉整门语言的开发者的。眼下,动手试一个函数,比读文档更能弄清它的行为。等你熟悉了 Python 的语言和词汇,它就会成为很有价值的参考资料。
1.2.4 名称与环境
编程语言的一个关键之处,在于能用名称指代计算对象。一个值一旦有了名字,我们就说名称绑定(binds)到了这个值上。
在 Python 里,我们用赋值语句建立新的绑定:= 左边是名称,右边是值:
>>> radius = 10
>>> radius
10
>>> 2 * radius
20名称也可以通过 import 语句绑定:
>>> from math import pi
>>> pi * 71 / 223
1.0002380197528042等号 = 在 Python(以及许多其他语言)里叫赋值运算符(assignment operator)。赋值是最简单的抽象方法:它让我们能用简单的名称,指代复合运算的结果,比如上面算出的 area。就这样一步一步搭出越来越复杂的计算对象,复杂的程序也就构造出来了。
名称能绑定到值,之后又能靠名称取回值,这意味着解释器得维护一块内存,把名称、值和绑定关系都记下来。这块内存就叫环境(environment)。
名称也可以绑定到函数。比如名称 max,就绑定在我们一直用的 max 函数上。和数字不同,函数很难用文字表示,所以你要查看一个函数时,Python 会打印一段标识说明:
>>> max
<built-in function max>我们可以用赋值语句给已有的函数起新名字:
>>> f = max
>>> f
<built-in function max>
>>> f(2, 3, 4)
4连续的赋值语句还能把名称重新绑定到新值:
>>> f = 2
>>> f
2在 Python 里,名称常被叫作变量名(variable names)或变量(variables),因为它们在程序执行过程中能绑定到不同的值。名称一旦被赋成新值,就不再指向旧值。你甚至可以把内置名称也绑到新值上:
>>> max = 5
>>> max
5把 max 赋成 5 之后,名称 max 就不再绑定函数了,这时调用 max(2, 3, 4) 就会报错。
执行赋值语句时,Python 会先求值 = 右侧的表达式,再修改左侧名称的绑定。所以,右侧表达式里可以引用某个名称,哪怕它正是这次赋值要绑定的名称:
>>> x = 2
>>> x = x + 1
>>> x
3一条语句里还能同时给多个名称赋多个值:= 左边的名称、右边的表达式,都用逗号隔开:
>>> area, circumference = pi * radius * radius, 2 * pi * radius
>>> area
314.1592653589793
>>> circumference
62.83185307179586改变一个名称的值,不会影响其他名称。下面这个例子里,area 当初是按 radius 算出来绑定的,但改 radius 并没有影响 area。想更新 area,得再写一条赋值语句。
>>> radius = 11
>>> area
314.1592653589793
>>> area = pi * radius * radius
380.132711084365多重赋值时,= 右边的所有表达式都会先求值,然后才绑定到左边的名称。正因为这条规则,交换两个名称绑定的值,一条语句就能完成。
>>> x, y = 3, 4.5
>>> y, x = x, y
>>> x
4.5
>>> y
31.2.5 求值嵌套表达式
本章的一个目标,是把程序化思维(procedural thinking)单独拎出来讲。举个例子:求值嵌套的调用表达式时,解释器自己其实也在按一套固定的流程走。
Python 求值一个调用表达式时,会这样做:
- 先求值运算符和操作数子表达式,得到它们的值;
- 再把运算符子表达式的值(也就是函数),作用到操作数子表达式的值(也就是参数)上。
就算规则这么简单,也点出了「计算过程」的一些本质。第 1 步就要求:要完成一个调用表达式的求值,得先求值别的表达式。所以,这套求值流程本质上是递归的——它把这条规则自己也当成了其中一步。
比如,下面这行代码:
>>> sub(pow(2, add(1, 10)), pow(2, 5))
2016要算出结果,这套求值流程一共要用到四次。把每一步要求值的表达式都画出来,就能看清它的层次结构:

这个图叫表达式树(expression tree)。在计算机科学里,树习惯上是从上往下长。树上每个点都叫节点(node),在这里,每个节点都是「表达式 + 它的值」。
求根节点(root,也就是顶部那个完整表达式)的值,得先求它各分支(branches,也就是子表达式)的值。没有分支的叶子节点(leaf)要么是函数,要么是数字。中间节点则有两部分:套用求值规则的调用表达式,以及求出的结果。按这棵树来看,可以想象操作数的值从最底下的叶子开始往上冒,一层层组合,最后汇到根节点。
接下来注意:第 1 步反复做下去,最后碰到的就不再是调用表达式,而是基本表达式,比如数字(2)和名称(add)。对这些基本情况,我们规定两条:
- 数字字面量求值为它所指的那个数;
- 名称求值为它在当前环境里绑定的那个值。
环境在这里很关键:不给环境信息,很多表达式的值就无从谈起。例如:
>>> add(x, 1)如果不说清 x(甚至 add)在当前环境里绑的是什么,这个表达式的值就根本定不下来。环境为求值提供了上下文(context),这是我们理解程序执行的关键。
上面这套求值流程,目前只能处理调用表达式、数字和名称这三种,还不足以求值所有 Python 代码。比如,它处理不了赋值语句:
>>> x = 3它不返回值,也不把函数作用到参数上——赋值的目的,只是把名称绑定到一个值。一般来说,语句不是被求值(evaluated),而是被执行(executed);它们不产生值,而是改变状态。每种表达式或语句,都有各自的求值或执行规则。
一点学究式的说明:我们说「数字字面量求值为一个数」,真正的意思是 Python 解释器把它求值成了一个数。赋予编程语言意义的,是解释器。正因为解释器是一个行为始终如一的固定程序,我们才可以说:在 Python 程序里,数字字面量(和表达式)本身就求值为某个值。
1.2.6 非纯函数 print
在本书中,我们会区分两种函数。
纯函数(Pure functions):有输入(参数),也有输出(返回值)。比如内置函数 abs:
>>> abs(-2)
2可以把它想象成一台小机器:丢进去一个数,吐出来另一个数。

纯函数的特性是:除了返回值之外没有别的副作用,而且同样的参数永远返回完全相同的结果。
非纯函数(Non-pure functions):除了返回值之外,调用它们还会产生副作用(side effect),也就是改变解释器或计算机的某种状态。最常见的副作用,就是用 print 在屏幕上多打印点东西:
>>> print(1, 2, 3)
1 2 3print 和 abs 在这些例子里看着差不多,但工作方式根本不同。print 返回的值永远是 None——Python 里代表「空」的特殊值。交互式解释器不会主动打印 None。上面看到的 1 2 3,其实是 print 被调用时打印出来的副作用,不是它的返回值。

嵌套的 print 调用,更能看出它的非纯特性:
>>> print(print(1), print(2))
1
2
None None如果你觉得这行输出很奇怪,画出表达式树就明白了:为什么会先打印 1 和 2,最后再打印两个 None。
一定要小心 print!它返回 None,所以千万别把它直接放在赋值语句右边:
>>> two = print(2)
2
>>> print(two)
None纯函数受到严格限制:不能有副作用,也不能随时间改变行为。但这些限制换来的好处很大:
- 纯函数能可靠地嵌套组合成更复杂的表达式。前面
print的例子说明,非纯函数放在子表达式里往往出问题;而max、pow、sqrt这类纯函数,随便嵌套都没事。 - 纯函数更好测试:同一组参数永远得到同一个返回值,直接和预期结果对比即可。本章后面会详细讲测试。
- 第 4 章会讲到,写并发程序(多个表达式可能同时求值)时,纯函数几乎是唯一安全的选择。
相比之下,第 2 章会介绍一大堆非纯函数,以及它们的典型用途。
正因为如此,本章剩下的部分,主要讲怎么定义和使用纯函数。print 只在要看中间计算结果时才用得上。