带你读《Python科学计算(原书第2版)》之三:Python简明教程-阿里云开发者社区

开发者社区> 华章出版社> 正文

带你读《Python科学计算(原书第2版)》之三:Python简明教程

简介: 本书讲解如何使用Python科学计算软件包来实现和测试复杂的数学算法,第2版针对Jupyter笔记本用户更新了部分代码,并新增了讲解SymPy的章节。书中首先介绍Python相关知识,涵盖IPython、NumPy和SymPy,以及二维和多维图形的绘制。之后讨论不同领域的应用实例,涉及常微分方程、偏微分方程和多重网格,并展示了处理Fortran遗留代码的方法。

点击查看第一章
点击查看第二章
|第3章|
Python for Scientists, Second Edition

Python简明教程

虽然Python是一种小语言,但其涉及的内容却非常丰富。在编写教科书时,常常会有一种按主题全面阐述程序设计语言各个方面的冲动。最明显的例子是来自Python创始人吉多·范罗苏姆的入门教程。Python入门教程以电子形式包含在Python文档中,也可以在线获得,或者购买纸质版本书籍(Guido van Rossum和Drake Jr.(2011))。这本书相对比较简洁,只有150页的内容,而且没有涉及NumPy。我最喜欢的教科书是Lutz(2013),该书超过1500页,是一种马拉松式学习曲线,而且只是稍微提及NumPy。该书之所以非常优秀,是因为它提供了各功能的详细解释,但对于Python语言的第一门课程而言,其内容太过繁杂。另外两本教科书Langtangen(2009)和Langtangen(2014)的内容更倾向于科学计算,但都存在同样的问题,它们都差不多800页,且内容重叠较多。建议读者把上述书籍(以及其他书籍)作为参考书,但它们都不是适用于学习Python语言的教科书。
学习一门外语时,很少有人会先学习一本语法教材,然后背字典。大多数人的学习方法是开始学习一些基本语法和少量词汇,然后通过实践逐渐扩大其语法结构和词汇量的范围。这种学习方法可以使他们快速地理解和使用语言,而这就是本书采用的学习Python的方法。这种学习方法的缺点是语法和词汇被分散到整个学习过程中,但这个缺点可以通过使用其他教科书来改善,例如前一段中所提到的那些教科书。

3.1 输入Python代码

虽然可以仅仅通过阅读教程内容的方式来学习,但在阅读的同时使用手头的IPython终端来尝试示例代码会更有帮助。对于较长的代码片段(如3.9节和3.11节中的代码片段),则建议使用笔记本模式,或者终端模式与编辑器一起使用,以便保存代码。附录A中的A.2和A.3节描述了这两种方法。在尝试了这些代码片段之后,强烈鼓励读者在解释器中尝试自己的实验。
每种程序设计语言都包含代码块,代码块是由一行或者多行代码组成的语法体。与其他语言相比,Python基本上不使用圆括号()和大括号{},而是使用缩进作为代码块格式化工具。在任何以冒号(:)结尾的行之后,都需要一个代码块,代码块通过一致的缩进与周围的代码区分开来。虽然没有指定缩进的空白字符个数,但非官方标准一般使用4个空白字符。IPython和所有支持Python的文本编辑器都会自动完成这种格式化操作。若要还原到原始缩进级别,则请使用回车键输入一个空行。不使用括号可以提高可读性,但缺点是代码块中的每一行都必须与前面的缩进相同,否则将发生语法错误。
Python允许两种形式的注释(comment)。一种是#符号,表示当前行中#符号后的其余部分是注释。文档字符串(docstring)可以跨越多行,并且可以包含任何可打印字符。文档字符串由一对三引号来界定,例如:
""" This is a very short docstring. """
为了完整性,读者注意到我们可以在同一行上放置多条语句,只要用分号将它们分开,但是应该考虑可读性。长语句可以使用续行符号“”来分成多行。并且,如果一个语句包括一对括号(),那么我们可以在它们之间的任何位置点分行,而不需要使用续行符号“”。一些简单的示例如下所示:

 a=4; b=5.5; c=1.5+2j; d='a'
 e=6.0*a-b*b+\
        c**(a+b+c)
 f=6.0*a-b*b+c**(
        a+b+c)
a, b, c, d, e, f

3.2 对象和标识符

Python包含大量的对象和标识符。对象可以被认为是一个计算机内存区域,包含某种数据以及与数据相关的信息。对于一个简单的对象,这些信息包括它的类型和标识(即内存中的位置,很显然这与计算机相关)。因此,大多数用户对对象标识并不感兴趣。用户需要与机器无关的访问对象的方法,这可以通过标识符提供。标识符是附加到对象上的一个标签,由一个或者多个字符组成。标识符的第一个字母必须是字母或者下划线,后续字符必须是数字、字母或者下划线。标识符是区分大小写的:x和X是不同的标识符。(以下划线开始或结束的标识符具有专门用途,因此初学者应该避免使用。)我们必须避免使用预定义的标识符(如list),并且应该总是尝试使用有意义的标识符。然而,选择xnew、x_new或xNew,则是用户个人的偏好。请尝试运行如下代码片段,建议读者在终端窗口中逐行键入,可以更加明确其含义:

1 p=3.14
2 p
3 q=p
4 p='pi'
5 p
6 q

注意,我们从来没有声明标识符p引用的对象的类型。在C语言中我们必须声明p为“double”类型,在Fortran中则必须声明p为“real*8”类型。这不是偶然或者疏忽。Python语言的一个基本特征是类型属于对象,而不是标识符。
接下来,在第3行我们设置q=p。右侧由p指向的对象替换,q是指向这个对象的新的标识符(如图3-1所示)。这里没有测试标识符q和p相等性的含义!注意,在第4行中,我们将标识符p重新赋值为一个“string”对象。但是,标识符q仍然指向原始的浮点数对象(如图3-1所示),第5行和第6行的输出可以证明上述结论。假设我们要重新赋值标识符q。然后,除非中间把q赋值给另一个标识符,否则原始的“float”对象将没有任何赋值的标识符,因此程序员将无法访问它。Python将自动检测并释放计算机内存,这个过程被称为自动垃圾收集。
image.png
图3-1 Python中赋值的示意图。在第一条命令p=3.14执行之后,创建浮点数对象3.14,并将其赋值给标识符p。在这里,对象被描述为对象标识(对象标识是一个大数值,对应于计算机中存储对象数据的内存地址(高度依赖于计算机))和对象类型。第二条命令q=p将标识符q分配给同一个对象。第三条命令p='pi'将p分配给一个新的“string”对象,而q指向原始的浮点数对象
赋值语句在稍后的章节中十分重要,我们强调赋值操作是Python程序的基本构建块之一,尽管赋值语句看起来像是相等性判断,但它与相等性判断无关。其语法格式如下:
=

3.3 数值类型

Python包括三种简单的数值对象类型,我们还将介绍第四种类型(稍微复杂)。

3.3.1 整型

Python语言中整型数据为int。早期版本支持的整型数值范围为[-231, 231-1],但是新的版本整型数值范围进一步扩大,现在Python支持的整型数值范围几乎没有限制(仅受计算机内存限制)。
整型数值当然支持通常的加法(+)、减法(-)和乘法(*)运算。而对于除法运算则需要稍微注意:即使p和q是整数,p/q的结果也不一定是整数。一般地,我们可以假设q>0(不失一般性),在这种情况下,存在唯一的整数m和n,满足:
p=mq+n,其中0≤n<q
在Python语言中,整型除法定义为p//q,其结果为m。使用p%q,可以求得余数n。使用p**q,可以求得乘幂pq,当q<0时,结果为实数。

3.3.2 实数

Python语言中浮点数为float。在大多数安装环境中,浮点数的精度大约为16位,其数值范围为(10-308, 10308)。浮点数对应于C语言家族中的double,对应于Fortran家族中的real*8。浮点数常量的标记遵循一般标准,例如:
-3.14, -314e-2, -314.0e-2, -0.00314E3
上述浮点数都表示相同的float值。
常用的加法、减法、乘法、除法和幂的运算规则同样适用于浮点数运算。对于前三种运算,可以无缝地实现混合模式操作,例如,如果需要求一个int和float之和,则int被自动向上转换(widening)为float。该规则同样适用于除法运算(如果一个操作数是int而另一个操作数是float)。然而,当两个操作数都是int(例如,±1/5)时,结果会是什么呢?Python的早期版本(<3.0)采用整数除法规则:1/5=0,-1/5=-1,而版本号≥3.0的Python则采用实数除法规则:1/5=0.2,-1/5=-0.2。这是一个潜在的坑,但很容易避免:可以采用整除运算符//,或者向上转换其中的一个操作数以保证运算结果没有二义性。
Python语言具有一个继承于C语言的有用特性。假设我们希望将a引用的浮点数递增2,很显然可以使用如下代码:

temp=a+2
a=temp

虽然上述代码结果正确,但若使用如下单一指令,则速度更快、效率更高:
a+=2
当然,上述规则同样适用于其他算术运算符。
可以显式地将一个int向上转换到一个float,例如,float(4)将返回结果4.或者4.0。把一个float向下转换(narrowing)到一个int的算法如下:如果x是一个正实数,则存在一个整数m和一个浮点数y,满足:
x=m+y,其中0≤y<1.0
在Python语言中,float向下转换为int可通过int(x)实现,结果为m。如果x为负数,则int(x)=-int(-x)。该规则可以简洁地表述为“向0方向截取整数”,例如,int(1.4)=1和int(-1.4)=-1。
在程序设计语言中,我们期望提供大量熟悉的数学函数来用于编程。Fortran语言内置了最常用的数学函数,但在C语言家族中,我们需要在程序的顶部通过一个语句(如#include math.h)导入数学函数。Python语言同样需要导入一个模块,作为示例我们讨论math模块,该模块包括许多用于实数的标准数学函数(模块将在3.4节中定义)。首先假设我们不知道math模块包含哪些内容。下面的代码片段首先加载模块,然后列出其内容的标识符。

import math
dir(math)        # or math.<TAB> in IPython

要了解有关具体对象的更多信息,可以查阅书面文档或者使用内置帮助,例如在IPython中:
math.atan2? # or help(math.atan2)
如果读者非常熟悉模块中包含的内容,则可以在调用函数之前,在代码中的任何地方使用一个快速的解决方案来替换上面的导入命令:
from math import *
随后,上面提到的函数可以直接使用atan2(y,x)的方式,而不是math.atan2(y,x)的方式,乍看起来这非常美妙。然而,还存在另一个模块cmath,其中包含了许多关于复数的标准数学函数。接下来假设我们重复使用上述快速导入解决方案:
from cmath import *
那么,atan2(y,x)代表哪一个函数呢?可以把一个实数向上转换到一个复数,而反过来则不能转换!注意,与C语言不同,import语句可以位于程序中的任何地方,只要在需要其内容的代码之前即可,所以混乱正静静地等待着破坏用户的计算!当然Python也意识到了这个问题,我们将在3.4节中描述推荐的工作流程。

3.3.3 布尔值

为了完整性,这里我们将讨论布尔值或者bool,它是int的子集,包含两个可能的值True和False,大致等同于1和0。
假设变量box和boy引用bool对象值,则表达式(如“not box”“box and boy”和“box or boy”)具有特殊的含义。
int和float类型值(如x、y)定义了标准的相等运算符,例如x==y(等于)、x!=y(不等于)。为了提醒读者注意Python浮点数的局限性,下面是一个简单的练习,请猜测以下代码行的运行结果,然后键入、运行该行代码并解释其执行结果。
math.tan(math.pi/4.0)==1.0
对于比较运算符“x>y”“x>=y”“xz”等同于:
(0<=x) and (x<1) and (1z)
注意,在上例中x和y或z之间并没有进行直接比较。

3.3.4 复数

前文已经介绍了三种数值类型,它们构成了最简单的Python对象类型,它们是更复杂的数值类型的基础。例如,有理数可以用一对整数来实现。在科学计算中,复数可能是更有用的复杂数值类型,它是使用一对实数来实现的。虽然数学家通常使用i表示,但大多数工程师则倾向于使用j,而Python语言采用了后者。因此,一个Python复数可以显式地定义为诸如c=1.5–0.4j的形式。请仔细观察该语法:j(也可以使用大写的J)紧跟在浮点数的后面,中间没有包括符号“*”。另一种把一对实数a和b转换为一个复数的语法是c=complex(a, b)。也可以使用下列语法把上面语句中定义的复数c转换为实数:c.real返回实数a;c.imag返回实数b。另外,语法c.conjugate()将返回复数c的共轭复数。有关复数属性的语法将在3.10节详细讨论。
Python复数支持五种基本的算术运算,并且在混合运算模式中,将自动进行向上数值类型转换。另外,还包含一个针对复数运算的数学函数库,这需要导入库cmath,而不是math。然而,根据显而易见的原因,复数没有定义前文描述的涉及排序的比较运算,但可以使用等于运算符以及不等于运算符。
到此为止,读者已经学习了足够的Python知识,可以将Python解释器作为一个复杂的包含五种功能的计算器来使用,强烈建议读者尝试一些自己的示例。

3.4 名称空间和模块

当Python正在运行时,它需要保存已分配给对象的那些标识符列表,此列表被称为名称空间(namespace),并且作为Python对象,名称空间也具有标识符。例如,当在解释器中工作时,名称空间具有一个不大好记忆的名称:__main__。
Python的优势之一是它能够包含由读者或者其他人编写的文件(其中包含对象、函数等)。为了实现这种包含其他文件的功能,假设读者已经创建了一个包含可以重用的对象(如obj1和obj2)的文件,并且保存了文件(例如,保存为foo.py,其后缀必须为.py。请注意,对于大多数文本编辑器而言,都要求该文件后缀为.py,以便支持处理Python代码)。这种Python文件被称为模块(module)。模块(foo.py)的标识符为foo,即其文件主名,不包括其后缀。
在后续的Python会话中,可以通过下列语句导入该模块:
import foo
(当首次导入该模块时,会将其编译成字节码并写入磁盘文件foo.pyc。在随后的导入中,解释器直接加载这个预编译的字节码,除非foo.py的修改日期更近,在这种情况下,将自动生成文件foo.pyc的新版本。)
上述导入语句的作用是引入模块的名称空间foo,随后可以借助如下方法来使用foo中的对象,例如标识符foo.obj1和foo.obj2。如果能够确信obj1和obj2不会与当前名称空间中的标识符冲突,则也可以通过下列语句直接导入obj1和obj2,然后使用诸如obj1的形式进行直接引用。
from foo import obj1, obj2
使用3.3.2节中的“快速导入”方法,则等同于如下语句:
from foo import *
该语句导入模块foo名称空间中的所有对象。如果当前名称空间中已经存在一个标识符obj1,则导入过程将覆盖标识符obj1,通常这意味着原来的对象将无法被访问。例如,假设我们已经有一个引用浮点数的标识符gamma,则执行如下导入语句:
from math import *
该导入语句将覆盖原来的标识符gamma,现在gamma指向cmath库中的(实数)gamma函数。接下来执行如下导入语句:
from cmath import *
该导入语句将把gamma覆盖为(复数)gamma函数!另外要注意,由于import语句可以在Python代码的任何位置出现,因此使用该导入方法将导致潜在的混乱。
除非是在解释器中进行快速的解释工作,否则最佳实践方案是修改导入语句,例如:
import math as re
import cmath as co
因此,在上述示例中,可以同时使用gamma、re.gamma和co.gamma。
我们现在已经了解了足够的背景知识,接下来解释如下神秘的代码行:
if name == "__main__"
该语句出现在2.5节中的两个代码片段内。第一次出现在文件fib.py中。现在,如果我们将这个模块导入解释器中,那么它的名称是fib,而不是__main__,因此这个代码行后面的代码行将被忽略。但是,在开发模块中的函数时,通常直接通过%run命令运行模块,此时(正如本节开始时所解释的)模块内容被读入__main__名称空间,所以满足代码行中的if条件,继而将执行随后的代码。在实践中,这种方法非常便利。在开发一系列对象(如函数)时,我们可以在附近编写辅助测试功能代码。而在生产模式中,通过import语句导入模块时,这些辅助功能代码被有效地“注释”了。

3.5 容器对象

计算机的有用性很大程度上取决于它们能快速执行重复性任务的能力。因此,大多数程序设计语言都提供容器对象,通常称为数组,它可以存储大量相同类型的对象,并通过索引机制检索它们。数学向量将对应于一维数组,而矩阵对应于二维数组。令人惊讶的是,Python核心语言竟然没有数组概念。相反,它有更加通用的容器对象:列表(list)、元组(tuple)、字符串(string)和字典(dict)。我们很快就会发现,可以通过列表来模拟数组对象,这就是以前在Python中进行数值处理的工作方式。由于列表的通用性,这种模拟方法与Fortran或者C中的等价结构相比要耗费更多的时间,其数值计算缓慢理所当然地为Python带来了坏名声。开发人员提出了各种方案来缓解这个问题,现在他们已经提出了标准的解决方案,就是使用NumPy附加模块(将在第4章阐述)。NumPy的数组具有Python列表的通用性,但其内部实现为C的数组,这显著地减少(但并非完全消除)了其速度损失。然而,本节将详细讨论这些核心容器对象,以进行大量的科学计算工作。它们擅长“行政、簿记杂务”,而这正是Fortran和C语言的弱项。用于特大数量的数值处理的数值数组则推迟到下一章,但是对数值特别感兴趣的读者也需要理解本节的内容,因为本节的知识点将延续到下一章。

3.5.1 列表

请读者在IPython终端输入并执行如下代码片段:

 1 [1,4.0,'a']
 2 u=[1,4.0,'a']
 3 v=[3.14,2.78,u,42]
 4 v
 5 len(v)
 6 len?        # or help(len)
 7 v*2
 8 v+u
 9 v.append('foo')
10 v

第1行是Python列表的第一个对象实例,是包括在方括号中由逗号分隔的Python对象的有序序列。它本身是一个Python对象,可以被赋值给一个Python标识符(例如第2行)。与数组不同,不要求列表的元素都是相同类型。在第3行和第4行中,我们看到在创建列表时,标识符被它所引用的对象替换,例如,一个列表可以是另一个列表中的元素。初学者应该再次参考图3-1。注意列表是对象,而不是标识符。在第5行中,我们调用一个非常有用的Python函数len(),它返回列表的长度,这里的返回结果是4。(Python函数将在3.8节中讨论。同时,我们可以在IPython中通过输入“len?”来了解len的用途。)我们可以通过类似第7行的构造语句来重复列表内容,第8行的代码用于拼接列表。我们可以将项目追加到列表的末尾(如第9行)。这里v.append()是另一个有用的函数,仅适用于列表。读者可以尝试v.append或者help(v.append),以查看其帮助信息。另外,输入list.然后按Tab键或者执行help(list)命令,将显示列表对象的内置函数。它们类似于3.3.4节中的c.conjugate()。

3.5.2 列表索引

我们可以通过索引来访问列表u的元素u[i],其中i是一个整数,并且i∈[0, len(u))。请注意,索引从u[0]开始,到u[len(u)-1]结束。到目前为止,这与数组索引访问(例如C或者Fortran语言)十分相似。然而,一个Python列表(例如u)“知道”其长度,因此我们也可以以相反的顺序来索引访问元素u[len(u)-k],其中k∈(0, len(u)],Python将其缩写为u[-k]。这种方法非常便利。例如,任何列表w的第一个元素都可以通过w[0]来访问,而其最后的元素可以通过w[-1]来访问。图3-2的中间部分显示了一个长度为8的列表的两组索引。使用上面的代码片段,请读者猜测一下对应于v[1]和v[-3]的对象,并使用解释器来检查答案。
image.png
图3-2 长度为8的列表u的索引和切片。中间部分显示u的内容及其两组索引,通过这些索引可以访问其元素。上面部分显示了一个长度为4的切片的内容(按正序)。下面部分显示了另一个切片的内容(按逆序)
乍一看,该功能似乎只是一个很小的增强,但当与切片和可变性的概念结合起来时,它就变得非常强大,我们接下来将介绍这些概念。因此,读者必须清楚地理解负数索引所代表的含义。

3.5.3 列表切片

给定一个列表u,我们可以通过切片操作来构造更多的列表。切片最简单的形式是u[start:end],结果是一个长度为end-start的列表,如图3-2所示。如果切片操作位于赋值语句的右侧,则会创建一个新的列表。例如,su=u[2:6]将创建一个包含4个元素的新列表,其中su[0]初始化为u[2]。如果切片操作位于赋值语句的左侧,则不会生成新的列表。相反,它允许我们改变现有列表中元素块的值。这里包含一些C语言和Fortran语言用户可能不大熟悉的重要新语法。
阅读如下简单示例,它说明了各种可能的操作;一旦理解其含义,便建议读者进一步尝试自己的实验,最好在IPython终端模式下进行操作。

1 u=[0,1,2,3,4,5,6,7]
2 su=u[2:4]
3 su
4 su[0]=17
5 su
6 u
7 u[2:4]=[3.14,'a']
8 u

如果start为0,则可以省略,例如,u[:-1]是u的除最后一个元素之外的副本。在另一端同样适用,u[1:]是u的除第一个元素之外的副本,而u[:]是u的副本。这里,我们假设切片操作位于赋值语句的右侧。切片操作的更一般的语法形式为su = u[start: end:step],结果su包含元素u[start]、u[start+step]、u[start+2*step]等,直到索引大于或者等于start+end。因此,以上述示例中的列表u为例,结果为u[2: -1:2]=[2,4,6]。一个特别有用的选项是step=-1,它允许以相反方向遍历列表。请参见图3-2中的示例。

3.5.4 列表的可变性

对于任何容器对象u,如果可以修改其元素或者切片操作,而无须对对象标识符进行任何明显的更改,则可称这样的对象为可变对象(mutable)。特别是,列表是可变对象。对于粗心大意的程序员,可变对象将是一个陷阱。请阅读如下代码:

 1 a=4
 2 b=a
 3 b='foo'
 4 a
 5 b
 6 u=[0,1,4,9,16]
 7 v=u
 8 v[2]='foo'
 9 v
10 u

前5行代码很容易理解:a赋值给对象4,b也赋值给同一个对象。随后b赋值给对象'foo',而这不会改变a。在第6行代码中,u被赋值给一个列表对象;在第7行代码中,v也赋值给同一个列表对象。由于列表是可变对象,因此我们在第8行中改变了列表对象的第2个元素。第9行显示了改变结果。但是u同时指向同一个对象(参见图3-1),第10行表明结果也被改变。虽然逻辑清晰无误,但这也许不是我们期望的结果,因为u没有被显式地修改。
请务必牢记前面关于切片的结论:一个列表的切片总是一个新的对象,即使切片的维度与原始列表相同。因此,请把上一个代码片段中的第6~10行与下面代码片段进行比较:

1 u=[0,1,4,9,16]
2 v=u[ : ]
3 v[2]='foo'
4 v
5 u

该代码片段中的第2行创建了一个切片对象,它是第1行定义的对象的一个拷贝。因此,修改列表v不会影响列表u,反之亦然。
列表是非常通用的对象,并且存在许多可以生成列表对象的Python函数。我们将在本书的其余部分讨论列表生成。

3.5.5 元组

下一个要讨论的容器是元组(tuple)。在语法上,元组与列表的唯一差别是使用()而不是[]作为分隔符,元组同样也支持类似于列表的索引和切片操作。然而,存在一个重要的区别:我们不能修改元组元素的值,即元组是不可变对象(immutable)。乍一看,元组似乎完全是多余的。那为什么不使用列表呢?然而,元组的不可变性具有一个优点:当需要一个标量时,我们可以使用元组;并且在许多情况下,当没有歧义时,我们可以省略括号(),事实上这是使用元组的最常见方式。请阅读如下代码片段,我们用两种不同的方式来对元组进行赋值。

(a,b,c,d)=(4,5.0,1.5+2j,'a')
a,b,c,d = 4,5.0,1.5+2j,'a'

第2行显示了如何使用单个赋值运算符来进行多个标量赋值。这在我们需要交换两个对象,或者交换两个标识符(如a和L1)的常见情况下非常有效。交换两个对象的传统方法如下:

temp=a
a=L1
L1=temp

许多程序设计语言都采用这种方式,假设temp、a和L1都指向相同的数据类型。然而,Python语言可以采用如下方式实现相同的任务:

a,L1 = L1,a

这种方式更加清晰、简洁,并且适用于任意数据类型。
元组的另一个用途(可能是最重要的用途)是将可变数量的参数传递给函数的能力,这将在3.8.4节讨论。最后,我们注意到一个经常让初学者迷惑的语法:我们有时需要一个只有一个元素(如foo)的元组。表达式(foo)的求值结果是去掉括号仅保留元素。正确的元组构造语法是(foo,)。

3.5.6 字符串

虽然前文已经涉及字符串,但我们注意到,Python将字符串视为包含字母数字字符的不可变容器对象。在项目之间没有逗号分隔符。字符串分隔符既可以是单引号也可以是双引号,但不能混合使用。未使用的分隔符可以出现在字符串中,例如:

s1="It's time to go"
s2=' "Bravo!" he shouted.'

字符串同样支持类似于列表的索引和切片操作。
有两个与字符串相关的非常有用的转换函数。当函数str()应用到Python对象时,结果返回该对象的字符串表示形式。在使用过程中,函数eval充当str()的逆函数。请阅读如下代码片段:

L = [1,2,3,5,8,13]
ls = str(L)
ls
eval(ls) == L

字符串对于数据的输入非常有用,而最重要的是从打印函数生成格式化输出(参见3.8.6节和3.8.7节)。

3.5.7 字典

如前所述,列表对象是对象的有序集合,字典对象则是对象的无序集合。我们不能根据元素的位置来访问元素,而必须分配一个关键字(一个不可变对象,通常是一个字符串)来标识元素。因此字典是一对对象的集合,其中第一个项目是第二个项目的键。键-对象(即键-值)一般书写为key:object。我们通过键而不是位置来获取字典的项目。字典的分隔符是花括号{}。下面是一个简单的例子,用于说明有关字典的基本操作。

1 empty={}
2 parms={'alpha':1.3,'beta':2.74}
3 #parms=dict(alpha=1.3,beta=2.74)
4 parms['gamma']=0.999
5 parms

被注释了的第3行是等同于第2行的字典构造方法。第4行显示了如何(非正式地)向字典中添加新项。这说明了字典的主要数值用法:传递数目不确定且可变的参数。另一个重要用途是函数中的关键字参数(参见3.8.5节)。一种更为复杂的应用将在8.5.2节讨论。
字典的用途非常广泛,具有许多其他属性,并且在更一般的上下文中有许多应用,详情请参阅其他教科书。

3.6 Python的if语句

通常,Python按语句编写的顺序依次执行。if语句是改变执行顺序的最简单方法,每种程序设计语言都包含if语句。Python中最简单的if语句语法格式如下:
if <布尔表达式>:

<代码块1>

<代码块2>
<布尔表达式>的求值结果必须为True或者False。如果为True,则执行<代码块1>,然后执行<代码块2>(程序的剩余部分);如果<布尔表达式>为False,则仅执行
<代码块2>。注意,if语句以冒号(:)结束,这表明必须紧跟一个代码块。不使用括号分隔符的优点是逻辑更简单,但缺点是必须仔细保证缩进的一致性。所有支持Python语言的编辑器都会自动进行处理。
以下代码片段是if语句和字符串的简单应用示例:

x=0.47
if 0<x<1:
    print "x lies between zero and one."
y=4

if语句的简单通用语法格式如下:
if <布尔表达式>:

<代码块1>

else:

<代码块2>

<代码块3>
执行<代码块1>或者<代码块2>,然后执行<代码块3>。
我们可以级联if语句,并且可以使用一个简便缩写elif。注意,必须提供所有的逻辑代码块,如果特定的代码块不需要执行任何操作,则需要包含一条空语句pass,例如:
if <布尔表达式1>:

<代码块1>

elif <布尔表达式2>:

<代码块2>

elif <布尔表达式3>:

pass

else:

<代码块4>

<代码块5>
如果<布尔表达式1>为True,则执行<代码块1>和<代码块5>;如果<布尔表达式1>为False并且<布尔表达式2>为True,则执行<代码块2>和<代码块5>。但是,如果<布尔表达式1>和<布尔表达式2>均为False,而<布尔表达式3>为True,则仅仅执行<代码块5>。如果<布尔表达式1>、<布尔表达式2>和<布尔表达式3>均为False,则执行<代码块4>和<代码块5>。
经常出现的情况是一种具有简洁表达式的结构,例如:
if x>=0:

y=f

else:

y=g

在C语言家族中,有一种缩写形式。在Python语言中,上述代码片段可以简写为如下清晰明了的一条语句:
y=f if x>=0 else g

3.7 循环结构

计算机能够快速地重复一系列动作。Python包含两种循环结构:for循环结构和while循环结构。

3.7.1 Python的for循环结构

这是最简单的循环结构,所有的程序设计语言都包含该结构,例如C语言家族中的for循环结构和Fortran语言中的do循环结构。Python循环结构是这些循环结构中更为通用、更为复杂的演化升级。其最简单的语法格式如下:
for <迭代变量> in <可迭代对象>:

<代码块>

这里<可迭代对象>(iterable)是任何容器对象。<迭代变量>(iterator)是可以用来逐个访问容器对象的元素的任何变量。如果<可迭代对象>是一个有序容器(如列表a),那么<迭代变量>可以是索引列表范围内的整数i。上面的代码将包括类似于a[i]的引用。
这些听起来很抽象,所以需要详细说明。许多传统的C语言和Fortran语言的用途将被推迟到第4章,因为这些语言只能为本章描述的核心Python提供非常低效的实现。我们从一个简单但非常规的例子开始。

c=4
for c in "Python":
    print c

此处使用c作为循环迭代变量,将覆盖前面标识符c的用途。针对字符串迭代对象中的每一个字符,执行代码块(此处仅打印输出其值)。当循环完所有的字符后,循环终止,c指向最后一个循环值。
乍一看,似乎<迭代变量>和<可迭代对象>都必须是单个对象,但我们可以通过使用元组来绕过这个要求(该方法经常被使用)。例如,假设Z是一个长度为2的元组列表,则包含两个元组变量的语法格式如下:
for (x,y) in Z:

<代码块>

这是完全允许的。对于另一种更一般的用法,请参见将在4.4.1节介绍的zip函数。
在展示更传统的用法之前,我们需要介绍Python内置的range函数。其一般语法格式如下:
range(start,end,step)
range函数生成一个整数列表:[start,start+step,start+2*step,...],每个整数都小于end。(我们在3.5.3节曾讨论过这个概念。)此处,step是可选参数,其缺省值为1;start也是可选参数,其缺省值为0。因此range(4)的结果为[0,1,2,3]。请阅读如下代码:

L=[1,4,9,16,25,36]
for it in range(len(L)):
    L[it]+=1
L

注意,循环是在for语句的执行过程中设置的。代码块可以改变迭代变量,但不能改变循环。请尝试运行如下简单示例:

for it in range(4):
    it*=2
    print it
it

应该强调的是,这些例子中的循环体没有太大意义,但实际不一定如此。Python提供了两种不同的方法来动态地改变循环执行过程中的控制流。在实际情况中,它们当然可以出现在同一个循环内。

3.7.2 Python的continue语句

请阅读如下语法格式示例:
for <迭代变量> in <可迭代对象>:

<代码块1>
if <测试1>:
    continue
<代码块2>

<代码块5>
这里的<测试1>返回一个布尔值,可以假设是<代码块1>中行为的结果。在每次循环时,都会检查其值,当其结果为True时,控制将传递到循环的顶部,从而递增<迭代变量>,然后进行下一次循环。如果<测试1>返回False,则执行<代码块2>,之后控制传递到循环的顶部。在循环结束(以通常方式)后,执行<代码块5>。

3.7.3 Python的break语句

break语句允许中断循环,也可以通过使用一个else子句,得到不同的结果。其基本语法格式如下:
for <迭代变量> in <可迭代对象>:

<代码块1>
if <测试2>:
    break
<代码块2>

else:

<代码块4>

<代码块5>
如果在任何一次循环中<测试2>的求值结果为True,则退出循环,并且控制传递到<代码块5>。如果<测试2>的求值结果始终是False,则循环以正常的方式终止,并且控制首先传递到<代码块4>,最后传递到<代码块5>。作者发现这里的控制流有悖直觉,但在此上下文中使用else子句是可选的,而且很少见。请阅读以下的简单示例。
y=107
for x in range(2,y):

if y%x == 0:
   print y, " has a factor ", x
   break

else:

print y, " is prime."

3.7.4 列表解析

一种意想不到但常常需要完成的任务是:给定一个列表L1,需要构造第二个列表L2,它的元素是第一个列表相应元素的某个固定函数的值。传统的方法是通过一个for循环来实现。例如,生成一个列表的各元素的平方的列表。

L1=[2,3,5,7,11,14]
L2=[]        # Empty list
for i in range(len(L1)):
    L2.append(L1[i]**2)
L2

然而,Python可以通过列表解析(list comprehension)来使用一行代码实现这种循环操作:

L1=[2,3,5,7,11,14]
L2=[x**2 for x in L1]
L2

列表解析不仅更简洁,而且更快速,特别是针对长列表,因为无须显式构造for循环结构。
列表解析的用途比上述代码更加广泛。假设我们仅仅需要为列表L1中的奇数元素构造列表L2,则代码如下:

L2=[x*x for x in L1 if x%2]

假设有一个平面上的点的列表,其中点的坐标存储为元组,并且还要求计算这些点和原点的欧几里得距离。相应的代码如下:

import math
lpoints=[(1,0),(1,1),(4,3),(5,12)]
ldists=[math.sqrt(x*x+y*y) for (x,y) in lpoints]

接下来有一个矩形网格的坐标点,其中x坐标存储在一个列表中,y坐标存储在另一个列表中。可以使用如下代码来计算距离列表:

l_x=[0,2,3,4]
l_y=[1,2]
l_dist=[math.sqrt(x*x+y*y) for x in l_x for y in l_y]

列表解析是Python的一个特性,尽管最初不容易理解,但还是非常值得掌握的。

3.7.5 Python的while循环

Python语言支持的另一种非常有用的循环结构是while循环。其最简单的语法格式如下:
while <测试表达式>:

<代码块1>

<代码块2>
这里<测试表达式>是一个表达式,其求值结果为布尔对象。如果求值结果为True,则执行<代码块1>;否则控制权转移到<代码块2>。每次结束执行<代码块1>后,重新求<测试表达式>,并重复该过程。因此下列代码片段在没有外界干扰的情况下将无限循环:

while True :
    print "Type Control-C to stop this!"

和for循环结构一样,while循环中也可以使用else、continue和break子句。continue和break子句同样可用于缩减循环执行步骤或者退出循环。特别值得注意的是,如果上述代码片段中使用了break子句,则将变得大有用途。这些在3.7.3节曾讨论过。

3.8 函数

函数(或者子程序)是将一系列语句组合在一起,并且可以在程序中执行任意次数。为了增加通用性,我们提供可以在调用时改变的输入参数。函数可以返回数据,也可以不返回数据。
在Python中,函数和其他任何东西一样,都是对象。我们首先讨论函数的基本语法和作用范围的概念,然后在3.8.2~3.8.5节中讨论输入参数的性质。(这个顺序似乎不合逻辑,但输入参数的多样性是极其丰富的。)

3.8.1 语法和作用范围

Python函数可以定义在程序的任何地方,但必须是在实际使用之前。其基本语法如下面的伪代码所示:
def <函数名称>(<形参列表>):

<函数体>

关键字def表示函数定义的开始;<函数名称>指定一个标识符或者名称来命名函数对象。可以使用满足通常规则的标识符名称,当然稍后也可以修改标识符名称。括号()是必需的。在括号中,可以插入用逗号分隔的零个、一个或者多个变量名,称之为参数。最后的冒号也是必需的。
接下来是函数的主体,即要执行的语句系列。正如我们已经看到的,这些代码块必须缩进。函数体的结束由返回到与def语句相同的缩进水平来标识。在极少数情况下,我们可能需要定义一个函数,但延迟实现函数体的内容;在这个初始阶段,函数体应该使用一条空语句pass。虽然不是必需的,但这是惯例并且强烈推荐,在函数头和函数体之间包含文档字符串(docstring),用以描述函数的具体功能。文档字符串是用一对三引号括起来的任意格式的文本,可以跨越一行或者多行。包含文档字符串信息可能看起来无关紧要,但事实上十分重要。函数len的作者编写了该函数的文档字符串,因此用户可以在3.5.1节通过len?获取文档字符串的帮助信息。
函数体的定义引入了一个新的私有名称空间,当函数体代码的执行结束时,该私有名称空间将被销毁。调用函数时,这个名称空间将导入def语句中作为参数的标识符,并将指向调用函数时参数所指向的对象。在函数体中引入的新标识符也属于这个名称空间。当然,该函数是在包含其他外部标识符的名称空间内定义的。那些与函数参数或者函数体中已经定义好的标识符具有相同名称的标识符在私有名称空间中不存在,因为它们会被那些私有参数覆盖。其他名称在私有名称空间中是可见的,但强烈建议不要使用它们,除非用户绝对确定每次调用函数时它们都将指向相同的对象。为了在定义函数时确保可移植性,尝试只使用参数列表中包含的标识符以及在私有名称空间中定义的标识符,这些名称只属于私有名称空间。
通常我们要求函数生成一些对象或者相关变量(例如y),这可以通过返回语句来实现,例如return y。函数在执行返回语句之后会退出,即返回语句将是最后执行的语句,因此通常是函数体中的最后一条语句。原则上,y应该是标量,但这很容易通过使用元组来规避。例如,为了返回三个标量(例如u、v和w),应该使用元组,例如return (u, v, w),甚至直接使用return u, v, w。如果没有返回语句,则Python会插入一条不可见的返回语句return None。这里None是一个特殊的Python变量,它指向一个空对象,并且是函数返回的“值”。这样,Python就避免了Fortran语言中必须将函数和过程分开的二分法。
下面是一些简单的用于说明上述特性的示例。请尝试在解释器中输入并运行这些代码片段,以便验证上面讨论的知识点,并进行进一步的实验。

1 def add_one(x):
2    """ Takes x and returns x+1. """
3    x = x+1
4    return x
5
6 x=23
7 add_one?         # or help(add_one)
8 add_one(0.456)
9 x

在第1~4行中,我们定义函数add_one(x)。在第1行中只有一个参数x,它引用的对象在第3行中被改变。在第6行中,我们引入了一个由x引用的整数对象。接下来的两行代码分别对文档字符串和函数进行测试。最后一行检查x的值,该值保持不变且一直为23,尽管我们在第8行中隐式地将x赋值为一个浮点数。
接下来请阅读如下包含错误的代码:

1 def add_y(x):
2    """ Adds y to x and returns x+y. """
3    return x+y
4
5 add_y(0.456)

在第5行,私有变量x被赋值为0.456,而第3行查找私有名称y;但没有找到,所以函数在包含该函数的封闭名称空间中查找标识符y,结果也找不到,故而Python终止运行,并打印输出一个错误。但是,如果我们在调用函数之前引入y的实例:

y=1
add_y(0.456)

虽然函数按预期正常运行,但这是不可移植的行为。只有在y的实例已经被定义的情况下,我们才能使用名称空间内的函数。在一些情况下,该条件可以得到满足,但一般来说,应该避免使用这种类型的功能。
下面的示例显示了更好的代码实现,该示例还显示了如何通过元组返回多个值,并且显示了函数也是对象。

1 def add_x_and_y(x, y):
2    """ Add x and y and return them and their sum. """
3    z=x+y
4    return x,y,z
5
6 a, b, c = add_x_and_y(1,0.456)
7 a, b, c

标识符z是函数的私有名称,并且在函数退出后不再可用。因为我们将c分配给z所指向的对象,所以当标识符z消失后,对象本身不会丢失。在接下来的两个代码片段中,我们将展示函数是对象,并且我们可以给它们分配新的标识符。(重新查看图3-1,可以帮助理解该知识点。)

f = add_x_and_y
f(0.456, 1)
f

在上述这些示例中,作为参数的对象都是不可变对象,因此函数调用没有修改它们的值。然而,当参数是可变(容器)对象的时候,情况则并非如此。请参见如下示例:

1 L = [0,1,2]
2 print id(L)
3 def add_with_side_effects(M):
4    """ Increment first element of list. """
5    M[0]+=1
6
7 add_with_side_effects(L)
8 print L
9 id(L)

列表L本身并没有变化。但是,在没有使用赋值运算符(在函数体之外)的情况下,列表L的内容可以并且已经被修改了。这种副作用(side effect)在此上下文中没有问题,但在实际应用代码中,则有可能导致细微的难以觉察的错误。补救方法是拷贝一个副本,如下述代码第5行所示。

 1 L = [0,1,2]
 2 id(L)
 3 def add_without_side_effects(M):
 4    """ Increment first element of list. """
 5    MC=M[ : ]
 6    MC[0]+=1
 7    return MC
 8
 9 L = add_without_side_effects(L)
10 L
11 id(L)

在某些情况下,拷贝一个长列表会产生额外的开销,从而影响代码的速度,因此一般会避免拷贝长列表。然而,在使用带副作用的函数之前,请牢记这句话:“过早优化是万恶之源”,并谨慎使用带副作用的函数。

3.8.2 位置参数

位置参数(positional argument)是所有程序设计语言的共同惯例。请阅读如下示例:
def foo1(a,b,c):

<函数体>

每次调用函数foo1时,都必须精确指定三个参数。一个调用示例为y=foo1(3,2,1),
很显然参数替换按照其位置顺序进行。另一种调用该函数的方法是y=foo1(c=1,a=3,b=2),
这允许更加灵活的参数顺序。指定三个以外个数的参数将导致错误。

3.8.3 关键字参数

另一种函数定义形式指定关键字参数(keyword argument),例如:
def foo2(d=21.2, e=4, f='a'):

<代码块>

调用这类函数时,既可以指定所有的参数,也可以省略部分参数(省略的参数使用def语句中定义的缺省值)。例如,调用foo2(f='b')将使用缺省值d=21.2和e=4,从而满足三个参数的要求。由于是关键字参数,因此其位置顺序不重要。
在同一个函数定义中可以结合使用位置参数和关键字参数,但所有的位置参数都必须位于关键字参数之前。例如:
def foo3(a,b,c,d=21.2,e=4,f='a')

<代码块>

调用该函数时,必须指定三到六个参数,且前三个参数为位置参数。

3.8.4 可变数量的位置参数

我们常常事先并不知道需要多少个参数。例如,假设要设计一个print函数,则无法事先指定要打印输出的项目的个数。Python使用元组来解决这个问题,print函数将在3.8.7节讨论。这里有一个更简单的示例,用于说明其语法、方法和用法。给定任意数量的数值,要求计算这些数值的算术平均值。

1 def average(*args):
2    """ Return mean of a non-empty tuple of numbers. """
3    print args
4    sum=0.0
5    for x in args:
6        sum+=x
7    return sum/len(args)
8
9 print average(1,2,3,4)
10 print average(1,2,3,4,5)

按照惯例(但不是强制性的)在定义中把元组取名为args,注意这里的星号是必需的。第3行是多余的演示代码,其目的只是为了说明所提供的参数真的被封装成元组。注意,通过在第4行中把sum强制定义为实数,可以确保第7行中的除法按预期工作(即实数除法),即使分母是整数。

3.8.5 可变数量的关键字参数

Python可以完美处理下列形式的函数:该函数接受固定数量的位置参数;随后是任意数量的位置参数;再随后是任意数量的关键字参数—因为必须遵守“位置参数位于关键字参数之前”的顺序规则。如前一节所述,附加的位置参数被打包成元组(由星号标识)。附加的关键字参数则被封装到字典中(由两个星号标识)。如下示例说明了这个过程:

1 def show(a, b, *args, **kwargs):
2    print a, b, args, kwargs
3
4 show(1.3,2.7,3,'a',4.2,alpha=0.99,gamma=5.67)

初学者不太可能主动地使用所有这些类型的参数。然而,读者偶尔会在库函数的文档字符串中看到它们,因此理解其用法可以帮助理解文档。

3.8.6 Python的输入/输出函数

每种程序设计语言都需要具有接受输入数据或者输出其他数据的函数,Python也不例外。输入数据通常来自键盘或者文件,而输出数据通常被“打印”输出到屏幕或者文件。文件输入/输出既可以是可读的文本数据,也可以是二进制数据。
我们将先讨论数据输出然后再讨论数据输入。对于科技工作者而言,绝大多数文件输入/输出将主要涉及数值数据,因此被推迟到4.4.1~4.4.3节讨论。数据的输出是一个复杂的问题,将在本节和接下来的###3.8.7节中讨论。
从键盘输入少量数据有多种方法,这里选择最简单的解决方案。让我们从如下简单的代码片段开始:

name = raw_input("What is your name? ")
print "Your name is " + name

执行第一条语句时会提示一个问题“What is your name?”,随后键盘输入将捕获到一个字符串。因此第二条语句的含义是输出两个拼接的字符串。
现在假设我们希望从键盘输入一个列表(例如[1,2,3])。如果使用上面的代码片段,则name指向一个字符串,因而需要使用语句eval(name)从字符串中构造出列表对象(参见3.5.6节)。请阅读如下的另一个代码片段:

ilist = input('Enter an explicit list')
ilist

假如我们输入[1,2,3],则同样起作用。如果预先定义了一个Python对象的标识符objname,则也可以输入该标识符。类似的代码片段应该可以处理绝大多数键盘输入任务。

3.8.7 Python的print函数

到前两节内容为止,我们并不需要输出命令或者输出函数。在解释器中,我们可以通过键入标识符来“输出”任何Python对象。然而一旦开始编写函数或者程序,我们就需要一个输出函数。这里有些麻烦:在Python 3.0以前的版本中,print是一条命令,其调用方法如下:
print <要打印输出的内容>
而在Python 3.0及以后的版本中,print被实现为一个函数,于是上述代码行书写为:
Print(<要打印输出的内容>)
在编写本书时,部分NumPy及其主要扩展仅支持较早的版本。由于早期版本的Python可能最终会被废弃,因此数值处理的用户会面临潜在的软件过时的困境,然而,这里有两种简单的解决方案。print函数要求一个可变数量的参数,即一个元组。如果我们在早期的Python版本中使用上面第二种代码片段,则print命令把参数看作是一个显式的元组,因为<要打印输出的内容>被包含在括号中。如果感觉多余的括号有些别扭,则可以使用另一种解决方案,即在代码的顶部包含如下代码行:
from future import print_function
如果在Python 3.0以前版本中使用Python 3.0及以后版本的print函数,则请删除该语句。
print命令要求使用一个元组作为参数。因此,假设it指向一个整数,y指向一个浮点数,不使用元组分隔符(括号),则可以编写如下语句:

print "After iteration ",it,", the solution was ",y

这里没有控制两个数值的格式,因此会导致潜在的问题,例如,在输出表格内容时需要高度一致的格式。首先,尝试如下稍微复杂的一种解决方法:
print "After iteration %d, the solution was %f" % (it,y)
格式化字符串的通用语法格式如下:
%
其中,格式化字符串中的%d项被替换为一个整数,而%f项则被替换为一个浮点数,这两个数按顺序从参数中的最终元组获得。这个版本的输出结果与上一个版本的输出结果相同,但我们还可以进一步改进。下面列举的格式化代码是基于C语言家族中有关printf函数的定义。
首先考虑整数it的格式,其中it的值为41。如果把代码中的格式化字符串%d替换为%5d,则输出将包括5个字符,数值右对齐,也即“41”,其中字符 表示空白符。同样%-5d将输出左对齐结果“41”。另外,%05d将输出结果“00041”。如果数值是负数,则符号将包含在字段的计数中。因此,如果同时输出正整数和负整数,则可能导致结果不整齐。我们可以强制输出以正号或者负号开始,右对齐时选择使用%+5d,而左对齐时选择使用%+-5d。当然,字段宽度5没有特殊含义,可以用任何其他合适的数字来替代。实际上,当整数的精确表示要求比指定宽度更多的位数时,Python会忽略格式化字符串的指示以保证输出精度。
格式化输出浮点数则有三种可能性。假设y的值为123.456789。如果把格式化字符串%f替换为%.3f,则输出结果为123.457,即浮点数值被四舍五入到保留3位小数。格式化字符串代码%10.3f输出右对齐10个字符宽度的字符串,也即“123.457”,而
%-10.3f的输出结果相同,只是左对齐而已。与整数格式化一样,紧跟在百分号后面的正号强制输出结果带正号或者负号。另外,%010.3f会把前面的空白符替换为0。
很显然,当y非常大或者非常小的时候,%f格式将损失精度,例如z=1234567.89的情形。在输出的时候,我们可以这样书写:“z=1.23456789×106”,而Python的输出则是z=1.23456789e6。现在当输出格式化字符串代码为%13.4e时,应用到z,其输出结果为“1.2346e+06”。小数点后恰好有4位数字,输出数值为13个字符宽度的字段。在该输出表示中,仅需要10个字符并且数值右对齐,所以左侧包含3个空白符。同上,
%-13.4e的输出结果为左对齐,%+13.4e输出结果的前面包含一个正号和2个空白符。(%+-13.4e结果相同但是左对齐。)如果宽度少于最低10个的要求,则Python会把宽度增加到10。最后,在格式化字符串代码中把'e'替换为'E',则结果使用大写的字母E,例如%+-13.4E的输出结果为“+1.2346E+06”。
有时候我们要求输出一个绝对值范围变化巨大的浮点数,但是希望显示特定位数的有效数字。以上面的z为例,%.4g将输出1.235e+06,即正好保留4位有效数字。注意,%g默认等同于%.6g。Python将选择使用'e'和'f'中较短的一种。同样,%.4G将在'E'和'f'之间选择。
考虑完整性,我们注意到,Python也提供字符串变量的格式化字符串代码,例如,%20s将输出一个至少有20个字符宽度的字符串,如果需要则在左边填充空白符。

3.8.8 匿名函数

很显然可以任意指定一个函数参数的名称,即f(x)和f(y)指向同一个函数。另一方面,我们在函数add_x_and_y(见3.8.1节)的代码片段中观察到,可以改变f的名称而不会导致不一致。这是数学逻辑的基本原理,通常用lambda演算(也即λ演算)的形式论来描述。在Python编码中,会出现函数名称完全不相关的情况,并且Python可以模拟lambda演算。我们可以把add_x_and_y编写为如下代码:
f= lambda x, y : x, y, x+y
或者:
lambda x,y:x,y,x+y
有关匿名函数的实际应用案例,请参见4.1.5节和8.5.3节。

3.9 Python类简介

在Python语言中,类是极其通用的结构。正因如此,有关类的文档既冗长又复杂。参考书籍中的例子通常不是取材于科学计算中的数据处理,而且往往过于简单或过于复杂。基本思想是,读者可能拥有经常发生的固定数据结构或者对象,以及直接与之关联的操作。Python类既封装对象又封装其操作。我们用一个科学计算示例来进行简单的介绍性陈述,但是却包含许多科学家最常用的特性。在这一教学背景下,我们将使用整数运算。
我们以分数为例。分数可以被认为是实数的任意精度表示。我们考虑将一个Frac类实现为一对整数num和den,其中den为非0整数。值得注意的是,3/7和24/56通常被视为相同的数值。我们已经在第2章的后半部分讨论了这个特殊的问题,需要使用第2章创建的文件gcd.py。下面的代码片段显示了Frac类的基本结构(借助于gcd.py文件)。

1 # File frac.py
2
3 import gcd
4
5 class Frac:
6    """ Fractional class. A Frac is a pair of integers num, den
7    (with den!=0) whose GCD is 1.
 8    """
 9
10    def __init__(self,n,d):
11        """ Construct a Frac from integers n and d.
12            Needs error message if d=0!
13        """
14        hcf=gcd.gcd(n, d)
15        self.num, self.den = n/hcf, d/hcf
16
17    def __str__(self):
18        """ Generate a string representation of a Frac. """
19        return "%d/%d" % (self.num,self.den)
20
21    def __mul__(self,another):
22        """ Multiply two Fracs to produce a Frac. """
23        return Frac(self.num*another.num, self.den*another.den)
24
25    def __add__(self,another):
26        """ Add two Fracs to produce a Frac. """
27        return Frac(self.num*another.den+self.den*another.num,
28                  self.den*another.den)
29
30    def to_real(self):
31        """ Return floating point value of Frac. """
32        return float(self.num)/float(self.den)
33
34 if __name__=="__main__":
35    a=Frac(3,7)
36    b=Frac(24,56)
37    print "a.num= ",a.num, ", b.den= ",b.den
38    print a
39    print b
40    print "floating point value of a is ", a.to_real()
41    print "product= ",a*b,", sum= ",a+b

这里的第一个新知识点是第5行中的class语句。请注意终止冒号。实际的类是通过缩进代码块定义的,即示例中从第5行一直到第32行。第6~8行定义了类的文档字符串,用于在线帮助文档。在类体中,我们定义了五个类函数,函数相对于类缩进(因为它们属于类成员),并且函数也有进一步缩进的函数体(和通常一样)。
第一个类函数从第10~15行,这在其他程序设计语言中被称为“构造函数”,其目的是把一对整数转换为一个Frac对象。函数的名称必须为__init__(似乎有点奇怪),但我们将看到,在类的外部从来不会用到该名称。类函数的第一个参数通常被称为self,同样只出现在类的定义中。这些都显得十分陌生,所以接下来我们看看位于第35行的解释测试集代码a=Frac(3,7)。这条语句把一对整数3和7对应的Frac对象赋值给标识符a。该语句隐式地调用__init__函数,并使用a代替self,3和7代替n和d。然后计算hcf,即3和7的最大公约数(GCD,结果为1),并在第15行代码中计算a.num=n/hcf和a.den=d/hcf。这些都可以以通常的方式进行访问,所以第37行代码打印输出分数的分子和分母的值。同理,在第36行代码的赋值语句中,调用__init__,并使用b替换self。
几乎所有的类都能通过提供类似第38~39行的代码而获得方便,即“打印输出”类的对象。这就是类的字符串函数__str__的目的,在代码的第17~19行中进行了定义。当执行第38行的代码时,将调用__str__函数,用a替换self;结果第19行返回字符串"3/7",而这就是打印输出的结果。
虽然这些类的函数或多或少都比较简单,但我们可以定义许多(或者一些)函数来执行类操作。让我们先关注乘法和加法,根据分数标准的运算规则:
image.png
两个Frac对象的乘法要求定义第21~23行的类的函数__mul__。当在第41行中调用ab时,将调用该函数,使用左侧操作数(即a)替换self,使用右侧操作数(即b)替换another。注意第23行代码计算乘积的分子和分母,然后调用__init__来创建一个新的Frac对象,因此c=ab将创建一个标识符为c的新的Frac对象。而在第41行代码中,则创建一个匿名的Frac对象并立即传递给__str__。上面的代码片段使用了同样的方法来处理加法运算。
注意在第37行代码中,我们可以直接访问类的对象实例a的构成部分。同样,在第40行代码中,我们使用了与类实例相关联的函数to_real()。它们都是类的属性,我们将越来越多地使用“点(.)访问机制”,这是Python最广泛的使用方式,请参见下一节。
考虑到简洁性,我们没有实现类的所有功能。对于初学者而言,以更高级的方式逐步完善代码将是非常有益的训练。
1.分别创建名为__div__和__sub__的除法和减法类函数,并测试。
2.如果分母为1,则打印输出的结果会显得有些奇怪,例如7/1。请改进__str__函数,使得当self.den为1时,创建的字符串恰好是self.num,并测试新版本是否正常工作。
3.当参数d为0时,很显然__init__会出错。请为用户提供警告信息。

3.10 Python程序结构

在3.2节的末尾,我们讨论了标识符和对象的关系,这里我们进一步展开这个话题。在3.9节有关Python类的教学示例中,我们注意到类Frac的一个实例对象,例如a=Frac(3,7),其将创建一个标识符a,指向类Frac的一个对象。现在一个Frac对象包含数据(一对整数)以及若干操作这些数据的函数。我们通过“点访问机制”来访问与对象实例关联的数据,例如a.num。同样,我们也可以访问相关联的函数,例如a.to_real()。
到目前为止,这里只是总结前文所陈述的事实。然而,Python中充满了各种各样的对象,有些非常复杂,这种“点访问机制”被广泛用于访问对象的组件。我们已经学习了很多有关Python的知识,下面给出一些示例。
我们的第一个示例是3.3.4节中讨论的复数。假设我们有c=1.5-0.4j,或者等价地c=complex(1.5,-0.4)。我们应该把复数看作是类Complex的对象,即使其为了保证效率而被内置到系统中。接下来,类似于Frac类的操作,我们可以通过c.real和c.imag来访问其数据,而c.conjugate()则创建了一个共轭复数1.5+0.4j。这些都是有关实例对象和属性的进一步示例。我们说Python是面向对象的程序设计语言。而在面向函数的程序设计语言(例如Fortran77)中,则会使用C=CMPLX(1.5,-0.40)、REAL(C)、AIMAG(C)和CONJG(C)。编写简单程序时,无须侧重于哪一种程序设计方法。
我们的下一个示例针对3.4节中讨论的模块。和Python语言的其他特性一样,模块也是对象。因此import math as re将包含math模块并为其指定一个标识符re。我们可以访问其数据(例如re.pi),也可以访问其函数,例如re.gamma(2)。
一旦读者掌握了“点访问机制”,那么理解Python语言将变得更加简单。例如,请参见3.5节中有关容器对象的讨论。所有更加复杂的包(如NumPy、Matplotlib、Mayavi、SymPy和Pandas)都是基于该基础。正是在这个级别上,面向对象的方法在提供C或者Fortran的早期版本中不具备的统一环境方面占据了优势。

3.11 素数:实用示例

本章最后通过一个实际问题来讨论“纯”Python。互联网使通信发生了革命性的变化,并且强调了安全传输数据的必要性。这种安全性在很大程度上是基于这样一个事实,即给定一个大整数n(例如n>10100),很难确定其是否可以表示成若干素数的乘积。我们来讨论一个更基本的问题:构建一个素数列表。
让我们回顾素数的定义:一个整数p如果不能表示成整数q和r(均大于1)的乘积q×r,则p是素数。因此,最初的几个素数是2、3、5、7…。确定小于或者等于给定整数n的所有素数的问题已经研究了几千年,也许最著名的方法是埃拉托色尼筛选法(Sieve of Eratosthenes)。在表3-1中描述了n=18的情况。表标题说明了其筛选过程的工作原理,并且显示了前3个步骤。注意,由于要删除的任何合数都≤n,其中至少一个因子。在本例中,,因此筛选过程不需要删除5、7…。读者也许已经注意到,在表3-1中,我们包含了一个绝对多余的行,其目的是阐明这一点。
表3-1 求素数(≤18)的埃拉托色尼筛选法。首先,我们在第1行写下从2到18的整数;在第2行中,从最左边开始删除所有2的倍数;然后我们处理最接近的剩余整数,这里是3,并在第3行中删除所有3的倍数。继续这个过程。很显然,剩下的数字不是整数的乘积,即它们是素数
image.png
直接实现该过程的Python函数代码如下所示:

 1 def sieve_v1(n):
 2    """
 3        Use Sieve of Eratosthenes to compute list of primes <= n.
 4        Version 1
 5    """
 6    primes=range(2,n+1)
 7    for p in primes:
 8        if p*p>n:
 9            break
10        product=2*p
11        while product<=n:
12            if product in primes:
13                primes.remove(product)
14            product+=p
15    return len(primes),primes

前5行是常规代码。在第6行中,我们将表3-1的顶部行编码为Python列表,称为primes,用于筛选。接下来,我们介绍一个for循环(第7~14行的代码),循环变量p的每一个值对应于表中的下一行。正如我们上面讨论的结果,我们不需要考虑,这将在第8行和第9行中测试。break命令将控制转移到循环结束后的语句,即第15行(这可以通过缩进来观察到)。接下来讨论while循环(第11~14行的代码)。正是因为前面出现的break语句,才保证了while循环至少执行一次。然后,如果product仍然在列表中,则第13行会将其删除。在3.5.1节中,我们了解到list.append(item)把item附加到list中,这里list.remove(item)从list中删除第一次出现的item。如果list中不包括item,则会出错,因此第12行代码确保其存在。(请读者尝试help(list)或者list?,以查看有关列表的帮助文档信息。)删除了可能存在的2p,接下来在第14行中构造3p并重复该过程。一旦通过while循环的迭代删除了p的所有倍数,程序便返回到第7行,并将p设置为列表中的下一个素数。最后,我们返回素数列表及其长度。后者是数论中的一个重要函数,通常表示为π(n)。
建议读者创建一个名为sieves.py的文件,并输入或者拷贝上述代码片段到文件中。然后在IPython中输入如下命令:
from sieves import sieve_v1
sieve_v1?
sieve_v1(18)
这样可以验证程序是否正常工作。读者还可以通过如下命令来检查程序运行消耗的时间:
timeit sieve_v1(1000)
表3-2中的结果表明,虽然这个简单直接的函数的性能对于小n来说是令人满意的,但对于即便是中等大小的整数值而言,所耗费的时间也变得非常大,令人无法接受。考虑如何方便地提高其性能将是一个非常有用的练习实践。
表3-2 素数个数π(n)≤n,以及使用上述代码片段中的Python函数在作者笔记本电脑上计算运行耗费的大约时间。这里关注的是相对时间(而不是绝对时间)
image.png
我们首先考虑算法,它包含两个循环。不可避免地,我们必须对实际素数进行迭代,但是对于每个素数p,我们进行循环,并从循环中去除合数n×p,其中n=2,3,4,…。我们可以在这里做两处非常简单的改进。注意,假设p>2,那么任何小于p2的合数都将从筛子中移除,因此我们可以从移除合数p2开始。此外,如果n为奇数,则合数p2+n×p是偶数,因此也在第一遍的筛选中被去除。因此,针对每个素数p>2,我们可以通过去除p2,p2+2p,p2+4p,…来改进算法。虽然这并不是最好的方法(例如,63被筛过两次),但这就足够满足要求。
接下来我们讨论代码实现。代码中包含了5个循环,这相当浪费。for循环和while循环都是显式的。第12行中的if语句涉及遍历素数列表。这在第13行中被重复,而在找到并丢弃product后,列表中的剩余元素都需要向下移动一个位置。请阅读如下重构代码(虽然其逻辑不那么明显):

1 def sieve_v2(n):
2    """
3        Sieve of Eratosthenes to compute list of primes <= n.
4        Version 2.
5    """
6    sieve = [True]*(n+1)
7    for i in xrange(3,n+1,2):
8        if i*i > n:
9            break
10        if sieve[i]:
11            sieve[i*i: :2*i]=[False]*((n - i*i) // (2*i) + 1)
12    answer = [2] + [i for i in xrange(3,n+1,2) if sieve[i]]
13    return len(answer), answer

第6行代码中,把筛选器实现为一个布尔值列表,所有的元素都初始化为True。相比同样长度的整数列表,布尔值列表更容易设定,并且占用更少的内存空间。外层循环是一个for循环(第7~11行的代码)。第7行的xrange是一个新函数,其作用和range函数类似,区别是不会在内存中创建整个列表,而是按需生成列表的元素。对于大型列表,这种操作会更快并且占用更少的内存。这里的循环覆盖了闭区间[3, n]中的所有奇数。和上一个版本一样,一旦,则第8行和第9行会终止外层循环。接下来讨论第10行和第11行。开始时i为3,并且sieve[i]为True。我们需要设定筛选器的第i2,i2+2i,…个为False,其作用等同于在实现中丢弃这些元素。第11行的代码使用了单行切片方法来实现该操作,而没有使用循环结构(右边的整数因子给出切片的维度)。在for循环结束时,列表sieve中与所有奇合数对应的索引位置的元素都会被设置为False。最后,第12行代码构建出素数列表。我们从包含一个元素2的列表[2]开始,使用一个列表解析式来构造一个包含所有奇数并且未筛除的数值,并把这两个列表拼接在一起,然后返回结果。
虽然一开始理解这个版本的代码可能需要一些时间,但程序并没有使用新的知识(除了xrange),代码长度缩短了25%,并且运行速度大大提高了,如表3-2中的最后一列所示。事实上,它能在几秒钟的时间内筛选出超过百万的小于108的素数。如果扩展到109,则需要耗时几分钟,并且需要几十GB的内存。很显然,对于非常大的素数,筛选方法不是最有效的算法。
同时请注意,在第2个版本的列表中,所有项都具有相同的类型。虽然Python没有强加这个限制,但引入一个新对象(同构类型的列表)是值得的,这自然而然就引出了下一章的主题:NumPy。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接