1.2 编程元素

编程语言不仅仅是指示计算机执行任务的一种手段。这种语言还充当一个框架,我们可以在其中组织关于计算过程的想法。程序的作用是在编程社区的成员之间传达这些想法。因此,程序必须是为人们阅读而编写的,只是偶尔为机器执行而编写的。

当我们描述一种语言时,我们应该特别注意这种语言所提供的将简单的思想组合成更复杂的思想的手段。每一种强大的语言都有三种机制:

  • 原始表达式和语句,表示语言提供的最简单构建块

  • 组合方法,用简单的元素构成复合元素

  • 抽象方法,通过这种方法可以将复合元素命名为单元并对其进行操作。

在编程中,我们处理两种元素:函数和数据。(很快我们就会发现它们其实并不是那么泾渭分明。)非正式地说,数据是我们想要操作的东西,而函数描述了操作数据的规则。因此,任何强大的编程语言都应该能够描述基本的数据和基本的函数,并且具有一些结合、抽象函数和数据的方法。

1.2.1 表达式

在上一节中已经尝试了完整的Python解释器,现在我们重新开始,一个元素一个元素地系统地开发Python语言。 如果这些例子看起来过于简单,请耐心等待——更多令人兴奋的材料很快就会出现。

我们从原始表达式开始。 一种原始表达式是数字。 更准确地说,您键入的表达式由代表以10为基数的数字的数字组成。

>>> 42
42

表示数字的表达式可以与数学运算符组合成一个复合表达式,解释器将对其求值:

>>> -1 - -1
0
>>> 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 + 1/128
0.9921875

这些数学表达式使用中缀表示法,其中操作符(例如,+、-、*或/)出现在操作数(数字)之间。Python包含了许多形成复合表达式的方法。我们不会试图立即将它们全部列举出来,而是将逐步引入新的表达式形式,以及它们支持的语言特性。

1.2.2 调用表达式

最重要的一种复合表达式是调用表达式,它将函数应用于一些参数。回想一下代数,函数的数学概念是从一些输入参数到输出值的映射。例如,max函数将其输入映射到单个输出,这是最大的输入。Python表达函数应用程序的方式与传统数学中相同。

>>> max(7.5, 9.5)
9.5

这个调用表达式有子表达式:操作符是圆括号前面的表达式,圆括号包含以逗号分隔的操作数表达式列表。

运算符指定一个函数。当这个调用表达式被求值时,我们说函数max被带参数7.5和9.5调用,并返回9.5的值。

调用表达式中参数的顺序很重要。例如,函数pow将它的第一个参数取第二个参数的幂。

>>> pow(100, 2)
10000
>>> pow(2, 100)
1267650600228229401496703205376

与中缀表示法的数学惯例相比,函数表示法有三个主要优点。首先,函数可以接受任意数量的参数:

>>> max(1, -2, 3, -4)
3

不会产生歧义,因为函数名总是在它的参数之前。

第二,函数表示法以一种简单的方式扩展到嵌套表达式,其中元素本身就是复合表达式。在嵌套调用表达式中,与复合中缀表达式不同,嵌套的结构完全显式地体现在圆括号中。

>>> max(min(1, -2), min(pow(3, 5), -4))
-2

(原则上)这种嵌套的深度和Python解释器可以计算的表达式的总体复杂性没有限制。然而,人们很快就会被多层嵌套所迷惑。作为一名程序员,对您来说一个重要的角色是构造表达式,以便您自己、您的编程伙伴以及将来可能阅读您的表达式的其他人都可以理解它们。

第三,数学符号有很多种形式:乘法出现在项之间,指数出现在上标,除法出现在横杆上,平方根出现在斜壁顶上。有些符号很难输入!然而,所有这些复杂性都可以通过调用表达式的符号来统一。虽然Python支持使用中缀符号(如+和-)的常见数学运算符,但任何运算符都可以表示为带有名称的函数。

1.2.3 导入库函数

Python定义了大量的函数,包括上一节提到的操作符函数,但在默认情况下不会使它们的所有名称都可用。相反,它将函数和它所知道的其他数量组织到模块中,这些模块一起组成了Python库。要使用这些元素,需要导入它们。例如,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)))
16

import语句指定一个模块名称(例如,operator或math),然后列出该模块要导入的命名属性(例如,sqrt)。 一旦一个函数被导入,它就可以被多次调用。

使用这些操作符函数(例如,add)和操作符符号本身(例如,+)之间没有区别。 通常,大多数程序员使用符号和中缀符号来表示简单的算术。

Python 3库文档列出了每个模块定义的函数,比如math模块。 然而,本文档是为熟悉整个语言的开发人员编写的。 现在,您可能会发现,与阅读文档相比,实际使用一个函数可以告诉您更多有关其行为的信息。 随着您逐渐熟悉Python语言和词汇表,该文档将成为一个有价值的参考来源。

1.2.4 命名和环境

编程语言的一个关键方面是它提供的使用名称引用计算对象的方法。如果一个值被赋予了一个名称,我们说这个名称绑定到这个值。

在Python中,我们可以使用赋值语句来建立新的绑定,该语句在=的左边包含一个名称,在右边包含一个值:

>>> radius = 10
>>> radius
10
>>> 2 * radius
20

名称也可以通过import语句绑定。

>>> from math import pi
>>> pi * 71 / 223
1.0002380197528042

=符号在Python(和许多其他语言)中被称为赋值操作符。赋值是我们最简单的抽象方法,因为它允许我们使用简单的名称来引用复合操作的结果,例如上面计算的面积。通过这种方式,复杂的程序是通过逐步构建越来越复杂的计算对象来构建的。

可以将名称绑定到值,然后通过名称检索这些值,这意味着解释器必须维护某种类型的内存来跟踪名称、值和绑定。这种内存称为环境。

名称也可以绑定到函数。例如,名称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中,名称通常被称为变量名或变量,因为它们可以在程序执行过程中绑定到不同的值。当一个名称通过赋值绑定到一个新值时,它不再绑定到任何以前的值。甚至可以将内置名称绑定到新值。

>>> 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被绑定到最初根据半径定义的值,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
3

1.2.5 计算嵌套表达式

我们在这一章的目标之一是分离有关程序性思考的问题。 作为一个恰当的例子,让我们考虑一下,在计算嵌套调用表达式时,解释器本身是在遵循一个过程。 要求调用表达式的值,Python将执行以下操作:

  1. 求运算符和操作数子表达式的值,然后

  2. 将作为操作符子表达式值的函数应用于作为操作数子表达式值的参数。

即使这个简单的过程也说明了一般过程的一些要点。 第一步规定,为了完成调用表达式的求值过程,我们必须首先求其他表达式的值。 因此,计算过程本质上是递归的; 也就是说,它包含了调用规则本身的需要,作为其步骤之一。 例如,计算

>>> sub(pow(2, add(1, 10)), pow(2, 5))
2016

要求此计算程序应用四次。如果我们画出每个要求值的表达式,我们就可以把这个过程的层次结构形象化。

这个插图叫做表达式树。 在计算机科学中,树通常是自上而下生长的。 树中每一点上的对象称为节点; 在本例中,它们是与其值配对的表达式。

求其根(顶部的完整表达式)的值,首先需要求其子表达式分支的值。 叶表达式(即没有从它们派生分支的节点)表示函数或数字。 内部节点有两部分:应用求值规则的调用表达式,以及该表达式的结果。 从这个树的角度来看求值,我们可以想象操作数的值向上渗透,从终端节点开始,然后在越来越高的级别上组合。

接下来,注意第一步的重复应用将我们带到了需要求值的地方,不是调用表达式,而是调用原始表达式,如数字(如2)和名称(如add)。 我们通过规定

  1. 一个数字计算为它所命名的数字,

  2. 一个名称计算为当前环境中与该名称相关联的值

来处理基本情况。 请注意环境在确定表达式中符号的含义方面的重要作用。 在Python中,谈论表达式的值是没有意义的,比如不指定任何关于环境的信息,以提供名称x(甚至名称add)的含义:

>>> add(x, 1)

环境提供了计算发生的上下文,它在我们理解程序执行过程中扮演着重要的角色。

这个求值过程不足以计算所有Python代码,只能计算调用表达式、数字和名称。例如,它不处理赋值语句。由于赋值的目的是将一个名称绑定到一个值,因此执行不会返回一个值,也不会对函数的某些参数求值。

>>> x = 3

一般来说,语句不是被求值,而是被执行;它们不产生值,而是做一些改变。每种类型的表达式或语句都有自己的求值或执行过程。

请注意:当我们说“一个数字计算为一个数字”时,我们实际上是指Python解释器将一个数字计算为一个数字。它是赋予程序语言意义的解释器。假定解释器是一个始终行为一致的固定程序,我们可以说数字(和表达式)本身在Python程序的上下文中计算值。

1.2.6 非纯打印功能

在整篇文章中,我们将区分两种类型的函数。

纯函数。函数有一些输入(它们的参数)并返回一些输出(应用它们的结果)。内置的函数可以描述为一个接受输入并产生输出的小型机器。

>>> abs(-2)
2

函数abs是纯函数。纯函数有这样一种特性:应用它们除了返回值之外没有其他效果。而且,纯函数在使用相同的参数调用两次时必须返回相同的值。

非纯函数。除了返回值之外,应用非纯函数还会产生副作用,对解释器或计算机的状态进行一些更改。一个常见的副作用是使用print函数在返回值之外生成额外的输出。

>>> print(1, 2, 3)
1 2 3

虽然在这些例子中print和abs可能看起来很相似,但它们的工作方式根本不同。print返回的值总是None,这是一个特殊的Python值,表示nothing。交互式Python解释器不会自动打印None值。在打印的情况下,函数本身打印输出作为被调用的副作用。

要print的嵌套调用表达式突出显示函数的非纯字符。

>>> print(print(1), print(2))
1
2
None None

如果您发现这个输出出乎意料,请画一个表达式树来阐明为什么对这个表达式求值会产生这种奇怪的输出。

小心打印!它返回None这一事实意味着它不应该是赋值语句中的表达式。

纯函数受到限制,因为它们不能产生副作用或随时间改变行为。 实施这些限制会带来实质性的好处。 首先,纯函数可以更可靠地组合成复合调用表达式。 在上面的非纯函数示例中可以看到,当在操作数表达式中使用print时,不会返回有用的结果。 另一方面,我们已经看到像max、pow和sqrt这样的函数可以在嵌套表达式中有效地使用。

其次,纯函数往往更容易测试。 参数列表总是会导致相同的返回值,可以将其与预期的返回值进行比较。 测试将在本章后面更详细地讨论。

第三,第四章将说明纯函数对于编写并发程序是必不可少的,在并发程序中,多个调用表达式可以同时计算。

相比之下,第2章研究了一系列非纯函数并描述了它们的用途。 由于这些原因,我们将在本章的其余部分着重讨论如何创建和使用纯函数。 print函数仅用于查看计算的中间结果。

Last updated

Was this helpful?