a、IPython

发布时间 2023-08-12 10:21:48作者: 昵称已经被使用

IPython:超越Python

Python 有很多开发环境可供选择,IPython(interactive Python 的简称,即交互式 Python)由 Fernando Perez 作为一个增强的 Python 解释器于 2001 年启动,并由此发展为 一个项目。用 Perez 的原话来说,该项目致力于提供“科学计算的全生命周期开发工具”。 如果将 Python 看作数据科学任务的引擎,那么 IPython 就是一个交互式控制面板。
除了作为 Python 的一个交互式接口,IPython 还提供了一些有用的 Python 语法附加功能。另外,IPython 被紧密地连接在Jupyter 项目(http:// jupyter.org)中。该项目提供一个基于浏览器的Notebook,它可以开发、协作、分享甚至发布数据科学结果。IPython Notebook 其实只是通用 Jupyter Notebook 结构的特例,而 Jupyter Notebook 不仅支持Python,还包括用于Julia、R 和其他编程语言的Notebook。IPython 就是用Python 进行有效的交互式科学计算和数据密集型计算。

1.1 shell还是Notebook

本章将介绍两种使用IPython 的方式:IPython shell 和 IPython Notebook。在开始之前,先简单介绍一下如何启动 IPython shell 和 IPython Notebook。

1.1.1 启动IPython shell

你可以在命令行中输入 ipython 启动 IPython 解释器。如果你安装了 Anaconda 或 EPD 的 Python 发行版,系统中将会有一个特别的启动器。
当你完成以上步骤后,将看到如下的界面:
image-20201212145954127
这时就可以进行接下来的步骤了。

1.1.2 启动Jupyter Notebook

Jupyter Notebook 是 IPython shell 基于浏览器的图形界面,提供了一系列丰富的动态展示功能。Jupyter Notebook 不仅可以执行 Python/IPython 语句,还允许用户添加格式化文本、静态和动态的可视化图像、数学公式、JavaScript 插件,等等。不仅如此,这些 Notebook 文档还能以共享方式存储,以便其他人可以打开这些Notebook,并且在他们自己的系统中执 行这些 Notebook 代码。
尽管 IPython Notebook 是通过你的 Web 浏览器窗口进行查看和编辑的,但是它必须与一个正在运行的 Python 进程连接才能执行代码。想要启动这个进程(也被称作“核”,kernel), 需要在你系统的命令行中输入以下命令:

>jupyter notebook

这个命令会启动一个本地的 Web 服务器,可以在你的浏览器中看到页面内容。同时,它会立刻生成日志,显示它正在做什么。
一旦以上命令执行,你的默认浏览器将会自动打开,并且自动导航到 localhost 网址(实际 地址会依据你的系统而定)。如果浏览器没有自动打开,你可以自己打开一个窗口,并且 手动打开这个网址(在这个示例中是 http://localhost:8888)。
image-20201212150240242

也可以通过点击开始菜单栏中的Jupyter Notebook进行打开

image-20201212150457456

1.2 IPython的帮助和文档

当一个技术型思维的人要帮助他的朋友、家人或同事解决计算机方面的问题时,大多数时候,重要的不是知道答案,而是知道如何快速找到答案。在数据科学领域也一样,通过搜索在线文档、邮件列表、Stack Overflow 等网络资源都可以获得丰富的信息,即使(尤其是)你曾经搜索过这个主题。要想成为一名高效的数据科学实践者,重要的不是记住针对每个场景应该使用的工具或命令,而是学习如何有效地找到未知信息,无论是通过搜索引擎还是其他方式。
IPython 和 Jupyter 最大的用处之一就是能缩短用户与帮助文档和搜索间的距离,帮助用户高效完成工作。虽然网络搜索在解答复杂问题时非常有用,但是仅仅使用 IPython 就能找到大量的信息了。以下是仅通过几次按键,IPython 就可以帮你解答的一些问题。

  • 我如何调用这个函数?这个函数有哪些参数和选项?
  • 这个 Python 对象的源代码是怎样的?
  • 我导入的包中有什么?这个对象有哪些属性和方法?

接下来将介绍如何通过 IPython 工具来快速获取这些信息。符号 '?' 用于浏览文档,符号 '??' 用于浏览源代码,而 Tab 键可以用于自动补全。

1.2.1 用符号?获取文档

Python 语言和其数据科学生态系统是应用户需求而创建的,而用户的很大一部分需求就是 获取文档。每一个 Python 对象都有一个字符串的引用,该字符串即 docstring。大多数情况下,该字符串包含对象的简要介绍和使用方法。Python 内置的 help() 函数可以获取这些信息,并且能打印输出结果。例如,如果要查看内置的 len 函数的文档,可以按照以下步骤操作:

In [1]: help(len)
Help on built-in function len in module builtins:

len(obj, /)
    Return the number of items in a container.

根据不同的解释器,这条信息可能会展示为内嵌文本,或者出现在单独的弹出窗口中。
获取关于一个对象的帮助非常常见,也非常有用,所以 IPython 引入了 ? 符号作为获取这 个文档和其他相关信息的缩写:

In [2]: len?
Signature: len(obj, /)
Docstring: Return the number of items in a container.
Type:      builtin_function_or_method

这种表示方式几乎适用于一切,包括对象方法:

In [3]: L = [1,2, 3]
In [4]: L.insert?
Signature: L.insert(index, object, /)
Docstring: Insert object before index.
Type:      builtin_function_or_method

甚至对于对象本身以及相关类型的文档也适用:

In [5]: L?
Type:        list
String form: [1, 2, 3]
Length:      3
Docstring:
Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list.
The argument must be an iterable if specified.

重要的是,这种方法也适用于你自己创建的函数或者其他对象!下面定义一个带有 docstring 的小函数:

In [6]: def suare(a):
   ...:     """Return the square of a."""
   ...:     return a ** 2
   ...:

请注意,为了给函数创建一个docstring,仅仅在第一行放置了一个字符串字面量。由于 docstring 通常是多行的,因此按照惯例,用 Python 的三个引号表示多行字符串。
接下来用 ? 符号来找到这个 docstring:

In [7]: suare?
Signature: suare(a)
Docstring: Return the square of a.
File:      c:\users\administrator\<ipython-input-6-465688d14d94>
Type:      function

1.2.2 通过符号??获取源代码

由于 Python 非常易读,所以你可以通过阅读你感兴趣的对象的源代码得到更高层次的理解。IPython 提供了获取源代码的快捷方式(使用两个问号 ??):

In [8]: suare??
Signature: suare(a)
Source:
def suare(a):
    """Return the square of a."""
    return a ** 2
File:      c:\users\administrator\<ipython-input-6-465688d14d94>
Type:      function

对于这样的简单函数,两个问号就可以帮助你深入理解隐含在表面之下的实现细节。
如果你经常使用 ?? 后缀,就会发现它有时不能显示源代码。这是因为你查询的对象并不是用 Python 实现的,而是用 C 语言或其他编译扩展语言实现的。在这种情况下,?? 后缀将等同于 ? 后缀。你将会在很多 Python 内置对象和类型中发现这样的情况,例如上面示例中提到的 len 函数。
? 和 ?? 提供了一个强大又快速的接口,可以查找任何 Python 函数或模块的用途信息。

1.2.3 用Tab补全的方式探索模块

IPython 另一个有用的接口是用 Tab 键自动补全和探索对象、模块及命名空间的内容。在接下来的示例中,我们将用 来表示 Tab 键。

1. 对象内容的Tab自动补全

每一个 Python 对象都包含各种属性和方法。和此前讨论的 help 函数类似,Python 有一个 内置的 dir 函数,可以返回一个属性和方法的列表。但是 Tab 自动补全接口在实际的应用过程中更简便。要想看到对象所有可用属性的列表,可以输入这个对象的名称,再加上一 个句点(.)和 Tab 键:

In [9]: L.<TAB>
           append()  count()   insert()  reverse()
           clear()   extend()  pop()     sort()
           copy()    index()   remove()

为了进一步缩小整个列表,可以输入属性或方法名称的第一个或前几个字符,然后 Tab 键将会查找匹配的属性或方法,如果只有一个选项,按下 Tab 键将会把名称自动补全。
尽管 Python 没有严格区分公共 / 外部属性和私有 / 内部属性,但是按照惯例,前面带有下划线表示私有属性或方法。为了清楚起见,这个列表中默认省略了这些私有方法和特殊方法。不过,你可以通过明确地输入一条下划线来把这些私有的属性或方法列出来:大部分是 Python 特殊的双下划线方法(昵称 叫作“dunder 方法”)。

2. 导入时的Tab自动补全

Tab 自动补全在从包中导入对象时也非常有用。下面用这种方法来查找 itertools 包中以 co 开头的所有可导入的对象:

In [9]: from itertools import co<TAB>
	combinations	compress
	combinations_with_replacement count

同样,你也可以用 Tab 自动补全来查看你系统中所有可导入的包(这将因你的 Python 会话中有哪些第三方脚本和模块可见而不同)

1.3 IPython shell中的快捷键

如果你常用计算机,你可能在工作流程中使用过快捷键。最熟悉的可能是 Cmd + C 和 Cmd + V(或者是 Ctrl + C 和 Ctrl + V),它们在很多程序和系统中用于复制和粘贴。高级用户会将快捷键用得更加深入和广泛,流行的文本编辑器(如 Emacs、Vim 等)通过复杂的按键组合为用户提供了很多快捷操作。
IPython 并没有上述编辑器那么强大,但是它也提供了一些快捷方式,能帮你在录入命令的 时候快速导航。但事实上,这些快捷方式并不是 IPython 本身提供的,而是通过 IPython 对 GNU Readline 库的依赖关系实现的。因此,接下来介绍的一些快捷方式可能会因你系统配置的不同而不同。此外,一些快捷方式也可以在基于浏览器的 Notebook 中起作用。
一旦你用惯了这些快捷方式,就能快速执行一些命令,而不用将手从“home”键上移开。 如果你是一名 Emacs 用户,或者有 Linux shell 的使用经验,你会对接下来的内容非常熟悉。我们会将这些快捷键分为几类:导航快捷键、文本输入快捷键、命令历史快捷键和其他快捷键。

1.3.1 导航快捷键

利用左箭头和右箭头在一行中向前或向后移动是非常常见的,不过还有其他一些选项也可以让你不用把手从“home”键上挪开。

快捷键 动作
Ctrl + a 将光标移到本行的开始处
Ctrl + e 将光标移到本行的结尾处
Ctrl + b (或左箭头键) 将光标回退一个字符
Ctrl + f (或右箭头键) 将光标前进一个字符

1.3.2 文本输入快捷键

每个人都知道用 Backspace 键可以删除前一个字符,但手指要移动一定的距离才能够到这个按键,并且它一次只能删除一个字符。IPython 中有一些可以删除你输入的部分文本的快捷键,其中立马能派上用场的就是删除整行文本的快捷键。一旦你开始用 Ctrl + b 和 Ctrl + d 组 合,而不是用 Backspace 按键删除前一个字符,就再也离不开这些快捷键了。

快捷键 动作
Backspace键 删除前一个字符
Ctrl + d 删除后一个字符
Ctrl + k 从光标开始剪切至行的末尾
Ctrl + u 从行的开头剪切至光标
Ctrl + y yank(即粘贴)之前剪切的文本
Ctrl + t transpose(即交换)前两个字符

1.3.3 命令历史快捷键

这个命令历史超越了你当前的 IPython 会话——你所有的命令历史都会存储在一个 IPython 配置文件路径下的 SQLite 数据库中。获取这些命令最直接的方式就是用上下箭头遍历历史,但是仍然还有些别的选项。

快捷键 动作
Ctrl + p(或向上箭头) 获取前一个历史命令
Ctrl + n(或向下箭头) 获取后一个历史命令
Ctrl + r 对历史命令的反向搜索

反向搜索特别有用。可以让我们从一个新的 IPython shell 中反向搜索 Python 历史,重新找到这个函数的定义。当你在 IPython 终端按 下 Ctrl + r 键时,将看到如下提示:

image-20201212161248614

如果你在该提示后开始输入字符,IPython 将自动填充时间最近的命令。如果有的话,将会匹配到如下字符:
image-20201212161534428

你可以随时添加更多的字符来重新定义搜索,或者再一次按下 Ctrl + r 键来寻找另外一个匹配该查询的命令。按下 Ctrl + r 键两次将可以看到自定义的函数。
找到你在寻找的命令后,按下 ESC 键将会终止查找。然后就可以利用查找到的命令,继续会话。
请注意,你也可以用 Ctrl + p / Ctrl + n 或者上下方向键查找历史,但是仅仅是匹配每行的前几个字符。也就是说,如果你输入 def 然后按下 Ctrl + p,则会在你的命令历史中找到以 def 开头的最近的命令(如果有的话)。

1.3.4 其他快捷键

还有一些不能归纳在之前几个类别中的快捷键,但是它们也非常有用。

快捷键 动作
Ctrl + l 清除终端屏幕的内容
Ctrl + c 中断当前的 Python 命令
Ctrl + d 退出 IPython 会话

如果你无意间开启了一个运行时间非常长的程序,Ctrl + c 快捷键就能派上大用场。

1.4 IPython魔法命令

一些 IPython 在普通 Python 语法基础之上的增强功能。这些功能被称作 IPython 魔法命令,并且都以 %符号作为前缀。这些魔法命令设计用于简洁地解决标准数据分析中的各种常见问题。魔法命令有两种形式:行魔法(line magic)和单元魔法(cell magic)。行魔法以单个%字符作为前缀,作用于单行输入;单元魔法以两个%%作为前缀,作用于多行输入。

1.4.1 粘贴代码块:%paste和%cpaste

当你使用 IPython 解释器时,粘贴多行代码块可能会导致不可预料的错误,尤其是其中包含缩进和解释符号时。
在直接粘贴的过程中,解释器被额外的提示符号搞晕了。IPython 的 %paste 魔法函数可以解决这个包含符号的多行输入问题,当输入%paste后,解释器会自动粘贴系统剪切板中的内容

%paste

另外一个作用类似的命令是 %cpaste。该命令打开一个交互式多行输入提示,你可以在这个提示下粘贴并执行一个或多个代码块:

In [2]: %cpaste
Pasting code; enter '--' alone on the line to stop or use Ctrl-D.
:def square(a):
:    """Return the square of a."""
:    return a ** 2
:--

这些命令和我们将会看到的其他魔法命令一样,实现了在标准的 Python 解释器中很难或是 不可能实现的功能。

1.4.2 执行外部代码:%run

当你开发更复杂的代码时,可能会发现自己在使用 IPython 进行交互式探索的同时,还需要使用文本编辑器存储你希望重用的代码。在 IPython 会话中运行之前的代码非常方便, 不用在另一个新窗口中运行这些程序代码。这个功能可以通过 %run 魔法命令来实现。
假设你在桌面创建了一个 myscript.py 文件,该文件包含以下内容:

# file: myscript.py 
def square(a):
    """Return the square of a."""
    return a ** 2

for N in range(1, 4):
    print(N, "squared is ", square(N))

你可以在像下面这样在 IPython 会话中运行该程序:

In [1]: %run C:/Users/Administrator/Desktop/myscript.py
1 squared is  1
2 squared is  4
3 squared is  9

请注意,当你运行了这段代码之后,该代码中包含的所有函数都可以在 IPython 会话中使用:

In [3]: square(5)
Out[3]: 25

IPython 提供了几种方式来调整代码如何执行。你可以在 IPython 解释器中输入 %run? 查看帮助文档。

1.4.3 计算代码运行时间:%timeit

%timeit会自动计算接下来一行的 Python 语句的执行时间。例如,我们可能想了解列表综合的性能:

In [4]: %timeit L = [n ** 2 for n in range(1000)]
406 µs ± 8.84 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit 的好处是,它会自动多次执行简短的命令,以获得更稳定的结果。对于多行语句, 可以加入第二个 % 符号将其转变成单元魔法,以处理多行输入。例如,下面是 for 循环的同等结构:

In [6]: %%timeit
   ...: L = []
   ...: for n in range(1000):
   ...:     L.append(n ** 2)
   ...:
442 µs ± 8.32 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

从以上结果可以立刻看出,列表综合比同等的 for 循环结构快约 10%。

1.4.4 魔法函数的帮助:?、%magic和%lsmagic

和普通的 Python 函数一样,IPython 魔法函数也有文档字符串,并且可以通过标准的方式获取这些有用的文档注释。例如,为了读到 %timeit 魔法函数的文档注释,可以简单地输 入以下命令:

In [10]: %timeit?

其他函数的文档注释也可以通过类似方法获得。为了获得可用魔法函数的通用描述以及一 些示例,可以输入以下命令:

In [11]: %magic

为了快速而简单地获得所有可用魔法函数的列表,可以输入以下命令:

In [12]: %lsmagic

1.5 输入和输出历史

我们在前面看到,IPython shell 允许用上下方向键或 Ctrl + p / Ctrl + n 快捷键获取历史命令。另外,IPython 在 shell 和 Notebook 中都提供了几种获取历史命令的输出方式,以及这些命令本身的字符串形式。

1.5.1 IPython的输入和输出对象

到目前为止,我想你应该特别熟悉 IPython 用到的 In[1]:/Out[1]: 形式的提示了。但实际 上,它们并不仅仅是好看的装饰形式,还给出了在当前会话中如何获取输入和输出历史的线索。假设你用以下形式启动了一个会话:

In [8]: import math

In [9]: math.sin(2)
Out[9]: 0.9092974268256817

In [11]: math.cos(2)
Out[11]: -0.4161468365471424

我们导入了一个内置的 math 程序包,然后计算 2 的正弦函数值和余弦函数值。这些输入和输出在 shell 中带有 In/Out 标签,但是不仅如此——IPython 实际上创建了叫作 In 和 Out 的 Python 变量,这些变量自动更新以反映命令历史:

In [12]: print(In)
['', "get_ipython().run_line_magic('run', 'C:/Users/Administrator/Desktop/myscript.py')", 'squared(5)', 'square(5)', "get_ipython().run_line_magic('timeit', 'L = [n ** 2 for n in range(1000)]')", "get_ipython().run_cell_magic('ttimeit', '', 'L = []\\nfor n in range(1000):\\n    L.append(n ** 2)\\n    \\n')", "get_ipython().run_cell_magic('timeit', '', 'L = []\\nfor n in range(1000):\\n    L.append(n ** 2)\\n    \\n')", "get_ipython().run_line_magic('pinfo', '%timeit')", 'import math', 'math.sin(2)', 'math,cos(2)', 'math.cos(2)', 'print(In)']

In [14]: print(Out)
{3: 25, 9: 0.9092974268256817, 11: -0.4161468365471424}

In 对象是一个列表,按照顺序记录所有的命令(列表中的第一项是一个占位符,以便 In[1] 可以表示第一条命令):

In [15]: print(In[1])
get_ipython().run_line_magic('run', 'C:/Users/Administrator/Desktop/myscript.py')

Out 对象不是一个列表,而是一个字典。它将输入数字映射到相应的输出(如果有的话):

In [20]: print(Out[3])
25

请注意,不是所有操作都有输出,例如 import 语句和 print 语句就不影响输出。对于后者你可能会感到有点意外,但是仔细想想,print 是一个函数,它的返回值是 None,这样就能说通了。总的来说,任何返回值是 None 的命令都不会加到 Out 变量中。
如果想利用之前的结果,理解以上内容将大有用处。例如,利用之前的计算结果检查 sin(2) ** 2 和 cos(2) ** 2 的和,结果如下

In [21]: Out[9] ** 2 + Out[11] ** 2
Out[21]: 1.0

输出结果是 1.0,符合勾股定理。在这个例子中,可能不需要利用之前的结果,但是如果你执行一个非常复杂的计算并且希望重复利用运算结果,那么该方法就会非常有用。

1.5.2 下划线快捷键和以前的输出

标准的 Python shell 仅仅包括一个用于获取以前的输出的简单快捷键。变量 _(单下划线) 用于更新以前的输出,而这种方式在 IPython 中也适用:

In [22]: print(_)
1.0

但是 IPython 更进了一步,你可以用两条下划线获得倒数第二个历史输出,用三条下划线获得倒数第三个历史输出(跳过任何没有输出的命令,IPython 的这一功能就此停止:超过三条下划线开始变得比较难计数,并且在这种情况下 通过行号来指定输出更方便。
这里还要提到另外一个快捷键,Out[X] 的简写形式是 _X(即一条下划线加行号):

In [24]: Out [9]
Out[24]: 0.9092974268256817

In [26]: _9
Out[26]: 0.9092974268256817

1.5.3 禁止输出

有时你可能希望禁止一条语句的输出。或者你执行的命令生成了一个你并不希望存储到输出历史中的结果,这样当其他引用被删除时,该空间可以被释放。要禁止一个命令的输出,最简单的方式就是在行末尾处添加一个分号:

In [27]: math.sin(2) + math.cos(2);

In [28]: 27 in Out
Out[28]: False

请注意,这个结果被默默地计算了,并且输出结果既不会显示在屏幕上,也不会存储在 Out 路径下。

1.5.4 相关的魔法命令

如果想一次性获取此前所有的输入历史,%history 魔法命令会非常有用。在下面的示例中可以看到如何打印前 4 条输入命令:

 %history

按照惯例,可以输入 %history? 来查看更多相关信息以及可用选项的详细描述。其他类似的魔法命令还有 %rerun(该命令将重新执行部分历史命令)和 %save(该命令将部分历史 命令保存到一个文件中)。

1.6 IPython和shell命令

当与标准 Python 解释器交互时,IPython 提供了在 IPython 终端直接执行 shell 命令的语法。这一神奇的功能是使用感叹号实现的:一行中任何在 ! 之后的内容将不会通过 Python 内核运行,而是通过系统命令行运行。
以下内容假定你在用一个类 Unix 系统,如 Linux 或者 Mac OS X。下文中的一些示例如果 在 Windows 系统中运行将会失败,因为 Windows 系统默认使用的是与类 Unix 系统不同的 shell。(微软已于 2016 年宣布在 Windows 系统中可以运行原生的 Bash shell,所以在不久后这就不是个问题了!)如果不熟悉 shell 命令,建议你查看 Software Carpentry Foundation 的 shell 教程(http://swcarpentry.github.io/shell-novice/)。

1.6.1 shell快速入门

shell 是一种通过文本与计算机交互的方式。自 20 世纪 80 年代中期,微软和苹果发布其第一版(现在已经非常普遍)图形操作系统以来,大多数计算机用户已经熟悉了通过菜单点击和拖拽移动等方式与操作系统进行交互。但是,操作系统早在这些图形用户界面出现之前就存在,并且早期主要通过输入文本来控制:用户在提示符后输入一个命令,计算机将按照命令执行任务。这些早期的提示系统是 shell 和终端的前身,并且大多数活跃的数据科学家至今仍然用它们与计算机交互。
有些不熟悉 shell 的人可能会问,明明通过简单地点击图标和菜单就可以完成很多任务,为 什么要把它复杂化? shell 用户可能想用另一个问题来回答:明明在命令行简单地输入就能完成任务,为什么还要点击图标和菜单?这听起来可能像一个典型的技术偏好僵局,但显然,shell 对于高级任务能提供更多的控制操作。但是,我们也不得不承认 shell 的学习曲线会使很多普通计算机用户望而却步。
例如,以下是一个用户在 Linux / OS X 系统中探索、创建和修改文件和路径的 shell 会话的 示例(osx:~ $ 是提示符,在 $ 符号后的所有内容是输入的命令;在 # 之前的文本是一个 描述,并不是实际输入的内容):

osx:~ $ echo "hello world"              # echo类似于Python的打印函数 hello world 
osx:~ $ pwd                             # pwd=打印工作路径
 /home/jake                              # 这就是我们所在的路径 
osx:~ $ ls                              # ls=列出当前路径的内容 notebooks  projects 
osx:~ $ cd projects/                    # cd=改变路径 
osx:projects $ pwd /home/jake/projects 
osx:projects $ ls datasci_book   mpld3   myproject.txt 
osx:projects $ mkdir myproject          # mkdir=创建新的路径 
osx:projects $ cd myproject/ 
osx:myproject $ mv ../myproject.txt ./  # mv=移动文件。这里将文件myproject.txt从上一级路径(../)移动到当前路径(./) osx:myproject $ ls myproject.txt

请注意,以上示例仅仅是通过输入命令而不是通过点击图标和菜单来执行熟悉操作(对路径结构的导航、创建路径、移动文件等)的一种紧凑方式。在这个例子中,仅仅通过几个简单的命令(pwd、ls、cd、mkdir 和 cp)就可以完成大多数常见的文件操作。当你执行一 些高级任务时,shell 方法将变得非常有用。

1.6.2 IPython中的shell命令

你可以通过将 ! 符号作为前缀在 IPython 中执行任何命令行命令。例如,ls、pwd 和 echo 命令可以按照以下方式运行:

In [1]: !ls 
myproject.txt 

In [2]: !pwd 
/home/jake/projects/myproject 

In [3]: !echo "printing from the shell" 
printing from the shell

1.6.3 在shell中传入或传出值

shell 命令不仅可以从 IPython 中调用,还可以和 IPython 命名空间进行交互。例如,你可以通过一个赋值操纵符将任何 shell 命令的输出保存到一个 Python 列表:

In [4]: contents = !ls 

In [5]: print(contents) 
['myproject.txt'] 

In [6]: directory = !pwd 

In [7]: print(directory)
['/Users/jakevdp/notebooks/tmp/myproject']

请注意,这些结果并不以列表的形式返回,而是以 IPython 中定义的一个特殊 shell 返回类 型的形式返回:

In [8]: type(directory)
IPython.utils.text.SList

这看上去和 Python 列表很像,并且可以像列表一样操作。但是这种类型还有其他功能,例 如 grep 和 fields 方法以及 s、n 和 p 属性,允许你轻松地搜索、过滤和显示结果。你可以 用 IPython 内置的帮助来查看更多的详细信息。
另一个方向的交互,即将 Python 变量传入 shell,可以通过 {varname} 语法实现:

In [9]: message = "hello from Python" 

In [10]: !echo {message}|
hello from Python

变量名包含在大括号内,在 shell 命令中用实际的变量替代。

1.7 与shell相关的魔法命令

操作 IPython shell 一段时间后,你可能会注意到你不能通过 !cd 来导航文件系统:

In [11]: !pwd 
/home/jake/projects/myproject 

In [12]: !cd .. 

In [13]: !pwd 
/home/jake/projects/myproject

原因是 Notebook 中的 shell 命令是在一个临时的分支 shell 中执行的。如果你希望以一种更 持久的方式更改工作路径,可以使用 %cd 魔法命令:

In [14]: %cd .. 
/home/jake/projects

事实上,默认情况下你甚至可以不用 % 符号实现该功能:

In [15]: cd myproject 
/home/jake/projects/myproject

这种方式被称作自动魔法(automagic)函数,可以通过 %automagic 魔法函数进行翻转。除了%cd,其他可用的类似shell 的魔法函数还有%cat、%cp、%env、%ls、%man、%mkdir、 %more、%mv、%pwd、%rm 和 %rmdir。如果 automagic 被打开,以上任何一个魔法命令都可以省略 % 符号,这使得你可以将 IPython 提示符当作普通 shell 一样使用:

In [16]: mkdir tmp 

In [17]: ls myproject.txt  tmp/ 

In [18]: cp myproject.txt tmp/ 

In [19]: ls tmp myproject.txt 

In [20]: rm -r tmp

这种在和 Python 会话相同的窗口中访问 shell 的方式,意味着你在编辑 Python 代码时,可以减少在 Python 解释器和 shell 之间来回切换的次数。

1.8 错误和调试

代码开发和数据分析经常需要一些试错,而 IPython 包含了一系列提高这一流程效率的工具。包含控制 Python 异常报告的选项和调试代码中错误的工具。

1.8.1 控制异常:%xmode

大多数时候,当一个 Python 脚本未执行通过时,会抛出一个异常。当解释器捕获到这些异常中的一个时,可以在轨迹追溯(traceback)中找到引起这个错误的原因。利用 %xmode 魔法函数,IPython 允许你在异常发生时控制打印信息的数量。以下面的代码为例:

In [33]: def func1(a, b):
    ...:     return a/b
    ...:

In [34]: def fun2(x):
    ...:     a = x
    ...:     b = x - 1
    ...:     return func1(a, b)
    ...:

In [35]: fun2(1)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-35-d1bbf126f1f6> in <module>
----> 1 fun2(1)

<ipython-input-34-e52f1fce665a> in fun2(x)
      2     a = x
      3     b = x - 1
----> 4     return func1(a, b)
      5

<ipython-input-33-a028a3a362ab> in func1(a, b)
      1 def func1(a, b):
----> 2     return a/b
      3

ZeroDivisionError: division by zero

调用 func2 函数导致一个错误,阅读打印的轨迹可以清楚地看见发生了什么。默认情况下, 这个轨迹信息包括几行,显示了导致错误的每个步骤的上下文。利用 %xmode 魔法函数(简称异常模式),可以改变打印的信息。

%xmode 有一个输入参数,即模式。模式有 3 个可选项:Plain、Context 和 Verbose。默认情况下是 Context,该模式的输出结果我们已经见过。Plain 更紧凑,给出的信息更少:

In [36]: %xmode Plain
Exception reporting mode: Plain

In [37]: fun2(1)
Traceback (most recent call last):
  File "<ipython-input-37-d1bbf126f1f6>", line 1, in <module>
    fun2(1)
  File "<ipython-input-34-e52f1fce665a>", line 4, in fun2
    return func1(a, b)
  File "<ipython-input-33-a028a3a362ab>", line 2, in func1
    return a/b
ZeroDivisionError: division by zero

Verbose 模式加入了一些额外的信息,包括任何被调用的函数的参数:

In [38]: %xmode Verbose
Exception reporting mode: Verbose

In [39]: fun2(1)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-39-d1bbf126f1f6> in <module>
----> 1 fun2(1)
        global fun2 = <function fun2 at 0x000002211728A400>

<ipython-input-34-e52f1fce665a> in fun2(x=1)
      2     a = x
      3     b = x - 1
----> 4     return func1(a, b)
        global func1 = <function func1 at 0x00000221172A2730>
        a = 1
        b = 0
      5

<ipython-input-33-a028a3a362ab> in func1(a=1, b=0)
      1 def func1(a, b):
----> 2     return a/b
        a = 1
        b = 0
      3

ZeroDivisionError: division by zero

这些额外的信息可以帮助你发现为什么会出现异常。那么为什么不在所有场景中都使用 Verbose 模式呢?这是因为如果代码变得更复杂,这种方式的轨迹追溯会变得非常长。根据不同情境,有时默认模式的简要描述更容易处理。

1.8.2 调试:当阅读轨迹追溯不足以解决问题时

标准的 Python 交互式调试工具是 pdb,它是 Python 的调试器。这个调试器允许用户逐行运行代码,以便查看可能导致错误的原因。IPython 增强版本的调试器是 ipdb,它是 IPython 专用的调试器。

启动和运行这两个调试器的方式有很多,这里不会一一介绍。你可以通过在线文档了解关于它们的更多信息。
IPython 中最方便的调试界面可能就是 %debug 魔法命令了。如果你在捕获异常后调用该调试器,它会在异常点自动打开一个交互式调试提示符。ipdb 提示符让你可以探索栈空间的当前状态,探索可用变量,甚至运行 Python 命令!
来看看最近的异常,然后执行一些简单的任务——打印 a 和 b 的值,然后输入 quit 来结束 调试会话:

In [40]: %debug
> <ipython-input-33-a028a3a362ab>(2)func1()
      1 def func1(a, b):
----> 2     return a/b
      3

ipdb> print(a)
1
ipdb> print(b)
0
ipdb> quit

这个交互式调试器的功能不止如此,我们甚至可以设置单步入栈和出栈来查看各变量的值:

In [41]: %debug
> <ipython-input-33-a028a3a362ab>(2)func1()
      1 def func1(a, b):
----> 2     return a/b
      3

ipdb> up
> <ipython-input-34-e52f1fce665a>(4)fun2()
      1 def fun2(x):
      2     a = x
      3     b = x - 1
----> 4     return func1(a, b)
      5

ipdb> print(x)
1
ipdb> up
> <ipython-input-39-d1bbf126f1f6>(1)<module>()
----> 1 fun2(1)

ipdb> down
> <ipython-input-34-e52f1fce665a>(4)fun2()
      1 def fun2(x):
      2     a = x
      3     b = x - 1
----> 4     return func1(a, b)
      5
 
ipdb> quit

这让你可以快速找到导致错误的原因,并且知道是哪一个函数调用导致了错误。

如果你希望在发生任何异常时都自动启动调试器,可以用 %pdb 魔法函数来启动这个自动过程:

In [51]: %xmode Plain
Exception reporting mode: Plain

In [52]: %pdb on
Automatic pdb calling has been turned ON

In [53]: fun2(1)
Traceback (most recent call last):
  File "<ipython-input-53-d1bbf126f1f6>", line 1, in <module>
    fun2(1)
  File "<ipython-input-34-e52f1fce665a>", line 4, in fun2
    return func1(a, b)
  File "<ipython-input-33-a028a3a362ab>", line 2, in func1
    return a/b
ZeroDivisionError: division by zero

> <ipython-input-33-a028a3a362ab>(2)func1()
      1 def func1(a, b):
----> 2     return a/b
      3

ipdb> print(b)
0
ipdb> quit

最后,如果你有一个脚本,并且希望以交互式模式运行,则可以用 %run -d 命令来运行, 并利用 next 命令单步向下交互地运行代码。

部分调试命令

这里仅仅列举了一部分可用的交互式调试命令。下表中包含了一些常用且有用的命令及其描述。

命令 描述
list 显示文件的当前路径
h(elp) 显示命令列表,或查找特定命令的帮助信息
q(uit) 退出调试器和程序
c(ontinue) 退出调试器,继续运行程序
n(ext) 跳到程序的下一步
重复前一个命令
p(rint) 打印变量
s(tep) 步入子进程
r(eturn) 从子进程跳出

在调试器中使用 help 命令,或者查看 ipdb 的在线文档(https://github.com/gotcha/ipdb)获取更多的相关信息。

1.9 代码的分析和计时

在开发代码和创建数据处理管道的过程中,经常需要在各种实现方式之间取舍,但在开发算法的早期就考虑这些事情会适得其反。正如高德纳的名言所说:“大约 97% 的时间,我们应该忘记微小的效率差别;过早优化是一切罪恶的根源。”
不过,一旦代码运行起来,提高代码的运行效率总是有用的。有时候查看给定命令或一组命令的的运行时间非常有用,有时候深入多行进程并确定一系列复杂操作的效率瓶颈也非常有用。IPython 提供了很多执行这些代码计时和分析的操作函数。我们将讨论以下 IPython 魔法命令。

  • %time 对单个语句的执行时间进行计时。
  • %timeit 对单个语句的重复执行进行计时,以获得更高的准确度。
  • %prun 利用分析器运行代码。
  • %lprun 利用逐行分析器运行代码。
  • %memit 测量单个语句的内存使用。
  • %mprun 通过逐行的内存分析器运行代码。

最后4 条魔法命令并不是与IPython 捆绑的,你需要安装line_profiler 和 memory_ profiler 扩展。我们将在接下来的部分介绍这些扩展。

1.9.1 代码段计时:%timeit和%time

%%timeit 可以让代码段重复运行来计算代码的运行时间:

In [54]: %timeit sum(range(100))
1.41 µs ± 77.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

请注意,因为这个操作很快,所以 %timeit 自动让代码段重复运行很多次。对于较慢的命令,%timeit 将自动调整并减少重复执行的次数:

In [55]: %%timeit
    ...: total = 0
    ...: for i in range(1000):
    ...:     for j in range(1000):
    ...:         total += i * (-1) ** j
    ...:
501 ms ± 5.76 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

有时候重复一个操作并不是最佳选择。例如,如果有一个列表需要排序,我们可能会被重复操作误导。对一个预先排好序的列表进行排序,比对一个无序的列表进行排序要快,所以重复运行将使结果出现偏差:

In [56]: import random

In [57]: L = [random.random() for i in range(10000)]

In [58]: %timeit L.sort()
40.9 µs ± 814 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

对于这种情况,%time 魔法函数可能是更好的选择。对于运行时间较长的命令来说,如果较短的系统延迟不太可能影响结果,那么 %time 魔法函数也是一个不错的选择。下面对一个无序列表排序和一个已排序列表排序分别计时:

In [69]: %time L.sort()
Wall time: 0 ns

可以看出,虽然对已排序的列表进行排序比对未排序的列表进行排序快很多,但是即使同样对已排序的列表进行排序,用 %time 计时也比用 %timeit 计时花费的时间要长。这 是由于 %timeit 在底层做了一些很聪明的事情来阻止系统调用对计时过程的干扰。例如, %timeit 会阻止清理未利用的Python 对象(即垃圾回收),该过程可能影响计时。因此, %timeit 通常比 %time 更快得到结果。
和 %timeit 一样,%time 魔法命令也可以通过双百分号语法实现多行代码的计时:

1.9.2 分析整个脚本:%prun

一个程序是由很多单个语句组成的,有时候对整个脚本计时比对单个语句计时更重要。 Python 包含一个内置的代码分析器(你可以在Python文档中了解更多相关信息),但是 IPython 提供了一种更方便的方式来使用这个分析器,即通过魔法函数 %prun 实现。

在以下例子中,我们将定义一个简单的函数,该函数会完成一些计算:

In [79]: def sum_of_lists(N):
    ...:     total = 0
    ...:     for i in range(5):
    ...:         L = [j ^ (j >> i) for j in range(N)]
    ...:         total += sum(L)
    ...:     return total
    ...:

In [80]: %prun sum_of_lists(10000)
         14 function calls in 0.007 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        5    0.006    0.001    0.006    0.001 <ipython-input-79-c8a2c5fe2cd3>:4(<listcomp>)
        5    0.000    0.000    0.000    0.000 {built-in method builtins.sum}
        1    0.000    0.000    0.007    0.007 <ipython-input-79-c8a2c5fe2cd3>:1(sum_of_lists)
        1    0.000    0.000    0.007    0.007 <string>:1(<module>)
        1    0.000    0.000    0.007    0.007 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

结果是一个表格,该表格按照每个函数调用的总时间,显示了哪里的执行时间最长。在这 个例子中,大部分执行时间用在 sum_of_lists 的列表综合中。通过观察这个数据,我们可 以开始考虑通过调整哪里来提升算法的性能。
关于%prun 的更多信息以及它们可用的参数选项,可以通过IPython 的帮助功能(在 IPython 提示符中输入 %prun?)获取。

1.9.3 用%lprun进行逐行分析

用 %prun 对代码中的每个函数进行分析非常有用,但有时逐行代码分析报告更方便。该功 能并没有内置于 Python 或 IPython,但是可以通过安装 line_profiler 包来实现。首先利用 Python 的包管理工具 pip 安装 line_profiler 包:

pip install line_profiler

接下来可以用 IPython 导入 line_profiler 包提供的 IPython 扩展:

In[9]: %load_ext line_profiler

现在 %lprun 命令就可以对所有函数进行逐行分析了。在下面的例子中,我们需要明确指出 要分析哪些函数:

In[10]: %lprun -f sum_of_lists sum_of_lists(5000)

和前面的性能分析过程一样,Notebook 会在页面上返回结果,如下所示:

Timer unit: 1e-06 s 
Total time: 0.009382 s 
File: <ipython-input-19-fa2be176cc3e> Function: sum_of_lists at line 1 

Line #      Hits         Time  Per Hit   % Time  Line Contents ============================================================== 
1                                           def sum_of_lists(N):      
2         1            2      2.0      0.0      total = 0      
3         6            8      1.3      0.1      for i in range(5):      
4         5         9001   1800.2     95.9          L = [j ^ (j >> i) ...      
5         5          371     74.2      4.0          total += sum(L)      
6         1            0      0.0      0.0      return total

最上面的信息给出了阅读这些结果的关键:报告中的运行时间单位是微秒,我们可以看到 程序中哪些地方最耗时。可以通过这些信息修改代码,使其更高效地实现我们的目的。

1.9.4 用%memit和%mprun进行内存分析

另一种分析是分析一个操作所用的内存量,这可以通过 IPython 的另一个扩展来评估,即 memory_profiler。和 line_profiler 一样,首先用 pip 安装这个扩展:

$ pip install memory_profiler

然后用 IPython 导入该扩展:

In[12]: %load_ext memory_profiler

内存分析扩展包括两个有用的魔法函数:%memit 魔法函数(它提供的内存消耗计算功能类似于 %timeit)和 %mprun 魔法函数(它提供的内存消耗计算功能类似于 %lprun)。 %memit 函数用起来很简单:

In[13]: %memit sum_of_lists(1000000) 

peak memory: 100.08 MiB, increment: 61.36 MiB

可以看到,这个函数大概消耗了 100MB 的内存。
对于逐行代码的内存消耗描述,可以用 %mprun 魔法函数。但不幸的是,这个魔法函数仅仅 对独立模块内部的函数有效,而对于 Notebook 本身不起作用。所以首先用 %%file 魔法函 数创建一个简单的模块,将该模块命名为 mprun_demo.py。它包含 sum_of_lists 函数,该 函数中包含一次加法,能使内存分析结果更清晰:

In[14]: %%file mprun_demo.py         
		def sum_of_lists(N):
			total = 0             
			for i in range(5):                 
				L = [j ^ (j >> i) for j in range(N)]                 
				total += sum(L)                
				del L # remove reference to L           
			return total 

Overwriting mprun_demo.py

现在可以重新导入函数,并运行逐行内存分析器:

In[15]: from mprun_demo import sum_of_lists         
%mprun -f sum_of_lists sum_of_lists(1000000)

页面中打印的结果概述了该函数的内存消耗情况,如下所示:

Filename: ./mprun_demo.py 

Line #    Mem usage    Increment   Line Contents ================================================      
4     71.9 MiB      0.0 MiB           L = [j ^ (j >> i) for j in range(N)] 


Filename: ./mprun_demo.py 

Line #    Mem usage    Increment   Line Contents ================================================      
1     39.0 MiB      0.0 MiB   def sum_of_lists(N):      
2     39.0 MiB      0.0 MiB       total = 0      
3     46.5 MiB      7.5 MiB       for i in range(5):     
4     71.9 MiB     25.4 MiB           L = [j ^ (j >> i) for j in range(N)]    
5     71.9 MiB      0.0 MiB           total += sum(L)     
6     46.5 MiB    -25.4 MiB           del L # remove reference to L     
7     39.1 MiB     -7.4 MiB       return total

Increment 列告诉我们每行代码对总内存预算的影响:创建和删除列表 L 时用掉了 25MB 的内存。这是除了 Python 解释器本身外最消耗内存资源的部分。

1.10 IPython参考资料

1.10.1 网络资源

IPython 网站(http://ipython.org):IPython 网站链接到各种相关文档、示例、教程以及很多其他资源。

nbviewer 网站(http://nbviewer.ipython.org/):该网站展示了网络上任何可用的 IPython Notebook 的静态翻译。该网站的首页展示了一 些示例 Notebook,通过这些示例你可以看到其他人用 IPython 做了什么。

有趣的IPython Notebook 集合(http://github.com/ipython/ipython/wiki/A-gallery-of-interestingIPython-Notebooks/):这是由 nbviewer 运行的最全的 Notebook 列表(并且该列表还在不断增长),展示了通 过 IPython 可以进行多深、多广的数值分析。它还包括短小的示例、全套课程教程以及 Notebook 格式的图书。

1.10.2 相关图书

《利用 Python 进行数据分析》 Wes McKinney 的这本书用一章介绍了如何像数据科学家那样使用 IPython。尽管其中的很多内容与上面介绍的内容有所重复,但多一个视角总不是坏事。

Learning IPython for Interactive Computing and Data Visualization(http://bit.ly/2eLCBB7) Cyrille Rossant 的这本薄书对如何用 IPython 进行数据分析作了很好的介绍。

IPython Interactive Computing and Visualization Cookbook(http://bit.ly/2fCEtNE) 这本也是 Cyrille Rossant 的著作。它篇幅更长,并且深入介绍了将 IPython 用于数据科学的方法。这本书不仅仅是关于 IPython的,还涉及了数据科学中更深、更广的主题。