带你读《Python科学计算(原书第2版)》之二:IPython入门

点击查看第一章
点击查看第三章
|第2章|
Python for Scientists, Second Edition

IPython入门

IPython字面上很像一款Apple?公司开发的软件,但实际上它是一个强大的Python解释器。它由科学家们设计和编写,目的是以最少的键入工作来完成快速的代码探索和构造任务,并在需要时提供适当(甚至最多)的屏幕在线帮助。有关文档等的更多信息请参见对应网站。本章简要介绍IPython的基本使用要点。更加详细的描述请参见其他书籍,例如Rossant(2015)。
在本章中,我们将集中讨论IPython的笔记本(notebook)模式和终端(terminal)模式,并且假设读者已经构建了A.2节和A.3节中所描述的环境。在我们开始实际示例之前,请心急的读者先忍耐片刻。2.1节讨论的Tab键代码自动补全功能是最大程度减少按键次数的非同寻常但有效的方法,2.2节讨论的自省特性将展示如何快速生成相关内联信息,而不需要停下来查阅手册。

2.1 Tab键代码自动补全功能

在使用IPython解释器时,任何时候都可以使用Tab键代码自动补全功能。这意味着,无论何时开始在命令行或者单元格中键入与Python相关的名称,我们都可以暂停并按下Tab键,以查看在此上下文中与已经键入的字符相一致的可用名称列表。
例如,假设我们需要键入import matplotlib。键入i然后按Tab键将显示15个可用的代码自动补全项。通过观察,发现其中只有一个的第2个字母为m,因此再键入m然后按Tab键将完成import关键字的输入。将此扩展为import m然后按Tab键,将显示30种列表选项,通过观察,我们需要通过键入import matp然后按Tab键来完成所需代码行的输入。
上述例子显得有些做作。但是存在一个使用Tab键代码自动补全功能的更加迫切的原因。在开发代码时,我们倾向于为变量、函数等使用短名称(为了偷懒)。(在Fortran的早期版本中,名称的确被限制为6个或者8个字符,但现在长度可以是任意的。)短名称通常意义不明确,其潜在的危险是当6个月后我们重新查看代码时,代码的意图可能不再显而易见。通过使用任意长度的有意义的名称,我们可以避免这个陷阱。而且由于Tab键代码自动补全功能,整个长名称的输入也只需一次按键。

2.2 自省

IPython能够检查几乎任何Python构造(包括其自身),并为开发人员提供其选择的任何可用信息报告。该功能被称为自省(introspection)。它是由单个字符(问号,即“?”)进行访问的。理解自省最简单的方法是使用这项功能,所以建议读者打开IPython解释器。
读者应该使用哪种模式呢?终端模式还是笔记本模式?初学者应该从终端模式开始(参见A.3节中的描述),以避免过高的复杂度。直到2.5节,我们将一直采用这种方式,因为其中涉及的代码段非常短。如果用户选择使用终端模式,则在命令行窗口中键入ipython,然后按回车键。IPython将予以响应并显示一长段标题,接着显示以“In [1]:”标注开始的用户输入行。接下来就是IPython解释器环境,读者可以尝试键入自省功能:在输入行键入?,然后按回车键。(注意,在IPython终端模式下,按回车键意味着实际执行当前行中的命令。)IPython以页面方式予以响应并显示所有可用功能一览。退出这个命令后,键入命令quickref(提示:可以使用Tab键代码自动补全功能)将显示一个更简洁的版本。强烈推荐读者仔细研究上述显示的两份帮助文档内容。
IPython笔记本用户则需要使用稍微不同的命令。在调用笔记本程序(详细内容请参阅A.2.2节)之后,呈现在用户面前的是一个未编号的单行空白单元格。当用户尝试在输入行中键入自省字符?后,此时按回车键将仅仅在单元格中增加一个新的行。为了执行单元格中的命令,则需要同时按Shift+回车键(执行命令,同时在当前单元格下面创建一个新的单元格),或者同时按Ctrl+回车键(仅仅执行命令)。而输出(长长的功能一览)则出现在屏幕底部的可滚动窗口中,通过点击右上角的x按钮可以关闭该窗口。键入命令quickref(提示:可以使用Tab键代码自动补全功能),然后同时按Ctrl+回车键将显示一个更简洁的版本。再次强烈推荐读者仔细研究上述显示的两份帮助文档内容。
然而,科学家们往往是时间宝贵的群体,本章的目的是帮助他们开始使用最有用的特征。因此,我们需要输入一些Python代码,新手必须信任这些代码,直到他们掌握了第3章和第4章的内容。同样,对于笔记本或者控制台模式,操作过程略有不同。
使用IPython笔记本的用户应该将每个代码框中的代码行或者片段键入到一个单元格中。然后可以通过按Ctrl+回车键或者Shift+回车键执行代码。使用终端模式的读者应该逐行地输入代码中的代码行或者代码片段,并通过按回车键来完成每一行的输入。
例如,请键入如下代码:

a=3
b=2.7
c=12 + 5j
s='Hello World!'
L=[a, b, c, s, 77.77]

前两行表示a引用一个整数,b引用一个浮点数。Python使用工程师的约定:(数学家可能更喜欢)。然后c引用一个复数。在第5行中分别键入a、b和c,将依次执行并显示标识符引用的对象的值。注意,显示多个值有一个有用的快捷方式—在单行上尝试键入a, b, c。接下来尝试在单行中键入c?,结果表明c确实指向一个复数。创建复数的另一种方式是c=complex(12, 5)。接下来尝试键入c.,然后按Tab键,解释器将立即提供三种可能的代码补全选项。它们代表什么意思呢?这里几乎是显而易见的,请先尝试键入c.real?。(请使用Tab键代码自动补全功能,而不需要键入eal。)结果显示c.real的值为浮点数12,即复数的实部。初学者可能会尝试c.imag。接下来请尝试键入c.conjugate?。(同样只需要5次按键加回车键!)结果表明c.conjugate是一个函数,使用方法如下:cc = c.conjugate()。
这种表述方式对Fortran或者C语言的用户而言也许有些奇特,他们可能期望类似real(c)或conjugate(c)的方式。语法的改变是因为Python是面向对象的语言。这里的对象是12 +5j,引用为变量c。因此c.real是关于对象组件的值的查询,它不会改变对象。然而,c.conjugate()则需要改变对象或者(此处)创建一个新的对象,因此它是一个函数。该表述对于所有对象都是一致的,更详细的讨论请参见3.10节。
返回到上一个代码片段,在一行中单独键入s,将输出一个字符串。我们可以键入s?和s.来确认,随后按Tab键将显示38种与字符串对象相关的代码自动补全选项。读者应该尝试使用自省方法来发现其中一些选项的具体功能。同样,键入L.?将显示L是一个列表对象,包括9个代码自动补全选项。一定要试一试!一般而言,在Python代码中的任何地方,使用自省和Tab键自动代码补全可以生成相关的帮助文档。还有进一步的自省命令??(双英文问号),用于在适当的情况下显示函数的源代码,稍后将在2.5节给出一个示例。(我们迄今为止涉及的对象函数都是内置函数,而内置函数不是使用Python语言编写的!)

2.3 历史命令

如果读者观察上一节中代码的输出结果,则会发现IPython采用了一种与Mathematica 笔记本非常类似的历史命令(history command)机制。输入行标记为In[1],In[2],…,如果输入行In[n]产生了任何输出,则将其标记为Out[n]。为了方便起见,前三个输入行/单元格可以通过变量名_i、_ii和_iii引用,而相应的输出行/单元格则可以通过_、__和___引用。但是,在实际操作中,可以通过使用键盘上的上方向键↑(或者Ctrl-p)和下方向键↓(或者Ctrl-n)导航来将前一个输入行/单元格的内容插入当前输入行/单元格中,这可能是最常见的使用方法。在终端模式中保存历史命令信息虽然非同寻常,但却方便使用。如果关闭IPython(使用exit命令)后又重新启动它,则上一个会话的历史命令记录仍然可以通过键盘上的上下方向键获得。有关历史命令机制,有很多更精细的方式,请读者尝试键入命令history?以获得相关帮助信息。

2.4 魔法命令

IPython解释器希望接收有效的Python命令。同时,还能够提供一些输入命令以控制IPython行为或底层操作系统行为。这种与Python共存的命令被称为魔法命令(magic command)。通过在解释器中键入命令%magic,可以显示一段很长的详细说明。通过键入命令%lsmagic,可以显示可用的命令简要列表。(别忘了使用Tab键代码自动补全功能!)请注意,存在两种类型的魔法命令:行魔法命令(前缀为%)和单元格魔法命令(前缀为%%)。单元格魔法命令仅与笔记本模式有关。通过使用自省功能,读者可以获取每个魔法命令的帮助文档信息。
首先让我们讨论操作系统命令。一个简单的示例是pwd,它来自UNIX操作系统,仅仅用于输出当前目录(输出工作目录)的名称并退出。在IPython窗口中通常有三种方法来实现该功能。请读者尝试如下命令。
!pwd
Python中没有任何以“!”开始的命令,并且IPython将此解释为UNIX shell命令pwd,并生成相应的ASCII文本输出结果。
%pwd
Python中没有任何以“%”开始的命令,IPython视此为行魔法命令,并将其解释为shell命令。结果字符串中的u表明结果使用Unicode编码,从而实现了丰富多样的输出。A.3.1节中将简要涉及Unicode的概念。
pwd
这是一个微妙但有用的特征。行魔法命令并不总是需要前缀%,请参见下文有关%auto-magic的讨论。由于解释器没有发现pwd的其他定义,因此将其作为魔法命令%pwd来处理。
pwd='hoho'
pwd
此时,pwd被赋值为一个字符串,因此不会作为魔法命令来处理。
%pwd
带%的魔法命令可以消除二义性。
del pwd
pwd
使用del删除变量pwd后,此时的pwd没有其他赋值,其作用又恢复为一个行魔法命令。
%automagic?
当%automagic设置为on时(默认值),所有行魔法命令前面的单个百分号(%)都可以省略。这带来了巨大的便捷性,但读者必须清醒地意识到自己正在使用魔法命令。
魔法单元格命令以双百分号(%%)开始,作用于整个单元格,功能十分强大,具体请参见下一节。

2.5 IPython实践:扩展示例

在本章的剩余部分,我们会呈现一个扩展示例的第一部分,以展示魔术命令的有效性。我们将在3.9节中讨论该扩展示例的第二部分内容,讨论如何通过分数实现任意精度的实数算术运算。关于分数有一个特点,如3/7和24/56通常被视为相同的分数。因此,这里存在一个问题,即确定两个整数a和b的“最大公因子”,或者正如数学家常用的称谓GCD,其可用于把分数a/b化简为规范形式。(通过检查因子,发现24和56的GCD为8,这意味着24/56=3/7,并且不可能进一步化简。)实现检查因子的自动化并不容易,一些研究表明可以通过欧几里得算法(Euclid’s algorithm)实现。为了简洁地表达该算法,我们需要一些专业术语。假设a和b是整数。求a整除b,其余数为a mod b,如13 mod 5 = 3,5 mod 13 = 5。如果用gcd(a, b)表示a和b的最大公因子GCD,则欧几里得算法可以通过递归算法简单地描述如下:
带你读《Python科学计算(原书第2版)》之二:IPython入门
请读者尝试基于上述算法手工求解gcd(56, 24)。其实很简单!可以看出,当a和b是连续的斐波那契(Fibonacci)数时,演算会出现最费力的情况,因此它们对测试用例很有用。斐波那契数列Fn通过递归算法定义如下:
带你读《Python科学计算(原书第2版)》之二:IPython入门
斐波那契数列为:0,1,1,2,3,5,8,…
如何在Python语言中快速有效地实现欧几里得算法和斐波那契数列呢?我们从计算斐波那契数列任务开始,因为它看起来更简单。
为了开始掌握IPython,希望初学者先充分信任如下两个代码片段。这里仅提供了部分解释,但在第3章中将对所有的特征进行更全面的解释。然而,这里需要强调的重点是,每种程序设计语言都需要辅助代码块,例如函数或者do循环的内容。大多数语言都以某种形式的括号来区分代码块,但Python只依赖缩进。所有的代码块必须具有相同的缩进。通常使用冒号(:)来表示需要子代码块。子代码块则进一步缩进(按惯例使用4个空格),并且使用取消额外缩进的方式指示子代码块的结束。IPython及所有支持Python语言的编辑器都会自动处理这个问题。下面给出三个示例。
在我们继续讨论执行代码片段的工作流之前,初学者应该尝试理解(也许是大致了解)正在发生的事情。

1 # File: fib.py Fibonacci numbers
2
3 """ Module fib: Fibonacci numbers.
4     Contains one function fib(n).
5 """
6
7 def fib(n):
8    """ Returns n'th Fibonacci number. """
9    a,b=0,1
10    for i in range(n):
11       a,b=b,a+b
12    return a
13
14 ####################################################
15 if __name__ == "__main__":
16    for i in range(1001):
17    print "fib(",i,") = ",fib(i)

Python语法的细节将在第3章解释。目前暂时只需要了解以#符号开始的行(如第1行和第14行)表示注释。另外第3~5行定义了一个文档字符串,其作用将在稍后进行解释。第7~12行定义了一个Python函数。注意前述内容表明每个冒号(:)都要求缩进代码块。第7行是函数声明。第8行是函数文档字符串信息(同样将在稍后阐述)。第9行引入了标识符a和b,它们是该函数的局部变量,初始值分别引用值0和1。接下来检查第11行,暂时忽略其缩进。此处a被设置为引用b最初引用的值,同时b被设置为引用最初a和b引用的值的和。很明显,第9行和第11行重复计算蕴含在式(2-2)中的公式。第10行引入了一个for循环(或者do循环,这将在3.7.1节阐述),循环包括第11行。此处range(n)生成了一个包含n个元素的列表[0, 1, …, n-1],因此第11行将被执行n次。最后,第12行退出函数,并且返回最终a引用的值。
当然,我们需要提供一个测试集来证明这个函数是按照预期的方式运行的。第14行仅仅是注释。第15行将随后解释。(在输入时,请注意有四对下划线。)因为它是一个以冒号结尾的if语句,所以后面的所有行都需要缩进。我们已经了解到第16行代码的思想。我们使用i=0,1,2,…,1000重复执行第17行代码1001次。它输出表示i的值的字符串(包含4个字符),以及表示fib(i)的值的另一个字符串(包含4个字符)。
接下来,我们展示创建和使用上述代码片段的两种可能的工作流程。

2.5.1 使用IPython终端的工作流程

首先用户使用喜欢的编辑器,在运行IPython的目录中创建一个文件fib.py,输入上述代码片段的内容并保存。然后,在IPython窗口中运行魔法命令run fib。如果用户所创建的文件内容没有语法错误,则运行结果为1001行斐波那契数。如果文件内容存在语法错误,则将输出其中的第一处错误。重新回到编辑器窗口,更正并保存源代码。然后再次尝试运行魔法命令run fib。(初学者可能需要重复数次,但这并不复杂!)

2.5.2 使用IPython笔记本的工作流程

打开一个新的单元格并在其中输入上述代码片段,为了稳妥起见请保存笔记本(使用快捷键按ESC键后再按s键)。接着运行单元格(使用快捷键Ctrl+回车键)。如果用户所输入的代码没有语法错误,则运行结果为1001行斐波那契数。如果输入的代码存在语法错误,则将输出其中的第一处错误。重新回到单元格,修改错误代码,然后再次运行单元格。(初学者可能需要重复数次,但这并不复杂!)一旦程序满足要求,重新回到单元格,并在最前面插入如下单元格魔法命令:
%%writefile fib.py
现在重新运行该单元格,则单元格中的内容将写入到当前目录的fib.py中,如果该文件已经存在,则覆盖其内容。
一旦程序验证并运行正确后,我们如何测量其运行速度呢?再次运行程序,但使用增强魔法命令run -t fib,则IPython将产生时间度量数据。在我自己的计算机上,“用户时间”(User time)是0.05秒,但是“系统时间”(Wall time)是0.6秒。很显然,该差异反映了有大量字符输出到屏幕上。要验证这一点,请将代码片段修改如下:在第17行的前面插入一个#符号以注释掉打印输出语句;添加新的第18行,输入fib(i),请注意缩进格式正确。(这将对函数进行求值,但不处理计算结果值。)接下来再次运行程序,在我的计算机上耗时0.03秒,这表明fib(i)运行速度极快,但打印输出速度缓慢。(最后别忘了注释掉第18行,并取消第17行的注释!)
接下来我们将解释第3~5行以及第8行中的文档字符串,以及奇特的第15行。请关闭IPython(在终端模式下,使用命令exit),然后重新打开一个IPython的会话。仅输入一行语句import fib,注意只需要文件主名,不需要输入文件后缀.py。该语句导入了一个对象fib。那么fib是什么呢?使用自省命令fib?,IPython将输出代码片段中第3~5行的文档字符串信息。这表明我们也可以获得有关函数fib.fib的更多信息,所以请尝试命令fib.fib?,结果将返回第8行中的函数文档字符串信息。文档字符串(docstring)是用三引号括起来的信息,用途是向其他用户提供在线帮助文档。另外,自省还有一个有用的技巧,请尝试fib.fib??,将输出该函数的源代码列表。
读者应该注意到,运行import fib并没有输出前1001个斐波那契数。但是,如果我们在一个单独的IPython会话中运行命令run fib,则将输出前1001个斐波那契数!代码片段的第15行检测文件fib.py是被导入还是被运行,并相应地不运行或者运行测试集。其实现原理将在3.4节阐述。
接下来我们继续讨论原来的任务,即实现公式(2-1)中的gcd函数。一旦我们认识到Python实现递归没有任何问题,并且a mod b被实现为a%b,那么最简单的解决方案可以通过如下的代码片段实现(请暂时忽略第14~18行)。

1 # File gcd.py Implementing the GCD Euclidean algorithm.
2
3 """ Module gcd: contains two implementations of the Euclid
4    GCD algorithm, gcdr and gcd.
5 """
6
 7 def gcdr(a,b):
 8    """ Euclidean algorithm, recursive vers., returns GCD. """
 9    if b==0:
10        return a
11    else:
12        return gcdr(b,a%b)
13
14 def gcd(a,b):
15    """ Euclidean algorithm, non-recursive vers., returns GCD. """
16    while b:
17        a,b=b,a%b
18    return a
19
20 ##################################################
21 if __name__ == "__main__":
22    import fib
23
24    for i in range(963):
25        print i, ' ', gcd(fib.fib(i),fib.fib(i+1))

上述代码片段中唯一真正的新内容是第22行中的import fib语句,而上文我们已经讨论了其作用。第24行和第25行中循环的执行次数十分关键。输出结果表明,上述代码片段运行耗时为几分之一秒。接下来将第24行中的参数963更改为964,保存文件,并再次执行run gcd。结果发现输出是一个无限循环,稍微耐心等待后,最终进程会终止,并显示错误信息“the maximum recursion depth has been exceeded”(超过最大递归深度)。虽然Python允许递归,但对自调用的数量存在限制。
这个限制对用户而言可能不是大问题。但有必要花点时间考虑是否可以在不使用递归的情况下实现欧几里得算法(2-1)。我提供了一个解决方案,位于代码片段第14~18行实现的函数gcd中。第16行和第17行定义一个while循环,请注意第16行后面的冒号。在while和冒号之间,Python期望一个计算结果为布尔值(True或者False)的条件表达式。只要条件表达式的计算结果为True,则循环执行第17行,然后重新测试条件表达式。如果测试结果为False,则循环终止,控制转移到下一语句(第18行)。在该上下文中,b是一个整数,那么整数值如何转换为布尔值呢?答案非常简单:整数值0总是被强制转换为False;所有非零值则被强制转换为True。因此,当b变为0时,则循环结束,然后函数返回值a。这是公式(2-1)的第一个子句。第17行的转换是第二个子句,所以这个函数实现了欧几里得算法。它比递归函数短,可以被调用任意次数,并且我们将看到,其运行速度更快。
那么,上述改进算法是否有效呢?使用run命令,我们可以获得统计数据。首先,编辑代码片段,修改代码使函数gcdr循环运行963次,并保存代码。接下来执行run -t gcd,以获得运行耗时。在我的计算机上,“用户时间”是0.31秒(每个读者计算机上的耗时可能会不同,但主要考虑的是相对时间。“系统时间”则反映输出显示时间开销,并且与此处无关。接下来执行run -p gcd以调用Python性能分析器。虽然理解输出显示结果的每个方面需要阅读相关帮助文档信息,但也可以使用科学直观感觉。结果表明,在总共464 167次实际调用中,有963次直接调用(与预期相符)函数gcdr。该函数实际耗时0.237秒。其次,函数fib被调用了1926次(与预期相符),并且耗时0.084秒。注意,这些时间不能与前面run -t gcd的运行结果相比较,因为run -p gcd的耗时包括性能分析器的时间开销(并且此处不能忽略)。然而,我们可以得出结论,函数gcdr耗费了大约74%的时间开销。
接下来我们重复有关函数gcd的练习。修改代码段的第25行,用gcd替换gcdr并保存文件。接下来执行run -t gcd,结果用户时间是0.20秒。执行另一个命令run -p gcd,结果显示函数fib被调用了1926次,耗时0.090秒。但是,函数gcd只被调用了993次(与预期相符),耗时为0.087秒。因此,gcd耗时占比为49%。近似地,相对耗时可以消除性能分析器时间开销的影响。结果表明,递归版本0.31秒耗时的74%是0.23秒,而非递归版本0.20秒耗时的49%是0.098秒。因此,改进算法思想的结果代码更加短小,运行时间是原始算法的43%!
从上述示例中我们可以总结如下两点:
1.IPython的魔法命令run或者%run可以提高Python的工作效率。读者可以通过自省命令来查阅其文档字符串。也请注意%run -t和%run -p的区别。同时,建议读者尝试自省命令%timeit。
2.读者在文献中会查阅到许多关于“加速”Python的方法,这些方法通常是非常聪明的软件工程方法。但没有一种能像人类的创造力那样有效!

上一篇:ASP.NET 进阶】仿百度文库文档在线预览(支持格式.pdf,.doc,docx,xls,xlsx,.ppt,pptx)


下一篇:带你读《Python科学计算(原书第2版)》之一:导论