IPython 交互式计算和可视化学习指南第二版(全)
原文:
annas-archive.org/md5/79c44f926c513143cc5edd8a84c783b9
译者:飞龙
协议:CC BY-NC-SA 4.0
前言
你是一个使用 Python 作为脚本语言的程序员,可能用于软件开发。学习 IPython 将使你能够以高效的方式交互式使用 Python,例如在探索算法或分析数据时。此外,它也是了解平台最先进功能的最佳方式,特别是数值计算、交互式可视化和并行编程。
本书内容简介
第一章, 开始使用 IPython,是对 IPython 关键功能的简短实践性介绍。它将为你提供 IPython 所提供功能的全面概览。本章中介绍的所有功能将在后续章节中进一步讲解。
第二章, 使用 IPython 进行交互式工作,将向你展示如何从 IPython 命令行界面交互式地使用 Python,以及各种魔法命令如何帮助你显著提高工作效率。本章还将介绍 IPython 笔记本,这是一个现代化的工具,便于可重现和协作式的交互式编程。
第三章, 使用 IPython 进行数值计算,介绍了 Numpy 和 Pandas 的数值计算功能,它们可以在 IPython 中方便地使用。这些工具在你需要分析大量数据时,或者在需要进行高效数值计算时至关重要。
第四章, 交互式绘图与图形界面,讲解了 Matplotlib 的图形功能,并展示了它们如何在 IPython 中顺利集成。Matplotlib 是一个非常强大的图形库,允许你生成高质量的图像或交互式地可视化数据。
第五章, 高性能与并行计算,是一本高级章节,详细介绍了加速代码的多种方法,如并行计算和动态 C 语言编译。前者是将任务分配到多个核心或计算机上,这在 IPython 中特别容易实现。后者让你使用 Python 的超集(使用 Cython 库)编写代码,然后动态地将其编译为 C,从而显著提升速度。
第六章, 自定义 IPython,展示了如何自定义 IPython,创建新的魔法命令,并在 IPython 笔记本中使用自定义表示。
本书所需的内容
本书假设读者已经熟悉 Python 语言。此外,你还需要在计算机上安装 Python(支持 Windows、OS X 或 Linux 系统)。你还需要安装 IPython 以及一些其他的外部库。安装过程详见第一章,IPython 入门。
本书适用对象
本书面向那些希望学习 IPython 高级控制台、笔记本以及该平台提供的互动计算功能的 Python 程序员。对互动计算、数据分析和可视化感兴趣的学生、黑客、科学家和爱好者也会对本书感兴趣,但他们需要先学习 Python 的基础知识。幸运的是,Python 是一门非常容易入门的语言,并且有很多书籍、课程和教程可供学习。
约定
本书中,你将会看到许多不同的文本样式,这些样式用来区分不同种类的信息。以下是这些样式的示例,并解释它们的含义。
文本中的代码词汇将按如下方式显示:“例如,标准的 Unix 命令pwd
、ls
、cd
在 IPython 中也可用。”
代码块如下所示:
print("Running script.")
x = 12
print("'x' is now equal to {0:d}.".format(x))
任何命令行输入或输出将按如下方式编写:
In [1]: run script.py
Running script.
'x' is now equal to 12.
In [2]: x
Out[2]: 12
新术语和重要词汇会以粗体显示。你在屏幕上看到的词汇,如在菜单或对话框中出现的词汇,会以这种方式出现在文本中:“点击页面右上方的新建笔记本按钮”。
注意
警告或重要说明会以类似这样的框显示。
小贴士
提示和技巧将以这种形式显示。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么,或者可能不喜欢什么。读者反馈对我们来说非常重要,有助于我们开发出真正能让你从中受益的书籍。
若要向我们发送一般反馈,请通过电子邮件发送至<feedback@packtpub.com>
,并在邮件主题中注明书名。
如果你在某个领域有专业知识并且有兴趣撰写或参与编写书籍,可以查看我们的作者指南,网址为:www.packtpub.com/authors。
客户支持
现在,你已经成为一本 Packt 书籍的骄傲拥有者,我们提供了多种方式帮助你最大限度地从购买中获益。
下载示例代码
你可以从你的 Packt 账户下载所有已购买书籍的示例代码文件,网址:www.packtpub.com
。如果你在其他地方购买了本书,可以访问www.packtpub.com/support
并注册,以便将文件通过电子邮件直接发送给你。此外,所有示例也可以从作者的网站下载:ipython.rossant.net
。
勘误
虽然我们已尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误—无论是文本错误还是代码错误—我们将非常感激您向我们报告。这样,您不仅能帮助其他读者避免困扰,还能帮助我们改进该书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/support
,选择您的书籍,点击勘误提交表格链接,并输入勘误的详细信息。一旦您的勘误被验证,您的提交将被接受,勘误将上传到我们的网站,或添加到该书的现有勘误列表中,出现在该书的勘误部分。
盗版
互联网上的版权材料盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上发现任何非法的我们的作品的副本,请立即提供相关地址或网站名称,以便我们采取补救措施。
如果您发现疑似盗版材料,请通过<copyright@packtpub.com>
与我们联系,并提供相关链接。
我们感谢您在保护我们的作者以及我们为您提供有价值内容方面的帮助。
问题
如果您在本书的任何方面遇到问题,请通过<questions@packtpub.com>
与我们联系,我们将尽力解决。
第一章:开始使用 IPython
在本章中,我们将首先介绍 IPython 的安装过程,并概述 IPython 所提供的各种功能。IPython 带来了经过高度改进的 Python 控制台和 Notebook。此外,当与第三方专业包(如 NumPy 和 Matplotlib)结合使用时,它是进行交互式计算的一个重要工具。这些包为 Python 生态系统带来了高性能计算和交互式可视化功能,而 IPython 则是其基石。在本章结束时,您将能够在计算机上安装 IPython 及其所需的包,并且您将通过一个简短的实践概述,了解我们将在后续章节中详细介绍的 IPython 最重要的功能,具体包括:
-
运行 IPython 控制台
-
将 IPython 作为系统 Shell 使用
-
使用历史记录
-
Tab 补全
-
使用
%run
命令执行脚本 -
使用
%timeit
命令进行快速基准测试 -
使用
%pdb
命令进行快速调试 -
使用 Pylab 进行交互式计算
-
使用 IPython Notebook
-
自定义 IPython
安装 IPython 及推荐的包
在本节中,我们将介绍如何安装 IPython 以及我们将在本书中使用的其他包。有关 IPython 安装的最新信息,您应查看 IPython 的官方网站(ipython.org
)。
IPython 的先决条件
首先,在安装 IPython 之前,您的计算机需要具备哪些条件?好消息是,IPython,以及更一般的所有 Python 包,原则上可以在大多数平台上运行(即 Linux、Apple OS X 和 Microsoft Windows)。在安装和运行 IPython 之前,您还需要在系统上安装一个有效的 Python 发行版。目前撰写本书时,IPython 的最新稳定版本是 0.13.1,并且官方要求安装 Python 2.6、2.7、3.1 或 3.2 版本。
提示
Python 2.x 和 3.x
Python 3.x 分支与 Python 2.x 分支不兼容,这也是为什么 2.7 版本仍然在维护的原因。尽管本书中使用的大多数外部 Python 包与 Python 3.x 兼容,但仍有一些包不兼容该分支。此时,对于一个新项目来说,选择 Python 2.x 还是 Python 3.x 通常取决于所需的外部 Python 包对 Python 3 的支持情况。目标用户的系统配置也是一个重要的考虑因素。在本书中,我们将使用 Python 2.7,并尽量减少与 Python 3.x 的兼容性问题。此问题超出了本书的范围,我们鼓励您查找有关如何编写尽可能兼容 Python 3.x 的 Python 2.x 代码的信息。以下官方网站页面是一个很好的起点:
wiki.python.org/moin/Python2orPython3
本书将使用 Python 2.7 版本。2.6 版本不再维护,如果您决定继续使用 2.x 分支,尽可能选择使用 Python 2.7。
本书中还将使用其他与 IPython 一起使用的 Python 包。这些包主要是 NumPy、SciPy 和 Matplotlib,但在一些示例中我们还将使用其他包。有关如何安装这些包的详细信息,请参见下一节安装一体化分发版。
有几种安装 IPython 和推荐包的方法。从最简单到最复杂,您可以选择以下任一方法:
-
安装一个独立的一体化 Python 发行版,带有各种内置 Python 包
-
只安装您需要的包
在后一种情况下,您可以使用二进制安装程序,或者直接从源代码安装这些包。
安装一体化分发版
这是目前最简单的解决方案。您可以下载一个单一的二进制安装程序,它包含完整的 Python 发行版和许多广泛使用的外部包,包括 IPython。流行的发行版包括:
-
Enthought Python Distribution (EPD)和 Enthought 的新 Canopy:
www.enthought.com/
-
Anaconda,由 Continuum Analytics 开发:
www.continuum.io/
-
Python(x,y),一个开源项目:
code.google.com/p/pythonxy/
-
ActivePython,由 ActiveState 提供:
www.activestate.com/activepython
所有这些发行版都支持 Linux、OS X 和 Windows,除了 Python(x,y)仅支持 Windows。它们都提供免费版本(并可能提供商业版本),并且都包含 IPython。ActivePython 和 EPD 也附带自己的打包系统;这使得安装额外包变得容易。这些发行版包含了我们在本书中将使用的大多数外部包。
一个一个地安装这些包
有时,您可能更愿意只安装所需的包,而不是安装一个庞大的所有功能包。幸运的是,这在大多数最新的系统上应该是直接的。Windows、OS X 和大多数常见的 Linux 发行版都可以使用二进制安装程序。否则,您始终可以从源代码安装这些包,通常这比看起来要容易。
包网站
下面是我们在本书中提到的 Python 包的列表,以及它们各自的网站,您可以在这些网站上找到最新的相关信息:
-
IPython:
ipython.org
-
NumPy,用于多维数组的高性能和向量化计算:
www.numpy.org
-
SciPy,用于高级数值算法:
www.scipy.org
-
Matplotlib,用于绘图和交互式可视化:
matplotlib.org
-
Matplotlib-basemap,一个用于 Matplotlib 的地图工具箱:
matplotlib.org/basemap/
-
NetworkX,用于处理图形:
networkx.lanl.gov
-
Pandas,用于处理各种表格数据:
pandas.pydata.org
-
Python Imaging Library(PIL),用于图像处理算法:
www.pythonware.com/products/pil
-
PySide,Qt 的图形用户界面(GUI)封装:
qt-project.org/wiki/PySide
-
PyQt,类似于 PySide,但许可证不同:
www.riverbankcomputing.co.uk/software/pyqt/intro
-
Cython,用于在 Python 中使用 C 代码:
cython.org
小贴士
PyQt 还是 PySide?
Qt 是一个跨平台的应用框架,广泛用于带有 GUI 的软件。它有着复杂的历史;最初由 Trolltech 开发,后来被诺基亚收购,现在由 Digia 所拥有。既有商业许可证,也有开源许可证。PyQt 是一个由 Riverbank Computing 开发的 Qt 的 Python 封装。PyQt 的开源版本是 GPL 许可证,这限制了它在商业产品中的使用。因此,诺基亚决定创建一个自己拥有 LGPL 许可证的包,名为 PySide。现在,由 Qt 项目进行维护。今天,两个包共存,并且它们具有极其相似的 API,因此可以用 Python 编写支持这两个库的 Qt 图形应用程序。
这些网站提供了各种系统的二进制安装程序下载,并且还提供了源代码供手动编译和安装。
还有一个名为 Python Package Index(PyPI)的 Python 包在线仓库,网址是 pypi.python.org
。它包含大多数现有 Python 包的 tarballs 和有时的 Windows 安装程序。
获取二进制安装程序
你可以在你感兴趣的包的官方网站上找到适合你系统的二进制安装程序。如果没有官方的二进制安装程序,社区可能已经创建了非官方的安装程序。我们将在这里提供一些关于在不同操作系统上找到二进制安装程序的建议。
Windows
官方 Windows 安装程序可以在相关包的官方网站或 PyPI 上找到某些包的安装程序。数百个 Python 包(包括 IPython 和本书中使用的所有包)的非官方 Windows 安装程序可以在 Christoph Gohlke 的个人网页上找到,网址为 www.lfd.uci.edu/~gohlke/pythonlibs/
。这些文件不提供任何形式的保证,但通常都非常稳定,这使得在 Windows 上安装几乎所有 Python 包变得极为简便。所有包都有适用于 Python 2.x 和 3.x 的版本,以及适用于 32 位和 64 位 Python 发行版的版本。
OS X
官方的 OS X 安装包可以在一些软件包的官方网站上找到,非官方安装包可以在 MacPorts 项目(www.macports.org
)和 Homebrew(mxcl.github.com/homebrew/
)中找到。
Linux
大多数 Linux 发行版(包括 Ubuntu)都配备了可能包含你所需的 Python 版本和我们将在此使用的大多数 Python 软件包的打包系统。例如,要在 Ubuntu 上安装 IPython,请在终端中输入以下命令:
$ sudo apt-get install ipython-notebook
在 Fedora 18 及更高版本的相关发行版中,输入以下命令:
$ sudo yum install python-ipython-notebook
相关的二进制软件包名称有时会以 python-
为前缀(例如,python-numpy
或 python-matplotlib
)。此外,PyQt4 的软件包名称是 python-qt4
,PyOpenGL 的软件包名称是 python-opengl
,PIL 的软件包名称是 python-imaging
,等等。
二进制软件包表
这里展示了一张表格,列出了在不同的 Python 发行版和操作系统中,本书将使用的各个软件包的二进制安装包的可用性(截至写作时)。所有这些安装包都可以用于 Python 2.7。下表中,"(W)" 表示 Windows,“CG:” 表示 Christoph Gohlke 的网页:
Package | EPD 7.3 | Anaconda 1.2.1 | Python(x,y) 2.7.3 | ActivePython 2.7.2 | Windows 安装包 | Ubuntu 安装包 | OSX 安装包(MacPorts) |
---|---|---|---|---|---|---|---|
NetworkX | 1.6 | 1.7 | 1.7 | 1.6 | CG: 1.7 | 1.7 | 1.7 |
Pandas | 0.9.1 | 0.9.0 | 0.9.1 | 0.7.3 | CG: 0.10.0, PyPI: 0.10.0 | 0.8.0 | 0.10.0 |
NumPy | 1.6.1 | 1.6.2 (W) | 1.6.2 | 1.6.2 | CG: 1.6.2, PyPI: 1.6.2(32 位) | 1.6.2 | 1.6.2 |
SciPy | 0.10.1 | 0.11.0 | 0.11.0 | 0.10.1 | CG: 0.11.0 | 0.10.1 | 0.11.0 |
PIL | 1.1.7 | 1.1.7 | 1.1.7 | 1.1.7 | CG: 1.1.7 | 1.1.7 | N/A |
Matplotlib | 1.1.0 | 1.2.0 | 1.1.1 | 1.1.0 | CG: 1.2.0 | 1.1.1 | 1.2.0 |
Basemap | 1.0.2 | N/A | 1.0.2(可选) | 1.0 beta | 1.0.5 | 1.0.5 | 1.0.5 |
PyOpenGL | 3.0.1 | N/A | 3.0.2 | 3.0.2 | CG: 3.0.2, PyPI: 3.0.2 | 3.0.1 | 3.0.2 |
PySide | 1.1.1 | 1.1.2 | N/A(PyQt 4.9.5) | N/A(PyQt 4.8.3) | CG: 1.1.2 | 1.1.1 | 1.1.2 |
Cython | 0.16 | 0.17.1 | 0.17.2 | 0.16 | CG: 0.17.3 | 0.16 | 0.17.3 |
Numba | N/A | 0.3.2 | N/A | N/A | CG: 0.3.2 | N/A | N/A |
使用 Python 打包系统
当没有可用的二进制软件包时,安装 Python 软件包的通用方式是直接从源代码安装。Python 打包系统旨在简化此步骤,以处理依赖关系管理、卸载和软件包发现。然而,打包系统多年来一直处于混乱状态。
Distutils,Python 的原生打包系统,一直因效率低下和带来过多问题而受到批评。其继任者 Distutils2 在写作时尚未完成。Setuptools 是一种替代系统,提供了 easy_install
命令行工具,允许通过命令行搜索(在 PyPI 上)并安装新的 Python 包。安装新包就像在命令行中输入以下命令一样简单:
$ easy_install ipython
Setuptools 也一直受到批评,现已被 Distribute 取代。easy_install
工具也正在被 pip 取代,pip 是一个更强大的工具,用于搜索、安装和卸载 Python 包。
目前,我们建议你使用 Distribute 和 pip。两者都可以通过源代码 tarball 或使用 easy_install 安装(这需要提前安装 Setuptools)。有关如何安装这些工具的更多细节,可以参考《打包指南》(guide.python-distribute.org/
)。要使用 pip 安装新包,请在命令行中输入以下命令:
$ pip install ipython
IPython 的可选依赖项
IPython 有几个依赖项:
-
pyreadline:此依赖项提供行编辑功能
-
pyzmq:此依赖项是 IPython 并行计算功能所需的,例如 Qt 控制台和 Notebook
-
pygments:此依赖项在 Qt 控制台中高亮显示语法
-
tornado:此依赖项是 Web-based Notebook 所必需的
当你从二进制包安装 IPython 时,这些依赖项会自动安装,但从源代码安装 IPython 时则不会。在 Windows 上,pyreadline 必须通过 PyPI 上提供的二进制安装程序,或 Christoph Gohlke 的网页,或者使用 easy_install 或 pip 安装。
在 OS X 上,你也应该使用 easy_install 或 pip 安装 readline。
其他依赖项可以通过以下命令自动安装:
$ easy_install ipython[zmq,qtconsole,notebook]
安装开发版本
最有经验的用户可能希望使用一些库的最新开发版本。详细信息可以在各自库的网站上找到。例如,要安装 IPython 的开发版本,我们可以输入以下命令(需要安装版本控制系统 Git):
$ git clone https://github.com/ipython/ipython.git
$ cd ipython
$ python setup.py install
为了能够轻松更新 IPython(通过使用 git pull
,以便它随开发分支的变化而变化),我们只需将最后一行替换为以下命令(需要安装 Distribute 库):
$ python setupegg.py develop
提示
获取 IPython 帮助
官方的 IPython 文档网页ipython.org/documentation.html
是获取帮助的好地方。它包含了指向在线手册、社区创建的非官方教程和文章的链接。StackOverflow 网站stackoverflow.com/questions/tagged/ipython
也是请求 IPython 帮助的好地方。最后,任何人都可以订阅 IPython 用户邮件列表mail.scipy.org/mailman/listinfo/ipython-user
。
十个 IPython 必备命令
在本节中,我们将快速浏览 IPython,介绍这个强大工具的 10 个必备功能。尽管时间简短,但这次实践将涵盖广泛的 IPython 功能,之后的章节将对其进行更详细的探讨。
运行 IPython 控制台
如果 IPython 已经正确安装,你应该能够通过系统 Shell 使用ipython
命令运行它。你可以像使用常规的 Python 解释器一样使用这个提示符,如下面的截图所示:
IPython 控制台
提示
Windows 上的命令行 Shell
如果你在 Windows 上使用旧版的cmd.exe
shell,你应该知道这个工具非常有限。你可以选择使用更强大的解释器,比如微软的 PowerShell,它在 Windows 7 和 8 中默认集成。大多数常见的与文件系统相关的命令(如pwd
、cd
、ls
、cp
、ps
等)与 Unix 中的命令名称相同,这一点就足以成为切换的理由。
当然,IPython 远不止这些。例如,IPython 附带了许多小命令,可以大大提高生产力。我们将在本书中看到其中的许多命令,从这一部分开始。
其中一些命令帮助你获取有关任何 Python 函数或对象的信息。例如,你是否曾经对如何使用super
函数来访问派生类中的父类方法感到困惑?只需输入super?
(这是命令%pinfo super
的快捷方式),你就可以找到关于super
函数的所有信息。将?
或??
附加到任何命令或变量后,你将获得关于它的所有信息,如下所示:
In [1]: super?
Typical use to call a cooperative superclass method:
class C(B):def meth(self, arg):super(C, self).meth(arg)
将 IPython 用作系统 Shell
你可以将 IPython 命令行界面作为一个扩展的系统 Shell 使用。你可以浏览整个文件系统并执行任何系统命令。例如,标准的 Unix 命令pwd
、ls
和cd
在 IPython 中也可以使用,并且在 Windows 上同样有效,如下面的示例所示:
In [1]: pwd
Out[1]: u'C:\\'
In [2]: cd windows
C:\windows
这些命令是 IPython Shell 中的特殊魔法命令。魔法命令有很多,我们将在本书中使用其中的许多。你可以使用%lsmagic
命令查看所有魔法命令的列表。
提示
使用 IPython 魔法命令
魔法命令实际上以%
为前缀,但默认启用的自动魔法系统使你可以方便地省略该前缀。使用前缀始终是可能的,特别是在没有前缀的命令被与之同名的 Python 变量遮盖时。%automagic
命令切换自动魔法系统。在本书中,我们通常会使用%
前缀来表示魔法命令,但请记住,如果你愿意,大多数时候可以省略它。
使用历史记录
像标准的 Python 控制台一样,IPython 提供了命令历史记录。然而,与 Python 控制台不同,IPython 的历史记录跨越了你之前的交互式会话。此外,一些键击和命令可以帮助你减少重复输入。
在 IPython 控制台提示符下,使用上下箭头键可以浏览整个输入历史记录。如果在按下箭头键之前开始输入,只会显示与已输入内容匹配的命令。
在任何交互式会话中,你的输入和输出历史保存在In
和Out
变量中,并按提示号进行索引。_
、__
、___
以及_i
、_ii
、_iii
变量分别包含最后三个输出和输入对象。_n
和_in
变量返回第n个输出和输入历史记录。例如,我们输入以下命令:
In [4]: a = 12
In [5]: a ** 2
Out[5]: 144
In [6]: print("The result is {0:d}.".format(_))
The result is 144.
在这个示例中,我们在第 6 行显示了提示符5
的输出,即144
。
Tab 自动补全
Tab 自动补全非常有用,你会发现自己一直在使用它。每当你开始输入命令、变量名或函数时,按下Tab键,IPython 会自动完成你输入的内容(如果没有歧义),或者显示与已输入内容匹配的命令或名称列表。它也适用于目录和文件路径,就像在系统 shell 中一样。
它对动态对象反射特别有用。输入任何 Python 对象名称后跟一个点,然后按下Tab键;IPython 会显示现有的属性和方法列表,如以下示例所示:
In [1]: import os
In [2]: os.path.split<TAB>
os.path.split os.path.splitdrive os.path.splitext os.path.splitunc
在第二行,如前面的代码所示,我们在输入完os.path.split
后按下Tab键。IPython 会显示所有可能的命令。
提示
Tab 自动补全与私有变量
Tab 自动补全会显示对象的所有属性和方法,除了那些以下划线(_
)开头的属性和方法。原因是,在 Python 编程中,使用下划线前缀表示私有变量。这是一个标准的惯例。如果你希望 IPython 显示所有私有属性和方法,可以在按下Tab键之前输入myobject._
。在 Python 中,没有什么是真正私有或隐藏的。这是 Python 的一种哲学,正如著名的说法所表达的那样:“我们都是成年人,在这里自愿参与。”
使用%run
命令执行脚本
尽管交互式控制台很重要,但在运行多个命令时,它会显得有些局限。在 Python 脚本中编写多个命令并使用.py
扩展名(按惯例)是非常常见的。可以通过%run
魔法命令在 IPython 控制台内执行 Python 脚本,后面跟上脚本的文件名。除非使用了-i
选项,否则脚本将在一个新的 Python 命名空间中执行;如果使用了该选项,则会使用当前的交互式 Python 命名空间进行执行。在所有情况下,脚本中定义的所有变量会在脚本执行完毕后可在控制台中使用。
让我们将以下 Python 脚本写入一个名为script.py
的文件中:
print("Running script.")
x = 12
print("'x' is now equal to {0:d}.".format(x))
现在,假设我们已经进入了包含该文件的目录,可以通过输入以下命令在 IPython 中执行它:
In [1]: %run script.py
Running script.
'x' is now equal to 12.
In [2]: x
Out[2]: 12
在运行脚本时,控制台的标准输出会显示任何 print 语句。执行结束后,脚本中定义的x
变量将被包含在交互式命名空间中,这非常方便。
使用%timeit 命令进行快速基准测试
你可以在交互式会话中使用%timeit
魔法命令进行快速基准测试。它可以估算单个命令执行所需的时间。该命令会在循环中多次执行,且该循环本身默认会重复多次。然后,命令的单次执行时间会通过平均值自动估算。-n
选项控制循环中执行的次数,-r
选项控制循环执行的次数。例如,输入以下命令:
In[1]: %timeit [x*x for x in range(100000)]
10 loops, best of 3: 26.1 ms per loop
在这里,计算从1
到100000
的所有整数的平方大约用了 26 毫秒。
使用%debug 命令进行快速调试
IPython 自带了一个强大的命令行调试器。每当控制台中抛出异常时,可以使用%debug
魔法命令在异常点启动调试器。然后,你可以访问所有局部变量并查看完整的栈追踪信息,在事后调试模式下进行分析。通过u
和d
命令可以在栈中向上和向下导航,使用q
命令退出调试器。通过输入?
命令查看调试器中所有可用命令的列表。
你可以使用%pdb
魔法命令来激活 IPython 调试器的自动执行,一旦抛出异常,调试器就会启动。
使用 Pylab 进行交互式计算
%pylab
魔法命令启用了 NumPy 和 matplotlib 包的科学计算功能,即对向量和矩阵的高效操作、绘图和交互式可视化功能。通过它,可以在控制台中执行交互式计算并动态绘制图形。例如,输入以下命令:
In [1]: %pylab
Welcome to pylab, a matplotlib-based Python environment [backend: TkAgg].
For more information, type 'help(pylab)'.
In [2]: x = linspace(-10., 10., 1000)
In [3]: plot(x, sin(x))
在此示例中,我们首先定义一个包含1000
个值的向量,这些值在-10
和10
之间线性分布。然后我们绘制图形(x, sin(x))
。一个带有图形的窗口会出现,如下截图所示,而控制台在该窗口打开时不会被阻塞。这使得我们能够在窗口打开时交互式地修改图形。
一个 Matplotlib 图形
使用 IPython Notebook
Notebook 将 IPython 的功能带入浏览器,提供多行文本编辑功能、交互式会话的可重复性等。这是一种使用 Python 进行交互式和可重复性操作的现代且强大的方式。
要使用 Notebook,请在终端中调用ipython notebook
命令(确保已安装安装部分中描述的所需依赖项)。这将启动一个本地 Web 服务器,默认端口为 8888。然后在浏览器中访问http://127.0.0.1:8888/
并创建一个新的 Notebook。
你可以在输入单元格中编写一行或多行代码。以下是一些最有用的键盘快捷键:
-
按Enter键在单元格中创建新的一行,而不执行单元格
-
按Shift + Enter执行单元格并跳转到下一个单元格
-
按下Alt + Enter执行单元格,并在其后添加一个新的空单元格
-
按Ctrl + Enter,快速进行实验而不保存输出
-
按Ctrl + M,然后按H键显示所有键盘快捷键的列表
我们将在下一章中更详细地探讨 Notebook 的功能。
自定义 IPython
你可以将用户偏好设置保存在一个 Python 文件中;该文件称为 IPython 配置文件。要创建默认配置文件,在终端中输入ipython profile create
。这将创建一个名为profile_default
的文件夹,位于~/.ipython
或~/.config/ipython
目录下。该文件夹中的ipython_config.py
文件包含有关 IPython 的偏好设置。你可以使用ipython profile create profilename
创建具有不同名称的配置文件,然后通过ipython --profile=profilename
启动 IPython 并使用该配置文件。
~
目录是你的主目录,例如,在 Unix 系统中是类似/home/yourname
的路径,在 Windows 系统中是C:\Users\yourname
或C:\Documents and Settings\yourname
。
总结
在本章中,我们详细介绍了安装 IPython 的各种方式以及推荐的外部 Python 包。最直接的方式是安装一个独立的 Python 发行版,内置所有包,例如 Enthought Python Distribution、Canopy、Anaconda、Python(x,y)或 ActivePython 等。另一种解决方案是手动安装不同的包,可以通过适用于大多数最新平台的二进制安装程序,或者使用 Python 的打包系统,在大多数情况下应当非常简单。
我们还介绍了 IPython 提供的 10 个最有趣的功能。这些功能主要涉及 Python 和 shell 的交互特性,包括集成的调试器和分析器,以及 NumPy 和 Matplotlib 包所带来的交互式计算和可视化功能。在接下来的章节中,我们将详细介绍交互式 shell 和 Python 控制台以及 Notebook。
第二章:与 IPython 进行交互工作
在本章中,我们将详细介绍 IPython 为标准 Python 控制台带来的各种改进。特别是,我们将执行以下任务:
-
从 IPython 访问系统 shell,实现强大的 shell 和 Python 之间的交互
-
使用动态内省来探索 Python 对象,甚至是一个新的 Python 包,甚至无需查看文档
-
从 IPython 轻松调试和基准测试您的代码
-
学习如何使用 IPython 笔记本显著改善与 Python 的交互方式
扩展的 shell
IPython 不仅是一个扩展的 Python 控制台,还提供了几种在 Python 交互会话期间与操作系统交互的方式,而无需退出控制台。IPython 的 shell 功能并不意味着取代 Unix shell,并且 IPython 提供的功能远不及 Unix shell。然而,在 Python 会话期间浏览文件系统并偶尔从 IPython 调用系统命令仍然非常方便。此外,IPython 提供了有用的魔术命令,可以显著提高生产力,并在交互会话期间减少重复输入。
浏览文件系统
在这里,我们将展示如何从互联网下载并提取压缩文件,在文件系统层次结构中导航,并从 IPython 打开文本文件。为此,我们将使用一个关于数百名匿名 Facebook 用户社交网络的真实数据示例(他们自愿匿名分享数据以供计算机科学家进行研究)。这些受 BSD 许可的数据由斯坦福大学的 SNAP 项目免费提供(snap.stanford.edu/data/
)。
提示
下载示例代码
您可以从您在www.packtpub.com
购买的所有 Packt 图书的帐户中下载示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support
并注册,以便直接通过电子邮件接收文件。此外,所有示例都可以从作者的网站下载:ipython.rossant.net
。
首先,我们需要从作者的网页下载包含数据的 ZIP 文件。我们使用原生 Python 模块urllib2
下载文件,并使用zipfile
模块进行解压。让我们输入以下命令:
In [1]: import urllib2, zipfile
In [2]: url = 'http://ipython.rossant.net/'
In [3]: filename = 'facebook.zip'
In [4]: downloaded = urllib2.urlopen(url + filename)
在这里,我们在内存中下载了文件http://ipython.rossant.net/facebook.zip
,并将其保存在硬盘上。
现在,我们在当前目录中创建一个名为data
的新文件夹,并进入其中。美元符号($
)允许我们在系统或魔术命令中使用 Python 变量。让我们输入以下命令:
In [5]: folder = 'data'
In [6]: mkdir $folder
In [7]: cd $folder
在这里,mkdir
是一个特定的 IPython 别名,它将一个魔法命令重定向为一个 shell 命令。可以通过魔法命令 %alias
获取别名列表。在这个文件夹中,我们将保存刚刚下载的文件(在第八行,我们将 ZIP 文件 facebook.zip
本地保存到当前目录 data
中),并在当前文件夹中解压它(如第九行所示,使用 zip
的 extractall
方法和 ZipFile
对象)。让我们输入以下命令:
In [8]: with open(filename, 'wb') as f:f.write(downloaded.read())
In [9]: with zipfile.ZipFile(filename) as zip:zip.extractall('.')
让我们使用以下命令来探索刚刚下载的内容:
In [10]: ls
facebook facebook.zip
In [11]: cd facebook
In [12]: ls
0.circles 0.edges [...]
在此示例中,每个数字表示一个 Facebook 用户(称为 ego
用户)。.edges
文件包含其 社交图,即每个节点是一个朋友,如果 ego
用户的两个朋友互为朋友,则它们之间有连接。该图以 edges
列表的形式存储,这是一个文本文件,每行包含两个用空格分隔的连接节点标识符。.circles
文件包含手动创建的朋友列表,即从 ego
用户的视角出发,拥有共同属性的朋友组。
最后,我们使用以下命令将当前 facebook
目录保存为书签,以便以后可以轻松进入该目录:
In [13]: %bookmark fbdata
现在,在任何未来使用相同 IPython 配置文件的会话中,我们都可以键入 cd fbdata
进入此目录,无论我们从哪个目录调用此命令。-l
和 -d
选项分别允许列出所有定义的书签和删除指定书签。输入 %bookmark?
会显示所有选项的列表。当在多个文件夹之间来回导航时,这个魔法命令非常有用。
IPython 中另一个方便的与导航相关的功能是 Tab 补全。只要按下 Tab 键,IPython 就能自动完成我们正在输入的文件或文件夹名称。如果有多个选项,IPython 会显示所有可能选项的列表。它也适用于文件名,例如在 open
内置函数中,如下例所示:
In [1]: pwd
/home/me/data/
In [2]: cd fa<TAB>
/home/me/data/facebook/
In [2]: cd facebook
In [3]: with open('0<TAB>
0.circles 0.edges
In [3]: with open('0.edges', 'r') as f:print(f.readline())
236 186
从 IPython 访问系统 shell
我们还可以直接从 IPython 启动命令,使用系统 shell 并将结果作为字符串列表保存在 Python 变量中。为此,我们需要在 shell 命令前加上 !
。例如,假设我们使用的是 Unix 系统,我们可以输入以下命令:
In [1]: cd fbdata
/home/me/data/facebook
In [2]: files = !ls -1 -S | grep edges
Unix 命令 ls -1 -S
列出当前目录中的所有文件,按大小降序排序,并且每行列出一个文件。管道 | grep edges
仅筛选包含 edges
的文件(这些是包含不同网络社交图的文件)。然后,Python 变量 files
包含所有文件名的列表,如下例所示:
In [3]: files
Out[3]: ['1912.edges','107.edges',[...]'3980.edges']
我们还可以在系统命令中使用 Python 变量,使用 $
语法表示单个变量,或使用 {}
来表示任何 Python 表达式,如下所示:
In [4]: !head -n5 {files[0]}
2290 2363
2346 2025
2140 2428
2201 2506
2425 2557
head -n5 {files[0]}
命令显示 files
列表中第一个文件的前五行,即数据集中最大 .edges
文件的前五行。
如果我们发现自己一遍又一遍地使用相同的命令,我们可以创建一个 别名 来减少重复输入,使用魔法命令 %alias
。例如,在以下示例中,我们创建了一个名为 largest
的别名,用于显示所有文件及其大小(-hs
),并按其大小降序排序(-S
),同时使用指定的字符串(grep
)进行过滤,显示在单列中(-1
):
In [5]: %alias largest ls -1sSh | grep %s
in [6]: largest circles
6.0K 1912.circles
4.0K 1684.circles
[...]
在第五行中,注意 %s
是 largest
别名的定位符,它会被传递给别名的任何参数所替代(如第六行所示)。
请注意,默认情况下,这个别名不会保存到下次交互式会话中(关闭 IPython 后)。我们需要使用 %store
魔法命令显式地保存它,如下所示:
In [7]: %store largest
Alias stored: largest (ls -1sSh | grep %s)
此外,要在以后的会话中恢复存储的别名和变量,我们需要输入 %store -r
。
扩展的 Python 控制台
我们现在将探索 IPython 控制台与 Python 相关的功能。
浏览历史记录
IPython 会跟踪我们所有会话中的输入历史记录。由于在使用 IPython 数月或数年后,这些历史记录可能会变得相当庞大,因此提供了方便的方式来浏览历史记录。
首先,我们可以随时按下上下键,在 IPython 提示符下线性地浏览我们最近的历史记录。如果我们在按上下键之前输入了内容,系统只会在匹配我们已输入的内容的命令中浏览历史记录。按下 Ctrl + R 会打开一个提示符,让我们搜索包含我们输入的任何内容的行。
%history
魔法命令(以及它的别名 %hist
)接受多个方便的选项,来显示我们感兴趣的输入历史记录部分。默认情况下,%history
会显示当前会话中的所有输入历史记录。我们可以使用简单的语法指定特定的行范围,例如,hist 4-6 8
表示第四到第六行和第八行。我们还可以选择使用语法 hist 243/4-8
来显示第 243 号会话中的第四到第八行历史记录。最后,我们可以使用语法 %hist ~1/7
来相对于当前会话编号历史记录行,它会显示前一个会话的第七行。
%history
还有其他有用的选项,包括 -o
,它会显示输入历史记录的输出;-n
,它会显示行号;-f
,它会将历史记录保存到文件;-p
,它会显示经典的 >>>
提示符。例如,这对于自动从历史记录中创建 doctest
文件可能会非常有用。另外,-g
选项允许使用指定的字符串(如 grep
)过滤历史记录。考虑以下示例:
In [1]: 2 + 3
Out[1]: 5
In [2]: _ * 2
Out[2]: 10
In [3]: %hist -nop 1-21: >>> 2 + 3
52: >>> _ * 2
10
在这个例子中,我们显示了前两行的历史记录,包括行号、输出和默认的 Python 提示符。
最后,一个相关的命令是%store
,用于保存任何 Python 变量的内容,以便在任何未来的交互式会话中使用。%store name
命令保存变量name
,%store -d name
删除它。要恢复存储的变量,我们需要使用%store -r
。
导入/导出 Python 代码
在接下来的部分中,我们将首先看到如何从 Python 脚本中的交互式控制台导入代码,然后看看如何将历史记录中的代码导出到外部文件。
在 IPython 中导入代码
在 IPython 中导入代码的第一种可能性是将代码从文件复制粘贴到 IPython 中。当使用IPython控制台时,%paste
魔术命令可用于导入并执行剪贴板中包含的代码。IPython 会自动去除代码的缩进,并删除行首的>
和+
字符,从而可以直接从电子邮件中粘贴diff
和doctest
文件。
此外,%run
魔术命令在控制台中执行 Python 脚本,默认情况下在一个空的命名空间中。这意味着在交互式命名空间中定义的任何变量在执行的脚本中不可用。然而,在执行结束时,控制返回到 IPython 提示符,并且在交互式命名空间中导入了脚本中定义的变量。这对于探索脚本执行结束时所有变量的状态非常方便。可以使用-i
选项更改此行为,该选项在执行时使用交互式命名空间。在执行脚本之前在交互式命名空间中定义的变量然后在脚本中可用。
例如,让我们编写一个脚本/home/me/data/egos.py
,列出 Facebook 的data
文件夹中的所有自我标识符。由于每个文件名的格式为<egoid>.<extension>
,我们列出所有文件,删除扩展名,并获取所有唯一标识符的排序列表。脚本应包含以下代码:
import sys
import os
# we retrieve the folder as the first positional argument
# to the command-line call
if len(sys.argv) > 1:folder = sys.argv[1]
# we list all files in the specified folder
files = os.listdir(folder)
# ids contains the sorted list of all unique idenfitiers
ids = sorted(set(map(lambda file: int(file.split('.')[0]), files)))
这里解释了最后一行的作用。lambda
函数以<egoid>.<extension>
的模板作为参数,返回egoid
ID 作为整数。它使用任何字符串的split
方法,该方法使用给定字符拆分字符串,并返回由此字符分隔的子字符串列表。在这里,列表的第一个元素是<egoid>
部分。内置的 Python map
函数将此lambda
函数应用于所有文件名。set
函数将此列表转换为set
对象,从而删除所有重复项,仅保留唯一标识符的列表(因为任何标识符都会以两种不同的扩展出现两次)。最后,sorted
函数将set
对象转换为列表,并按升序排序。
假设 IPython 中的当前目录是 /home/me/data
,以下是执行此脚本的命令:
In [1]: %run egos.py facebook
In [2]: ids
Out[2]: [0, 107, ..., 3980]
在 egos.py
脚本中,文件夹名称 facebook
是从命令行参数中检索的,就像在标准命令行 Python 脚本中一样,使用 sys.argv[1]
。脚本执行后,在交互式命名空间中可用的 ids
变量包含了唯一 ego
标识符的列表。
现在,如果我们不将文件夹名称作为脚本的参数提供,将会发生以下情况:
In [3]: folder = 'facebook'
In [4]: %run egos.py
NameError: name 'folder' is not defined
In [5]: %run -i egos.py
In [6]: ids
Out[6]: [0, 107, ..., 3980]
在第四行引发了一个异常,因为未定义 folder
。如果希望脚本使用交互式命名空间中定义的 folder
变量,需要使用 -i
选项。
提示
探索性研究中的交互式工作流
在探索性研究或数据分析中的标准工作流程是在一个或多个 Python 模块中实现算法,并编写一个执行完整流程的脚本。然后可以使用 %run
执行此脚本,并允许进一步交互式探索脚本变量。这个迭代过程涉及在文本编辑器和IPython控制台之间切换。一个更现代和实用的方法是使用 IPython 笔记本,我们将在 使用 IPython 笔记本 部分看到。
将代码导出到文件
虽然 %run
魔术命令允许从文件导入代码到交互式控制台,但 %edit
命令则相反。默认情况下,%edit
打开系统的文本编辑器,并在关闭编辑器时执行代码。如果向 %edit
提供参数,此命令将尝试使用我们提供的代码打开文本编辑器。参数可以是以下内容:
-
Python 脚本文件名
-
包含 Python 代码的字符串变量
-
一系列行号,与之前使用的
%history
语法相同 -
任何 Python 对象,此时 IPython 将尝试使用包含定义此对象的文件打开编辑器
使用 IPython 与多行文本编辑器的更现代和强大的方式是使用笔记本,我们将在 使用 IPython 笔记本 部分看到。
动态内省
IPython 提供了几个功能,用于动态检查命名空间中的 Python 对象。
制表完成
随时可以在控制台中键入 TAB
,让 IPython 完成或提供与我们迄今为止键入的内容匹配的可能名称或命令列表。这允许特别动态地检查任何 Python 对象的所有属性和方法。
制表完成也适用于交互式命名空间中的全局变量、模块和当前目录中的文件路径。默认情况下,以 _
(下划线)开头的变量不会显示,因为 Python 习惯于使用下划线前缀私有变量。但是,在按下 Tab 之前键入 _
强制 IPython 显示所有私有变量。
在 NetworkX 中使用制表完成的示例
在这里,我们将使用制表符补全来找出如何使用 NetworkX 包加载和操作图形。在处理图形时,这个包通常被使用。让我们执行以下命令来导入这个包:
In [1]: import networkx as nx
要找出打开图形的可用选项,我们可以查找以 read
为前缀的可能方法,如下所示:
In [2]: nx.read<TAB>
nx.read_adjlist nx.read_dot nx.read_edgelist [...]
由于 .edges
文件包含一系列边,我们尝试以下命令(假设我们在 fbdata
文件夹中):
In [3]: g = nx.read_edgelist('0.edges')
现在图形 g
看起来已经加载,我们可以探索这个新对象提供的方法,如下所示:
In [4]: g.<TAB>
g.add_cycle [...] g.edges [...] g.nodes
In [5]: len(g.nodes()), len(g.edges())
Out[5]: (333, 2519)
第 0 个 ego 用户似乎有 333 个朋友,这些朋友之间有 2519 个连接。
让我们更深入地探索这个图形的结构。在这个图形中,任意两个用户之间有多好的连接?小世界图的理论预测,在社交图中,任意两个人之间大约相隔六个链接。在这里,我们可以计算图形的半径和直径,即任意两个节点之间的最小和最大路径长度。制表符补全显示 NetworkX 包中有一个 radius
方法。因此,我们尝试以下命令:
In [6]: nx.radius(g)
[...]
NetworkXError: Graph not connected: infinite path length
我们的图形似乎是不连通的,因为半径和直径没有明确定义。为了解决这个问题,我们可以取图形的一个连通分量,如下所示:
In [7]: nx.connected<TAB>
nx.connected nx.connected_component_subgraphs [...]
第二个命题看起来是一个不错的选择(因此,在创建包时选择好的名称的重要性就显现出来了!),如下所示:
In [8]: sg = nx.connected_component_subgraphs(g)
In [9]: [len(s) for s in sg]
Out[9]: [324, 3, 2, 2, 2]
In [10]: sg = sg[0]
In [11]: nx.radius(sg), nx.diameter(sg)
Out[11]: (6, 11)
有五个连通分量;我们取最大的一个并计算它的半径和直径。因此,任意两个朋友之间通过不到 11 个级别连接,而且有一个朋友距离任何其他朋友不到六个链接。
使用自定义类的制表符补全
如果我们定义自己的类,我们可以定制它们的实例在 IPython 制表符补全中的工作方式。我们所要做的就是重写 __dir__
方法,返回属性列表,如下所示:
In [12]: class MyClass(object):def __dir__(self):return ['attr1', 'attr2']
In [13]: obj = MyClass()
In [14]: obj.<TAB>
obj.attr1 obj.attr2
这个特性在某些情况下可能很有用,其中实例的有趣属性列表是动态定义的。
源代码内省
IPython 还可以显示有关变量内部的信息,特别是当它在文件中定义时的源代码信息。首先,在变量名之前或之后键入 ?
会打印有用的信息。键入 ??
会给出更详细的信息,特别是对象的源代码,如果它是在文件中定义的函数。
此外,几个魔术命令显示有关变量的特定信息,例如函数的源代码(%psource
)或定义它的文件的源代码(%pfile
),文档字符串(%pdoc
),或定义头部(%pdef
)。
%pfile
魔术命令还接受一个 Python 文件名,此时它会打印带有语法高亮的文件内容。有了这个功能,IPython 就可以作为带有语法高亮的代码查看器。
使用交互式调试器
对我们大多数人来说,调试是编程工作中一个重要的部分。IPython 使得调试脚本或整个应用程序变得极其方便。它提供了交互式访问增强版的 Python 调试器。
首先,当我们遇到异常时,可以使用 %debug
魔法命令在异常抛出的位置启动 IPython 调试器。如果我们激活 %pdb
魔法命令,调试器将在下一个异常发生时自动启动。我们还可以通过 ipython --pdb
启动 IPython,以获得相同的行为。最后,我们可以使用 %run -d
命令在调试器的控制下运行整个脚本。此命令会在第一行设置断点执行指定的脚本,以便我们可以精确控制脚本的执行流程。我们还可以显式指定第一断点的位置;输入 %run -d -b29 script.py
会在 script.py
的第 29 行暂停程序执行。我们需要先输入 c
来开始执行脚本。
当调试器启动时,提示符变为 ipdb>
。程序执行将在代码中的某个特定位置暂停。我们可以使用 w
命令来显示调试器暂停时的代码行及堆栈跟踪位置。在这个时候,我们可以访问所有本地变量,并可以精确控制如何继续执行。在调试器中,提供了若干命令用于在堆栈跟踪中导航:
-
u
/d
用于在调用堆栈中向 上/下 移动 -
s
用于 进入 下一条语句 -
n
用于继续执行,直到当前函数中的 下一行 -
r
用于继续执行,直到当前函数 返回 -
c
用于 继续 执行,直到下一个断点或异常
其他有用的命令包括:
-
p
用于评估并 打印 任何表达式 -
a
用于获取当前函数的 参数 -
!
前缀用于在调试器中执行任何 Python 命令
完整的命令列表可以在 Python 中的 pdb
模块文档中找到。
交互式基准测试和性能分析
唐纳德·克努斯(Donald Knuth)曾说:
“过早的优化是万恶之源。”
这意味着优化只应在绝对必要的情况下进行,并且在代码经过充分分析后,您需要明确知道哪些部分的代码需要优化。IPython 使得这一基准测试和性能分析过程变得简便。
控制命令的执行时间
首先,%timeit
魔法函数使用 Python 的 timeit
模块来估算任何 Python 语句的执行时间。如果我们定义了一个函数 fun(x)
,%timeit fun(x)
将多次执行该命令并返回执行时间的平均值。调用次数会自动确定;每个循环有 r
次执行,每次执行 n
次。这些数字可以通过 %timeit
的 -r
和 -n
选项进行指定。此外,我们还可以使用 %run -t
命令轻松估算脚本的执行时间。
在以下示例中,我们计算sg
的中心,即偏心率等于半径的节点集合(即在ego
圈子中与所有其他朋友最为连接的朋友),并估算其所需时间:
In [19]: %timeit nx.center(sg)
1 loops, best of 3: 377 ms per loop
In [20]: nx.center(sg)
Out[20]: [u'51', u'190', u'83', u'307', u'175', u'237', u'277', u'124']
我们可以在前面的示例中看到,Python 和 NetworkX 计算sg
的中心花费了 377 毫秒。center
函数被调用了三次(输出中的best of 3
为19
),并且自动选择了最短的执行时间(因为第一次执行可能会更长,举例来说,可能因为一些 Python 导入的原因)。
脚本分析
为了获取有关程序执行时间的更详细信息,我们可以在分析器的控制下执行它,像 Python 的profile
模块原生提供的那样。分析是一个复杂的话题,我们这里只展示一个基本的使用示例。有关profile
模块的更多细节,可以参考官方的 Python 文档。
要在分析器的控制下运行脚本,我们可以通过 IPython 执行%run -p
或等效的%prun
魔术命令。
在这里,我们将编写一个小的 Python 脚本,计算图的中心,而不使用内置的 NetworkX center
函数。让我们创建一个名为center.py
的脚本,代码如下:
import networkx as nx
g = nx.read_edgelist('0.edges')
sg = nx.connected_component_subgraphs(g)[0]
center = [node for node in sg.nodes() if nx.eccentricity(sg, node) == nx.radius(sg)]
print(center)
现在,让我们运行它并使用以下命令估算所需的时间:
In [21]: %run -t center.py
[u'51', u'190', u'83', u'307', u'175', u'237', u'277', u'124']
IPython CPU timings (estimated):User : 128.36 s.
这个脚本执行了超过两分钟;这看起来特别糟糕!我们可以使用命令%run –p center.py
运行分析器,找出到底是什么导致了这么长的时间。
分析器输出了关于每个直接或间接使用的 Python 函数的调用详情。例如,cumtime
列打印了每个函数内累计的时间。从前面的示例来看,eccentricity
和radius
是主要的瓶颈,因为它们分别被调用了 648 次和 324 次!仔细查看代码后发现,我们确实做了一些愚蠢的事情;也就是说,我们在循环内重复调用这两个函数。通过缓存这些函数的输出,我们可以显著提高这个脚本的性能。让我们修改center2.py
中的脚本:
import networkx as nx
g = nx.read_edgelist('data/facebook/0.edges')
sg = nx.connected_component_subgraphs(g)[0]
# we compute the eccentricity once, for all nodes
ecc = nx.eccentricity(sg)
# we compute the radius once
r = nx.radius(sg)
center = [node for node in sg.nodes() if ecc[node] == r]
print(center)
在这里,我们在循环之前通过一次调用eccentricity
计算所有节点的偏心率,并且只计算一次图的半径。让我们通过执行以下命令来检查改进后脚本的性能:
In [23]: %run -t center2.py
[u'51', u'190', u'83', u'307', u'175', u'237', u'277', u'124']
IPython CPU timings (estimated):User : 0.88 s.
通过这个修改,我们的计算时间缩短到不到一秒,而不是两分钟!当然,即使这个示例特别简单,这种错误也可能是任何程序员在编写长程序时犯的。然后,仅通过阅读代码,可能不容易发现这个瓶颈。找到这些热点的最佳方式是使用分析器,而 IPython 使这个任务变得特别简单。
提示
逐行分析器的使用
为了更精细的性能分析,我们可以使用逐行分析器。该工具分析程序员选择的一组函数中每一行所花费的时间。在 Python 中,line_profiler
包正是完成这项工作的工具。要分析的函数通过 @profile
装饰器标注。其使用方法比 IPython 分析器稍微复杂一些,我们邀请有兴趣的读者访问该包的官方网站 packages.python.org/line_profiler/
。我们将在 第六章,定制 IPython 中进一步提及它。
使用 IPython 笔记本
IPython 笔记本在 Python 社区中越来越常用,尤其是在科学研究和教育领域。它为 IPython 提供了一个强大的 HTML 用户界面,并且能够将整个交互式会话保存在 JSON 格式的笔记本文件中。后者的功能为交互式计算带来了可重现性,这在科学研究中尤其重要。笔记本可以在浏览器中运行,除了 Python 代码外,还可以包含文本(如 Markdown 等标记语言),以及图像、视频或富媒体内容。笔记本可以转换为其他格式,如 Python 脚本、HTML 或 PDF。许多课程、博客文章和书籍都是使用笔记本编写的。
提示
IPython Qt 控制台
还有另一个类似于笔记本的丰富的 IPython 前端,它基于 Qt 而非 HTML。你可以在 ipython.org/ipython-doc/stable/interactive/qtconsole.html
查找更多信息。
安装
IPython 笔记本服务器需要一些依赖项。如果你使用完整的发行版,或者从二进制包安装了 IPython,那么应该无需额外操作。如果你是手动安装的 IPython,你需要安装 PyZMQ 和 Tornado。PyZMQ 是 ZMQ 套接字库的 Python 封装,而 Tornado 是一个 Python 库,用于实现笔记本使用的 HTTP 服务器。你可以通过 easy_install
、pip
或源代码安装这些包。
笔记本仪表盘
要检查一切是否正确安装,在终端中输入 ipython notebook
。这将在 8888 端口(默认)启动一个本地 web 服务器。然后在浏览器中访问 http://127.0.0.1:8888/
,检查是否可以看到以下页面:
笔记本仪表盘
提示
笔记本的浏览器兼容性
IPython 笔记本与以下浏览器兼容:Chrome、Safari、Firefox 6 及更高版本,以及 Internet Explorer 10 及更高版本。这些浏览器支持笔记本使用的 WebSocket 协议。
前面截图中的页面是笔记本仪表板;它列出了我们从中启动ipython notebook
的目录中的所有笔记本。IPython 笔记本文件的扩展名为.ipynb
,它是一个包含 JSON 结构化数据的文本文件。
本文件包含一个交互会话的输入和输出,以及 IPython 内部使用的一些元数据。
小贴士
在线查看笔记本
IPython 笔记本可以在 IPython 笔记本查看器上在线查看和共享,网址为nbviewer.ipython.org/
。
让我们开始创建一个新的笔记本。点击页面右上角的New Notebook按钮。
处理单元格
我们现在处于一个笔记本中。用户界面简洁,专注于基本功能。顶部的菜单和工具栏提供了所有命令的访问。它们下方的主要区域默认显示一个空的输入单元格。Python 代码可以输入到这个输入单元格中。输入单元格的一个重要特性是按下Enter键不会执行该单元格,而是插入一个新行。因此,将代码写入单元格更接近于标准文本编辑器提供的功能,而不是经典的IPython控制台。
开始输入如下截图中显示的命令,并注意 Tab 补全的实现方式:
笔记本中的 Tab 补全
可以通过两种方式执行输入单元格。按下Shift + Enter,将执行单元格内的所有代码在当前 IPython 交互命名空间中。然后,输出会显示在输入单元格正下方的输出区域,并在其下创建一个新的输入单元格。按下Ctrl + Enter,不会创建新的输入单元格,只显示输出。通常,当我们只需评估一些 Python 表达式并且不想在笔记本中保存单元格的输出时,会使用后者进行快速原地实验(尽管我们随时可以稍后删除单元格)。
最后,一个笔记本包含一系列线性的输入和输出单元格,代表一个连贯且可重现的交互式会话。通常,单个单元格包含一组指令,执行一些需要几个连续命令的高级操作。
该界面提供了编辑、删除、拆分和合并单元格的命令。可以通过菜单、工具栏或键盘快捷键访问这些命令。我们可以通过按下Ctrl + M,然后H来显示所有键盘快捷键的列表。大多数笔记本命令通过以Ctrl + M开头的一系列按键操作来执行,后面跟着单个按键。
单元格魔法
Cell magics 是应用于整个单元格的特殊魔法命令,而不是单独的一行。它们以 %%
而不是 %
为前缀,可以在 IPython 控制台或 IPython 笔记本中使用。通过命令 %lsmagic
可以获得所有 cell magics 的列表。两个有用的 cell magics 包括 %%!
,用于从 IPython 执行多个系统 shell 命令,以及 %%file
,用于创建文本文件,下面的示例展示了这一点:
In [1]: %%file test.txtHello World!
Writing test.txt
In [2]: %%!more test.txt
Out[2]: ['Hello World!']
管理笔记本
我们可以随时通过点击 Save 按钮或按 Ctrl + S 或 Ctrl + M,然后 S 来保存正在工作的笔记本。默认情况下,笔记本文件名为 Untitled0
,但我们可以通过 File 菜单中的 Rename
命令重命名它。
我们可以通过将 Python 文件从系统资源管理器拖到 IPython 控制台,来根据现有的 Python 脚本创建一个新的笔记本。这将创建一个与我们的脚本同名但扩展名为 .ipynb
的新笔记本。笔记本可以作为 Python 脚本或 .ipynb
文件进行下载。
多媒体和富文本编辑
一个非常有用的功能是可以通过流行的标记文本格式 Markdown 在单元格中插入富文本(详细描述见daringfireball.net/projects/markdown/syntax
)。编辑功能如粗体、斜体、标题和项目符号可以通过简单的语法插入。为了做到这一点,我们需要使用 Cell >
Markdown 命令将单元格转换为 Markdown 单元格。
然后,我们可以使用 Markdown 语法输入文本。如果按下 Shift + Enter,文本将自动格式化,且可以通过双击进行编辑。以下截图展示了 Markdown 代码和相应的格式化文本:
笔记本中的 Markdown 输入和输出
图表绘制
让我们通过社交网络示例来说明笔记本的绘图功能。我们将绘制图表 sg
。首先,我们需要通过命令 ipython notebook --pylab inline
启动笔记本。这个选项将在第四章中详细介绍,图形和图形界面。它允许在笔记本中插入图形,得益于 Matplotlib 库。NetworkX 提供了几个基于 Matplotlib 的命令来绘制图表。在以下示例中,我们使用 draw_networkx
函数绘制图表 sg
,并通过多个参数来提高图表的可读性(完整的选项列表可以在 NetworkX 文档网站上找到):
在笔记本中绘制图表
总结
我们现在对 IPython 提供的功能有了一个全面的了解,这些功能简化并扩展了我们在日常编程工作中与 IPython 的交互方式。从强大的 Python 历史记录到必不可少的动态 introspection 特性,决定是使用 IPython 还是标准的 Python 控制台进行交互式编程,简直是显而易见的选择。而且,notebook 提供了一种现代化的方式来使用 IPython,适用于多种用途,比如简单地记录交互式会话,创建编程课程、演示文稿,甚至是一本书!
然而,IPython 提供的功能远不止这些。它真正发挥作用的时刻是与外部包结合使用时,这些包提供了数值计算和可视化功能:NumPy、SciPy、Matplotlib 等。这些包完全可以在没有 IPython 的情况下使用。然而,完全使用 IPython 是有意义的,因为它可以让你在 Python 编程语言中进行交互式的数值计算和可视化。综合来看,这些工具正逐渐成为开源科学计算的首选平台,甚至与广泛使用的商业解决方案进行竞争。在下一章,我们将介绍该平台的数值计算能力。
第三章:使用 IPython 进行数值计算
虽然 IPython 强大的 shell 和扩展控制台可以被任何 Python 程序员充分利用,但这个软件包最初是为 科学家设计的工具。它的确旨在为科学计算提供一种方便的方式,进行 互动式科学计算。
IPython 本身并不提供科学计算功能 本身,而是提供了一个与强大外部库(如 NumPy、SciPy、Pandas、Matplotlib 等)互动的接口。这些工具共同提供了一个科学计算框架,能够与科学界广泛使用的商业工具如 Matlab 或 Mathematica 竞争。
NumPy 提供了一个支持优化向量操作的多维数组对象。SciPy 提供了多种科学算法(如信号处理、优化等),这些算法基于 NumPy。Pandas 提供了便于处理来自现实数据集的表格数据的结构。Matplotlib 允许轻松绘制图形,从而交互式地可视化各种数据,并生成出版质量的图表。IPython 提供了一个合适的交互框架,使得使用这些工具更加高效。
在本节中,我们将:
-
探索 NumPy 和 Pandas 提供的互动计算可能性
-
理解为什么多维数组适合进行高性能计算
-
了解数组如何在实际应用中使用
-
查找一些包含更高级示例和应用的参考资料
向量计算简介
在本节中,我们将介绍向量化计算的概念。这是一个至关重要的概念,因为它是使用像 Python 这样的高级语言实现高性能的最简便方法。
一个使用 Python 循环进行计算的示例
当今的科学和工程都围绕数字展开。大多数数据处理和数值模拟不过是对大量数值数据进行一系列基本操作,计算机在这方面非常擅长。然而,数据必须以某种合理的方式进行结构化。数值数据的一般结构是 向量 和 矩阵,更一般地说是 多维数组。
在我们更详细地解释什么是数值数组之前,让我们先看一个激励引入这些对象的示例。假设我们已经获取了大量位置的地理数据,包括它们的坐标(纬度和经度),并且我们需要找出最靠近给定兴趣点的位置。例如,我们可能想找出离智能手机用户 GPS 位置最近的餐馆。
如果这些位置存储在 Python 的元组列表中,我们可以写出如下代码:
def closest(position, positions):x0, y0 = positiondbest, ibest = None, Nonefor i, (x, y) in enumerate(positions):# squared Euclidean distance from every position to the position of interestd = (x - x0) ** 2 + (y - y0) ** 2if dbest is None or d < dbest:dbest, ibest = d, ireturn ibest
在这里,我们遍历所有位置。变量i
保持当前位置信息的索引,而(x, y)
则包含该位置的坐标。我们关注的位置是position=(x0, y0)
。在第一次迭代中,当前的位置被记录为最优位置,在接下来的迭代中,只有当前的位置比最优位置更接近时,才会更新最优位置。循环结束时,最接近位置的索引是ibest
,对应的位置是positions[ibest]
,而从目标位置到最接近位置的平方距离存储在dbest
中。为了计算距离,我们使用的是平方欧几里得距离公式,D = (x - x0)² + (y - y0)²。
这是一个标准且基础的算法。让我们在一个大型数据集上评估它的性能。我们首先生成一个包含 1000 万个随机位置的列表,如下所示:
In [1]: import random
In [2]: positions = [(random.random(), random.random()) for _ in xrange(10000000)]
我们定义了一个名为positions
的列表,其中包含坐标对,每个数字都是 0 和 1 之间的随机数。现在,使用以下命令设置一些基准:
In [3]: %timeit closest((.5, .5), positions)
1 loops, best of 3: 16.4 s per loop.
这个算法处理 1000 万个位置用了 16.4 秒。我们来看看这是否接近 CPU 的理论最大性能。该代码是在一颗 2 GHz 单核处理器上执行的。理论上,它每个周期可以处理四个浮点操作,相当于每秒 80 亿次操作。在我们的算法中,每次迭代涉及五个数学操作和一次比较,总共 50 百万次浮点操作(仅计算数学操作)。理论最大性能应该是 6.25 毫秒。这意味着我们的算法表现比理论最大性能差了大约 2600 倍!
当然,这只是一个非常粗略的估算,理论上的最大性能通常是远未达到的,但 2600 倍的差异显得特别严重。我们能做得更好吗?我们将在下一节中找出答案。
数组是什么
在前面的例子中,同样的计算(计算与一个固定点的距离)在大量数字上进行。NumPy 提供了一种新的类型,完美适应这种情况:多维数组。那么,数组是什么?
数组是一个组织成多个维度的数据块。一维数组是一个向量,它是一个元素(通常是数字)有序的序列,使用一个整数进行索引。二维数组是一个矩阵,包含通过一对整数索引的元素,即行索引和列索引。更一般地说,n维数组是一个由相同数据类型的元素组成的集合,通过一个n个整数的元组进行索引。
多维 NumPy 数组的示意图
数组中的所有元素必须具有相同的类型:这称为数据类型(dtype
)。NumPy 中有多种可能的类型:布尔值、带符号/无符号整数、单精度/双精度浮点数、复数、字符串等。还可以定义自定义数据类型。
数组中的元素在内存中是连续存储的。例如,一个大小为 10 的向量中的元素占有 10 个连续的内存地址。当数组的维度为两维或更多时,元素的排序方式有不止一种选择。对于矩阵,元素可以按照行主序(也称为C-顺序)或列主序(也称为Fortran-顺序)存储,具体取决于在遍历数组中的所有元素时,横向或纵向索引中哪个索引的变化最快。这个概念在三维或更多维度中得以推广。NumPy 中的默认顺序是 C-顺序,但在创建数组时可以通过 order
关键字参数更改这一顺序。
多维数组中行主序和列主序的区别
这个概念可以扩展到任意维度。步幅定义了在每个维度中随着遍历所有元素所需的步数。NumPy 自动处理所有这些底层细节,并提供了方便的方式来创建、操作和计算这些数组。大多数时候,我们不需要关心这些细节,可以把我们的变量当作多维数组来思考。然而,了解内部工作原理可以帮助我们修复某些错误,并优化涉及非常大数组的代码部分。
数组相比于原生 Python 类型的优势在于,可以对数组执行非常高效的计算,而不是依赖于 Python 循环。区别在于,循环是在 NumPy 中以 C 语言实现的,而不是用 Python,这样就没有了在循环中解释的开销。实际上,由于 Python 是一种解释型、动态类型语言,每次迭代都会涉及 Python 执行的多种低级操作(如类型检查等)。这些操作通常消耗的时间很小,但当它们重复执行数百万次时,它们会对性能产生不良影响。
此外,现代 CPU 实现了矢量化指令(SSE、AVX、XOP 等),这些指令使用大寄存器(128 位或 256 位),可以包含多个单精度或双精度浮点数。如果 NumPy 使用适当的选项进行编译,数组计算可以利用这些矢量化 CPU 指令,从而比原来快两倍或四倍以上。
这些是使用 NumPy 进行向量化计算可能比 Python 循环更高效的主要原因之一。这与单指令,多数据(SIMD)计算范式有关,因为在 NumPy 的数组操作中,多个元素会同时执行相同的计算。我们将通过之前的示例来演示这一点。
使用数组重新实现示例
让我们使用数组重新编写我们的示例。首先,我们需要导入 NumPy。在 IPython 中,我们可以使用%pylab
魔法命令(或者通过ipython --pylab
启动 IPython),该命令会在交互式命名空间中加载 NumPy 和 Matplotlib(NumPy 为np
,Matplotlib.pyplot 为plt
)。这是在 IPython 交互式会话中使用 NumPy 的最便捷方法。另一种方法是通过import numpy
导入 NumPy(或者对于懒人来说使用import numpy as np
)或使用from numpy import *
。前者语法更适合脚本使用,而后者可以在交互式会话中使用。在此,以及所有后续章节中,我们将始终假定已激活pylab
模式,如下所示:
In [1]: %pylab
首先,我们需要生成一些随机数据。NumPy 提供了一种高效的方法来完成这一点,具体命令如下:
In [2]: positions = rand(10000000,2)
positions
数组是一个二维数组,包含 1000 万行和两列,列中是介于零和一之间的独立均匀随机数。我们注意到,在数组创建过程中,我们没有使用for
循环。每当可以使用 NumPy 操作时,都应该避免使用循环。让我们看看这个对象的一些属性:
In [3]: type(positions)
Out[3]: numpy.ndarray
In [4]: positions.ndim, positions.shape
Out[4]: 2, (10000000, 2)
shape
属性包含数组形状,以整数元组的形式表示。数组的其他重要属性包括:
-
ndim
:维度的数量,也就是len(positions.shape)
-
size
:元素的总数(即positions.shape
中所有值的乘积) -
itemsize
:每个元素的字节大小(int32
数据类型为四字节,float64
为八字节,以此类推)
现在,我们将分两步计算每个位置到我们感兴趣位置的平方距离。我们首先输入以下命令:
In [5]: x, y = positions[:,0], positions[:,1]
在这里,x
和y
包含所有位置的* x 和 y 坐标。实际上,变量positions[:,0]
指的是positions
的第一列(在 Python 中,索引是从零开始的)。这是 Python/NumPy 中特殊的索引语法。方括号[]
用于访问 Python 容器对象中的元素。方括号内的:,0
表示所有索引对,其中第一个元素可以是任何值(冒号:
),第二个元素等于零。由于在 NumPy 中,第一维始终指的是行,第二维指的是列,因此我们这里准确地指的是第一列。同理,positions[:,1]
指的是第二列,其中包含所有位置的 y *坐标。变量x
和y
是二维向量。我们可以通过以下命令计算distances
变量:
In [6]: distances = (x - .5) ** 2 + (y - .5) ** 2
在这里,我们计算了从感兴趣位置(0.5, 0.5)到所有位置的距离向量。事实上,x - .5
表达式将 0.5 从所有位置的第一列元素中减去。原因是x
是一个包含 1000 万元素的一维向量,而0.5
只是一个浮点数。NumPy 的约定遵循向量微积分中的数学约定,也就是说,减法会对数组中的所有元素进行操作。
同样,(x - .5) ** 2
计算了括号内向量中所有元素的平方。最后,+
运算符对两个 1000 万长的向量执行逐点操作。
我们看到,NumPy 允许通过非常简单的语法执行向量运算。使用数组进行计算是一种非常特定的编程方式,需要一些时间才能掌握。它与大多数语言中标准的顺序编程方式有很大的不同,但在 Python 中,它效率更高,正如我们在以下命令中所看到的:
In [7]: %timeit exec(In[6])
1 loops, best of 3: 508 ms per loop
当我们再次使用%timeit
魔法函数计算distances
变量时,我们发现计算速度比纯 Python 版本快得多。即使我们添加了最小元素的计算,这在 NumPy 中也很容易实现,我们仍然发现总时间是比纯 Python 版本快 30 倍,如以下命令所示:
In [8]: %timeit ibest = distances.argmin()
1 loops, best of 3: 20 ms per loop
总结来说,多维数组的raison d'être是尽可能避免在大数据量的数值计算中使用 Python 循环。将计算向量化有时可能比较困难,但从性能提升的角度来看,始终是值得的。
创建和加载数组
在本节中,我们将看到如何从头开始或从现有数据中创建和加载数组。这是用 Python 分析数据的第一步。
创建数组
有多种方法可以创建数组。在本节中,我们将回顾它们。
从头开始,逐个元素
首先,我们可以通过手动指定其系数来创建一个数组。这是创建数组的最直接方式,但在实际中并不常用。NumPy 的array
函数接受一个元素列表并返回一个对应的 NumPy 数组,如下所示(需要激活 IPython 的pylab
模式):
In [1]: x = array([1, 2, 3])
In [2]: x.shape
Out[2]: (3,)
In [3]: x.dtype
Out[3]: dtype('int32')
在这里,我们创建了一个一维数组(即向量),包含三个 32 位整数(在 32 位系统中,整数的默认类型)。创建的数组的数据类型是根据array
中提供的元素自动推断的。我们可以通过dtype
关键字参数强制指定数据类型,如下所示:
In [4]: x = array([1, 2, 3], dtype=float64)
In [5]: x.dtype
Out[5]: dtype('float64')
要创建二维数组(矩阵),我们需要提供一个嵌套的列表,每个内层列表包含一行,如下所示:
In [6]: array([[1, 2, 3], [4, 5, 6]])
Out[6]:
array([[1, 2, 3],[4, 5, 6]])
要创建一个 n 维数组,我们需要提供一个具有 n 层递归的嵌套列表。举例来说,我们可以使用两个嵌套的 Python 列表推导式来创建一个乘法表:
def mul1(n):return array([[(i + 1) * (j + 1) for i in xrange(n)] for j in xrange(n)])
该函数将表格大小作为参数,并从行列表中创建乘法表数组,如以下示例所示:
In [7]: mul1(4)
Out[7]:
array([[ 1, 2, 3, 4],[ 2, 4, 6, 8],[ 3, 6, 9, 12],[ 4, 8, 12, 16]])
In [8]: %timeit mul1(100)
100 loops, best of 3: 5.14 ms per loop
稍后我们将看到更高效的创建此乘法表的方法。
从头开始,使用预定义模板
手动指定每个系数来创建数组通常不太实用。可以使用 NumPy 中定义的多种便捷函数来创建具有所需形状的典型数组。例如,要创建一个填充 100 个零的向量,可以使用以下命令:
In [1]: x = zeros(100)
要创建一个 2D 矩阵,我们需要提供一个包含所需形状的元组作为参数,因此以下命令中使用了双括号:
In [2]: x = zeros((10, 10))
默认的数据类型是 float64
。类似地,ones
函数创建一个填充了 1
的数组。函数 identity
、eye
和 diag
用于创建对角矩阵。
还有一些便捷的函数可以创建具有规律间隔数字的向量,如以下示例所示:
In [5]: arange(2, 10, 2)
Out[5]:
array([2, 4, 6, 8])
在这里,我们创建一个从 2
到 10
之间按步长为 2 线性间隔的数字向量。请注意,第一个数字是 包括 在内的(第一个 2
),但序列中的最后一个数字(10
)是 排除 在外的。这是 Python 中的一个通用约定,实际上比它看起来的更直观。另一个相关的函数是 linspace
,它与 arange
类似,只不过输出向量的 大小,而不是步长,是作为第三个参数提供的。这一次,序列的第一个和最后一个元素都会被包括在内。
提示
函数签名
在 IPython 中,可以通过 ?
或 help()
获取函数签名,包括参数顺序和关键字参数列表。此外,在 Qt 控制台和笔记本中,输入 linspace(
会自动弹出 linspace(
函数的签名提示框。然后,可以通过按 Tab 键展开该提示框。
从随机值生成
NumPy 提供了多种随机采样方法,用于生成具有不同概率分布的独立随机值数组。例如,要创建一个 2 x 5 的数组,里面的随机浮点数均匀分布在 0
和 1
之间,可以使用 rand
函数,如下所示:
In [1]: rand(2, 5)
Out[1]:
array([[ 0.925, 0.849, 0.858, 0.269, 0.644],[ 0.796, 0.001, 0.183, 0.397, 0.788]])
注意在 rand
中指定数组形状时没有使用双括号(NumPy 的一个特性)。
提示
IPython 中的数字格式化
可以使用 %precision
魔法命令指定在 IPython 中显示数字的方式。例如,要将浮点数显示为精确到三位小数,可以在 IPython 中输入 %precision 3
。实际上,可以提供任何格式化字符串,具体请参见文档 %precision?
。
其他函数包括randn
(从高斯分布中采样的随机值)、randint
(随机整数)、exponential
(指数分布)等。相关函数包括shuffle
和permutation
,它们随机排列现有的数组。
加载数组
数组结构的主要兴趣在于能够从 Python 或外部源加载现有数据。NumPy 提供了高效便捷的方式来从文本(Python 字符串或文本/CSV 文件)或二进制缓冲区或文件中加载多维数组。此外,Pandas 包在加载表格数据时尤为有用,即包含异构数据类型的表格,而不仅仅是数字。
从原生 Python 对象
数据常常以某种原生 Python 对象的形式存在,我们希望将其转换为 NumPy 数组。标准方法是使用array
函数。当我们通过直接指定值来创建数组时,我们实际上是将 Python 数字列表转换为了数组。
从缓冲区或外部文件
创建数组的另一种常见方式是从内存缓冲区或文件中加载数据,无论是二进制数据还是字符串元素。从一个 Python 缓冲区对象(我们知道它的确切数据类型)中,我们可以使用frombuffer
函数获得一个 NumPy 数组。同样,fromstring
函数接受 ASCII 文本,值由任意分隔符分隔,或者接受任何数据类型的二进制数据,如下例所示:
In [1]: np.fromstring('1 2 5 10', dtype=int, sep=' ')
Out[1]: array([ 1, 2, 5, 10])
fromfile
、loadtxt
和genfromtxt
函数允许从文本文件或二进制文件加载数据,并将其转换为 NumPy 数组。loadtxt
函数是genfromtxt
的简化版本,适用于文件格式简单的情况。fromfile
函数在处理二进制数据时效率很高。例如,要导入 Facebook 数据集的文本文件中的数据,我们可以输入以下命令:
In [1]: cd fbdata
In [2]: loadtxt('0.edges')
Out[2]:
array([[ 236., 186.],...,[ 291., 339.]])
最后,将数组保存到文件中和加载 NumPy 数组一样简单。基本上有两个函数,save
和savetxt
,分别将数组保存为二进制文件和文本文件。相关的,loadz
和savez
函数也非常方便,用于保存字典类型的变量(包括 NumPy 数组)。所有这些函数使用平台无关的文件格式。
使用 Pandas
Pandas 是另一个较新的 Python 包,提供了便捷高效的方式来加载和操作来自异构源的数据集。它特别适用于处理表格数据集,而不是纯粹的数值数据(矩阵或数字数组)。它能够处理缺失值和数据对齐问题(例如,时间序列)。加载的数据集可以与 NumPy 一起用于高效的数值计算。简而言之,Pandas 提供了对表格数据的高层次访问,而 NumPy 则提供了对原始同质多维数组的低层次访问。
提示
NumPy 的未来
NumPy 的创始人 Travis Oliphant 目前正在开发它的继任者 Blaze。这个项目将把 NumPy、Pandas、SciPy、Numba、Theano 等当前提供的许多功能统一到一个框架中。
这里是如何使用 Pandas 加载数据集的一个示例。我们将下载并分析一个关于世界各地大量城市及其人口的数据集。这个数据集由MaxMind创建,并可以免费从www.maxmind.com
获得。
提示
在线公共数据集
随着开放数据运动的推进,越来越多的数据变得公开可用。分析有趣的数据是使用本书中描述的工具的好方法,这些工具特别适合执行此类任务。然而,在线找到好的数据集并不总是那么显而易见。以下是一些链接,其中包含指向高质量数据集的指示,这些数据集通常由政府机构、国际组织、大学或研究机构等维护:
-
由 Hilary Mason 维护的研究质量数据集可以在
bitly.com/bundles/hmason/1
找到。 -
由 Google 维护的公共数据可在
www.google.com/publicdata/
上获取。 -
数据目录可以在
datacatalogs.org/dataset
上找到。
我们首先下载 ZIP 文件并将其解压到一个文件夹中,如以下命令所示(ZIP 文件约为 40 MB,因此下载可能需要一些时间):
In [1]: import urllib2, zipfile
In [2]: url = 'http://ipython.rossant.net/'
In [3]: filename = 'cities.zip'
In [4]: downloaded = urllib2.urlopen(url + filename)
In [5]: folder = 'data'
In [6]: mkdir $folder
In [7]: with open(filename, 'wb') as f:f.write(downloaded.read())
In [8]: with zipfile.ZipFile(filename) as zip:zip.extractall(folder)
为了方便起见,我们可以使用命令%bookmark citiesdata data
创建对新建文件夹的别名。现在,我们将加载已提取的 CSV 文件,使用 Pandas 的read_csv
函数可以打开任何 CSV 文件,如以下命令所示:
In [9]: import pandas as pd
In [10]: filename = 'data/worldcitiespop.txt'
In [11]: data = pd.read_csv(filename)
现在,让我们来探索新创建的dat
对象:
In [12]: type(data)
Out[12]: pandas.core.frame.DataFrame
data
对象是一个DataFrame
对象,这是 Pandas 的一种类型,包含一个二维的标签化数据结构,其列可能具有不同的数据类型(如 Excel 电子表格)。像 NumPy 数组一样,shape
属性返回表格的形状。但与 NumPy 不同,DataFrame
对象具有更丰富的结构,特别是keys
方法返回不同列的名称,如以下命令所示:
In [13]: data.shape, data.keys()
Out[13]: ((3173958, 7),Index([Country, City, AccentCity, Region, Population, Latitude, Longitude], dtype=object))
我们可以看到data
有超过三百万行,并且包括国家、城市、人口和每个城市的地理坐标在内的七个列。head
和tail
方法分别允许我们快速查看表格的开头和结尾。请注意,当在 IPython 笔记本中使用 Pandas 时,显示的数据可以格式化为 HTML 表格,方便阅读,如以下示例所示:
In [14]: data.tail()
以下是示例表格:
在 IPython 笔记本中显示 Pandas 表格
我们可以看到一些城市的NaN(不是数字)值表示人口数据。原因是数据集中并不是所有城市都有可用的人口信息,Pandas 会透明地处理这些缺失值。
我们将在接下来的章节中看到,如何使用这些数据进行操作和计算,从而获取有用的信息。
操作数组
一旦 NumPy 数组创建或加载完成,我们基本上可以做三件事:
-
选择
-
操作
-
计算
选择
选择指的是在数组中访问一个或多个元素。可以使用 NumPy 或 Pandas 来完成。
使用 Pandas
让我们继续使用之前用 Pandas 打开的示例数据。DataFrame
的 data
对象的每一列都可以通过其名称进行访问。在 IPython 中,按下 Tab 键可以自动补全数据的不同列。以下示例中,我们获取所有城市的名称(AccentCity
是城市的完整名称,包含大写字母和重音符号):
In [15]: data.AccentCity
Out[15]:
0 Aixas
1 Aixirivali
...
3173956 Zuzumba
3173957 Zvishavane
Name: AccentCity, Length: 3173958
这一列是 Series
类的一个实例。我们可以使用索引访问某些行。以下示例中,我们获取第 30,001 个城市的名称(记住索引是从零开始的):
In [16]: data.AccentCity[30000]
Out[16]: 'Howasiyan'
因此,我们可以通过索引访问一个元素。但我们如何通过城市名称来获取城市数据呢?例如,我们想获取纽约的人口和 GPS 坐标。一个方法是循环遍历所有城市并检查它们的名称,但由于 Python 对于百万级元素的循环效率很低,这种方法非常慢。Pandas 和 NumPy 提供了更优雅和高效的方式——布尔索引。
通常在同一行代码上会执行两个步骤。首先,我们创建一个包含布尔值的数组,表示每个元素是否满足某个条件(此处为城市名称是否为 New York
)。然后,我们将这个布尔数组作为索引传递给原始数组。结果是原数组的一个子部分,其中仅包含对应 True
的元素,如以下示例所示:
In [17]: data[data.AccentCity=='New York']
Out[17]:Country City AccentCity Region Population Latitude Longitude
998166 gb new york New York H7 NaN 53.083333 -0.150000
...
2990572 us new york New York NY 8107916 40.714167 -74.006389
相同的语法在 NumPy 和 Pandas 中都适用。在这里,我们找到了十几个名为 New York
的城市,但只有一个位于纽约州。要使用 Pandas 访问单个元素,我们可以使用 .ix
属性(ix
代表索引),如下所示的命令:
In [18]: ny = 2990572
In [19]: data.ix[ny]
Out[19]:
Country us
City new york
AccentCity New York
Region NY
Population 8107916
Latitude 40.71417
Longitude -74.00639
Name: 2990572
使用 NumPy
现在,让我们把这个系列对象转化为一个纯粹的 NumPy 数组。我们从 Pandas 世界转向 NumPy(记住,Pandas 是基于 NumPy 构建的)。我们主要处理所有城市的人口数,如以下命令所示:
In [20]: population = array(data.Population)
In [21]: population.shape
Out[21]: (3173958,)
population
数组是一个一维向量,包含所有城市的人口数据(如果人口数据不可用,则为 NaN
)。可以使用基本索引在 NumPy 中访问纽约的人口数据,如下所示:
In [22]: population[ny]
Out[22]: 8107916.0
让我们找出哪些城市有实际的人口统计数据。为此,我们将选择人口数组中值不为 NaN 的所有元素。我们可以使用 NumPy 的isnan
函数,如下所示:
In [23]: isnan(population)
Out[23]: array([ True, True, True, ..., True, True, False], dtype=bool)
In [24]: x = population[~_]
In [25]: len(x), len(x) / float(len(population))
Out[25]: (47980, 0.015)
请注意,~_
包含isnan(population)
的负值。我们发现大约有 48,000 个城市,占数据集中所有城市的 1.5%,这些城市有实际的人口统计数据。
更多的索引可能性
更一般地说,索引允许我们获取数组的任何部分。我们在上一节中看到过如何使用布尔条件过滤数组。我们也可以直接指定要保留的索引列表。例如,如果x
是一个一维的 NumPy 数组,x[i:j:k]
表示一个x
的视图,只包含那些索引在i
(包含)和j
(不包含)之间,并且步长为k
的元素。如果省略i
,默认从 0 开始。如果省略j
,默认到该维度数组的长度。负值表示从末尾开始计数。最后,k
的默认值为 1。这种符号在多维情况下也有效;例如,M[i:j,k:l]
创建一个二维数组M
的子矩阵视图。此外,我们还可以使用x[::-1]
来反转x
的顺序。
这些约定中,包含i
而排除j
,在处理数组的连续部分时非常方便。例如,假设x
的大小为2n
,x
的前半部分和后半部分分别为x[:n]
和x[n:]
。此外,x[i:j]
的长度就是j - i
。最后,通常不应在索引中留有+1
或-1
的值。
使用数组视图时需要考虑的一个重要点是,它们指向内存中的同一位置。因此,对一个大数组的视图并不意味着内存分配,并且在视图中更改元素的值也会更改原始数组中相应元素的值,如下例所示:
In [1]: x = rand(5)
In [2]: x
Out[2]: array([ 0.5 , 0.633, 0.158, 0.862, 0.35 ])
In [3]: y = x[::2]
In [4]: y
Out[4]: array([ 0.5 , 0.158, 0.35 ])
In [5]: y[0] = 1
In [6]: x
Out[6]: array([ 1\. , 0.633, 0.158, 0.862, 0.35 ])
在这个例子中,y
包含x
中所有偶数索引的元素(这里是索引 0、2 和 4)。更改y[0]
的值会同时更改y[0]
和x[0]
,因为y[0]
指向的是x
的第一个元素。如果不希望这种行为,可以通过y = x.copy()
或y = array(x)
强制创建一个新数组。在后者的情况下,还可以使用dtype
关键字参数更改x
的数据类型。
最后,选择数组的一部分的另一种方法是传递一个包含显式整数索引值的数组。这种方式称为花式索引。如果x
是一个一维向量,而indices
是另一个包含正整数的一维向量(或列表),那么x[indices]
会返回一个包含x[indices[0]]
、x[indices[1]]
等元素的向量。因此,x[indices]
的长度等于indices
的长度,而不是x
的长度,如下所示:
In [7]: ind = [0, 1, 0, 2]
In [8]: x[ind]
Out[8]: array([ 1\. , 0.633, 1\. , 0.158])
请注意,给定的索引在索引数组中可以重复多次。
操作
数组可以被操作和重塑,这在执行矢量化计算时有时很有用。还可以通过原始数组的相同副本来构造一个新数组。所有函数的完整列表可以在 NumPy 参考指南中找到:docs.scipy.org/doc/numpy/reference/routines.html
。
重塑
首先,reshape
方法允许在保持总元素个数不变的情况下改变数组的形状,如以下示例所示:
In [1]: rand(6)
Out[1]: array([ 0.872, 0.257, 0.083, 0.788, 0.931, 0.232])
In [2]: x.reshape((2, 3))
array([[ 0.872, 0.257, 0.083],[ 0.788, 0.931, 0.232]])
在reshape
的参数中,最多可以在一个维度上使用-1
,以指定其值必须自动推断;例如,使用x.reshape((2, -1))
,而不是x.reshape((2, 3))
。
维度的数量也可以通过ravel
(移除数组中的所有多维结构并返回一个扁平化的向量)、squeeze
(移除数组形状中的所有单维条目)和expand_dims
(在数组中插入一个新轴)来改变。
重复和拼接
til
和repeat
函数允许通过在指定轴上连接相同的数组副本,或按任何次数复制每个元素,来创建数组副本,如以下示例所示:
In [1]: x = arange(3)
In [2]: tile(x, (2, 1))
Out[2]:
array([[0, 1, 2],[0, 1, 2]])
In [3]: repeat(x, 2)
Out[3]:
array([0, 0, 1, 1, 2, 2])
在这里,我们首先创建一个包含两个相同x
副本的垂直堆叠数组,然后创建一个新数组,其中x
的每个元素重复三次。repeat
的第二个参数也可以是一个列表reps
,此时系数x[i]
会重复reps[i]
次。
例如,让我们使用reshape
和tile
创建一个乘法表。其思路是先定义一个包含从1
到n
之间所有整数的行向量和列向量,将它们拼接在一起并进行乘法运算,注意乘法是逐元素进行的,如以下代码片段所示:
def mul2(n):M = arange(1, n + 1).reshape((-1, 1))M = tile(M, (1, n))N = arange(1, n + 1).reshape((1, -1))N = tile(N, (n, 1))return M * N
让我们使用以下命令来计时执行此函数:
In [1]: %timeit mul2(100)
10000 loops, best of 3: 188 us per loop
这个函数比之前的版本mul1
快约 27 倍,后者使用了嵌套的 Python 循环。
同样,我们可以使用hstack
、vstack
、dstack
或concatenate
,分别沿第一个、第二个、第三个或任意维度将多个数组连接成一个数组。
类似地,hsplit
、vsplit
、dsplit
或split
函数允许沿任意维度将数组拆分为多个连续的子数组,如以下示例所示:
In [1]: x = arange(6)
In [2]: split(x, 2)
Out[2]:
[array([0, 1, 2]), array([3, 4, 5])]
In [3]: split(x, [2,5])
Out[3]:
[array([0, 1]), array([2, 3, 4]), array([5])]
split
的第二个参数是一个整数n
,此时数组被拆分成n个相等的数组,或者是一个包含拆分点索引的列表(即每个子数组第一个元素的索引,除了第一个)。
广播
在之前的乘法表示例中,我们不得不重复行和列的相同副本,以便能够乘以形状相同的两个数组(n, n)
。实际上,repeat
步骤是不必要的,因为形状不同的数组在特定条件下仍然可以兼容;这就是所谓的广播。一般规则是:当两个维度相等,或其中一个维度为 1 时,它们是兼容的。例如,两个形状为(1, n)
和(n, 1)
的数组M
和N
可以相乘,因为在第一维度中,M
数组的形状为1
,而在第二维度中,N
数组的形状为1
。维度等于 1 的数组会透明且默默地被扩展,以匹配另一个维度,这个操作不涉及内存复制。
因此,我们可以像下面这样去掉乘法表示例中的tile
操作:
def mul3(n):M = arange(1, n + 1).reshape((-1, 1))N = arange(1, n + 1).reshape((1, -1))return M * N
以下命令被使用:
In [1]: timeit mul3(100)
10000 loops, best of 3: 71.8 us per loop
最后,mul3
比mul2
快大约 2.6 倍,比mul1
快大约 70 倍!原因在于tile
涉及数组复制和内存分配,而mul3
中仅仅进行乘法运算。
排列
有几个函数可以用于排列数组的轴。例如,transpose
函数可以排列数组的维度。描述排列的索引可以通过axes
关键字参数提供。
其他可能有用的转置函数包括fliplr
和flipud
,它们分别用于在左右或上下方向翻转数组,roll
用于沿给定轴执行元素的循环排列,以及rot90
用于逆时针旋转数组 90 度。
计算
创建和操作数组的关键在于对它们进行高效的矢量化计算。四个基本操作在数组之间进行,前提是它们具有兼容的形状。此外,许多数学函数也可以以矢量化形式应用于 NumPy 数组。
如果A
和B
是两个形状兼容的 NumPy 数组,A + B
、A - B
、A x B
和A / B
都是逐元素操作。特别地,当A
和B
是二维矩阵时,A x B
不是矩阵乘积。矩阵乘积是通过dot
函数来提供的,该函数更一般地计算两个数组的点积。
常见的单目运算包括-A
、A ** x
(系数的x
次幂)、abs(A)
(绝对值)、sign(A)
(返回数组中每个元素的符号,值为-1
、0
或1
)、floor(A)
(向下取整)、sqrt(A)
(平方根)、log(A)
(自然对数)、exp(A)
(指数函数)以及其他许多数学函数(例如三角函数、双曲函数、算术函数等)。
NumPy 还提供了计算数组中所有元素或指定维度元素和(sum
)或乘积(prod
)的函数。axis
关键字参数指定进行求和的维度。此函数返回一个比原数组少一个维度的数组。
max
和min
函数返回数组或给定维度中的最大值和最小值。argmin
和argmax
函数返回数组中最小或最大元素的索引。例如,继续使用我们的cities
示例,我们可以对locate
函数执行以下命令:
In [26]: def locate(x, y):# locations is a Ncities x 2 array with the cities positionslocations = data[['Latitude','Longitude']].as_matrix()d = locations - array([x, y])# squared distances from every city to the position (x,y)distances = d[:,0] ** 2 + d[:,1] ** 2# closest in the index of the city achieving the minimum distance to the position (x,y)closest = distances.argmin()# we return the name of that cityreturn data.AccentCity[closest]
In [27]: print(locate(48.861, 2.3358))
Paris
locate
函数接受两个坐标,分别是位置的纬度和经度,并返回离该位置最近的城市名称。argmin
函数返回离指定位置最近城市的索引。
最后,统计函数如mean
、median
、std
和var
计算给定维度或整个数组中元素的均值、中位数、标准差和方差。此外,Pandas 对象的describe
方法提供了几个有用的统计量(包括均值、标准差、50 百分位数或中位数、25 百分位数和 75 百分位数),如下所示:
In [28]: population.describe()
count 47980.000000
mean 47719.570634
std 302888.715626
min 7.000000
25% 3732.000000
50% 10779.000000
75% 27990.500000
max 31480498.000000
在模拟数学模型时,一些相关函数可能很有用,包括diff
(离散差分)、cumsum
(累积和)和cumprod
(累积积)。diff
函数允许计算信号的离散导数(最多为标量系数),而cumsum
计算信号的离散不定积分。
高级数学处理
NumPy 提供了进行高效数值计算所需的所有类型和例程。SciPy 建立在 NumPy 之上,并实现了大量更高级的数学处理算法。这些算法涵盖了数值计算的多个领域,如优化、线性代数、信号处理、统计等。此外,各种SciKits包(如scikit-learn
、scikit-image
等)是更先进的包,实施了特定领域(如机器学习、图像处理等)中的高度专业化算法。
我们在这里简要概述了 SciPy 和其他几个包提供的科学计算功能。功能的完整列表可以在官方参考指南中找到:docs.scipy.org/doc/scipy/reference/
。提供实际的例子和应用超出了本书的范围,感兴趣的读者可以在NumPy Cookbook、Ivan Idris、Packt Publishing和Learning SciPy for Numerical and Scientific Computing、Francisco Blanco-Silva、Packt Publishing中找到各种各样的例子,均由 Packt Publishing 出版。
-
线性代数例程由
scipy.linalg
子包提供:线性方程求解器、矩阵例程、特征值问题、矩阵分解等。 -
优化例程由
scipy.optimize
子包提供:实值函数的无约束或有约束最小化、全局优化、曲线拟合等。 -
数值积分器由
scipy.integrate
子包提供。它可以用来解决微分方程,例如,在物理仿真引擎中使用。 -
信号处理算法由
scipy.signal
子包实现:卷积、线性滤波器、小波变换等。scipy.fftpack
子包实现了傅里叶变换例程,scipy.ndimage
子包实现了若干图像处理算法。最后,其他感兴趣的图像处理包包括scikit-image
、PIL
和OpenCV
(计算机视觉)。 -
统计例程由
scipy.stats
子包提供:概率分布、描述性统计和统计检验等。SciPy.cluster
实现了聚类算法,可用于在非结构化数据中找到类别。其他相关的统计包包括Pandas
和scikit-learn
(机器学习)。
总结
本章中,我们介绍了 NumPy 提供的多维数组对象,并展示了它如何用于对数值数据集进行高效计算。特别是,它非常适合加载任何类型的数据,Pandas 包使得这项任务变得简单,即使是复杂的数据文件。使用高级算法也是可能的,借助于强大的外部包,如 NumPy、SciPy 和 SciKit 库,在 IPython 中进行计算。然而,这一主题超出了本书的范围,感兴趣的读者可以在《NumPy Cookbook, Ivan Idris, Packt Publishing》和《Learning SciPy for Numerical and Scientific Computing, Francisco Blanco-Silva, Packt Publishing》中找到各种实例。
在下一章中,我们将介绍 IPython 和 Matplotlib 提供的可视化相关功能,它们通常与 NumPy 结合使用,用于数据的交互式可视化。
第四章:交互式绘图与图形界面
在本章中,我们将展示 Python 的图形功能以及如何与 IPython 交互式使用它们。
NumPy 提供了一种非常高效的方式来处理大量以多维数组结构化的数据。但是,查看数字网格通常比查看图形(如曲线图、散点图、图像等)直观得多。Matplotlib 是一个特别强大的 Python 包,用于从 NumPy 数据生成高质量的图形。它提供了一个简单的高级接口,类似于 Matlab,这是一种在工程和科学界广受欢迎的商业产品。Matplotlib 与 IPython 的集成非常好。
我们还将介绍图形用户界面(GUI)编程。全面覆盖这一丰富的主题远远超出了本书的范围。因此,我们将在本章中仅展示一些基本示例。我们将涵盖以下几个要点:
-
使用 Matplotlib 绘制图形
-
图像处理技术
-
地理地图
-
图形用户界面介绍
-
使用 IPython 的事件循环集成设计和调试图形用户界面(GUI)
使用 Matplotlib 绘制图形
有许多 Python 包可以用于曲线绘制,但目前使用最广泛的仍然是 Matplotlib。它是最完整、最强大的图形库之一。它既可以用于交互式可视化,也可以用于生成高质量的图形,方便在科学出版物中使用。此外,它的高级接口使得它特别容易使用。
在本节中,我们将展示 Matplotlib 提供的一些功能,以及如何将它与 IPython 结合使用。
设置 IPython 进行交互式可视化
IPython 实现了一个循环集成系统,允许从命令行界面显示图形窗口,而不会阻塞控制台。这在使用 Matplotlib 或创建图形用户界面时非常有用。
使用 Matplotlib
可以使用事件循环集成在 IPython 中交互式地显示图形。然后,它们可以从命令行界面动态更新。%pylab
魔法命令(或ipython
shell 命令中的--pylab
选项)会自动激活这一集成。可以选择用于 Matplotlib 和 IPython 的后端渲染器,例如,--pylab qt
,这需要 PyQt 或 PySide。
我们假设在本章中,IPython 的%pylab
模式是处于激活状态的。当从脚本中使用 Matplotlib 而不是 IPython 时,我们可以将from pylab import *
命令放在脚本的顶部。在 Python 模块中,使用import matplotlib.pyplot as plt
可能是个更好的选择,这样 Matplotlib 对象就会保持在它们的命名空间内。
此外,在脚本中生成图形的方式与在 IPython 中的方式略有不同。在脚本中,只有在调用show()
函数时,图形才会显示,通常是在脚本的最后,而在 IPython 命令行界面中,图形会在每次调用plot
函数时显示和更新。
交互式导航
在使用 Matplotlib 显示图形时,窗口中包含一些按钮,可以在图形中进行交互式导航(平移和缩放),以及更改图形的选项。还可以将图形保存为位图或矢量格式。
Matplotlib 在 IPython 笔记本中的应用
Matplotlib 也可以在笔记本中使用。当使用ipython notebook --pylab inline
启动笔记本时,绘制的图形会以图像形式出现在输出单元格中,并作为 base64 字符串保存在 IPYNB 文件中。如果没有使用此 inline 选项,图形将像往常一样显示在单独的窗口中。也可以通过使用命令%pylab inline
在笔记本中激活此选项。
标准图形
在本节中,我们将看到一些标准图形的示例,例如线图、曲线图、散点图和条形图。在接下来的章节中,我们还将看到图像和地图。但 Matplotlib 提供的图形类型远远不止我们在这里讲解的内容,包括 3D 图形、几何形状、向量场等。
曲线
使用 Matplotlib 绘制曲线实际上是绘制一小段一小段的线段,当线段足够多时,它们会给人一种平滑曲线的错觉。为了绘制一个数学函数,我们需要在给定区间内绘制该函数的样本,就像 NumPy 将函数表示为具有采样值的数组一样。
例如,一个时变信号可以表示为一个一维向量,其中包含在规则时间间隔(例如,每 1 毫秒以 1 kHz 的采样频率)采样的值,这样 1 秒钟的信号就可以表示为一个 1000 个单位长的向量。可以使用plot
函数将此信号绘制在屏幕上,例如:
在笔记本中绘制白噪声信号
在这里,我们生成一个包含随机值的向量,这些值遵循独立的正态随机变量。生成的信号是所谓的白噪声信号,即功率谱密度平坦的随机信号。当使用--pylab inline
选项在笔记本中绘制图形时,Matplotlib 会生成一张表示该曲线的图像,随后图像会自动插入到输出单元格中。
当plot
函数接收一个单一的向量作为参数时,它假设该向量包含 y 轴的值,而 x 轴的值则自动生成,从0
到len(y) - 1
的整数。若要显式指定 x 轴的值,我们可以使用以下命令:plot(x, y)
。
散点图
散点图通过使用像素或其他标记来表示二维空间中的一组点。让我们继续使用我们的城市示例。假设我们已经进入正确的目录(citiesdata
别名),我们可以加载数据并尝试绘制所有城市的地理坐标:
In [1]: import pandas as pd
In [2]: cd citiesdata
In [3]: filename = 'worldcitiespop.txt'
In [4]: data = pd.read_csv(filename)
In [5]: plot(data.Longitude, data.Latitude, ',')
在笔记本中显示城市坐标
在这个示例中,我们绘制了所有城市的纬度(y 轴)与经度(x 轴)。plot
函数的第三个参数(','
)指定了标记类型。在这里,它对应于一个散点图,每个城市用一个像素表示。即使大陆的形状看起来有些扭曲,我们仍然可以轻松识别出它们的轮廓。这是因为我们在笛卡尔坐标系中绘制地理坐标,而使用地图投影方法会更加合适。我们将在本章后面回到这个问题。
条形图
条形图通常用于直方图,表示不同区间内值的分布。Matplotlib 中的 hist
函数接受一个值的向量并绘制直方图。bins
关键字允许指定箱数或箱的列表。
例如,让我们绘制 Facebook 图示例中节点度数的直方图:
In [1]: cd fbdata
In [2]: import networkx as nx
In [3]: g = nx.read_edgelist('0.edges')
In [4]: hist(g.degree().values(), bins=20)
图中节点度数的分布
在这里,g.degree()
是一个字典,包含每个节点的度数(即与其连接的其他节点数)。values
方法返回所有度数的列表。
在 Matplotlib 中,比我们展示的图形类型要多得多,绘图的可能性几乎是无穷无尽的。可以在 Matplotlib 官方网站的画廊中找到各种图形示例(matplotlib.org/gallery.html
),以及 Nicolas Rougier 的教程(www.loria.fr/~rougier/teaching/matplotlib/
)。
绘图自定义
Matplotlib 提供了许多自定义选项。在这里,我们将看到如何更改图形中的样式和颜色,如何配置坐标轴和图例,以及如何在同一图形中显示多个图表。
样式和颜色
默认情况下,曲线是连续的,并且具有统一的颜色。可以在 plot
函数中轻松指定曲线的样式和颜色。
plot
函数的第三个参数指定了曲线的样式和颜色,使用简洁的语法。例如,'-r
' 表示“连续且为红色”,而 '--g
' 表示“虚线且为绿色”。有许多可能的样式,例如,':'
代表虚线,'-.'
代表点划线, '.'
代表点, ','
代表像素, 'o'
代表圆形标记,等等。
此外,有八种颜色对应一个字符的快捷键,分别是 b
、g
和 r
(主要的加色性颜色——蓝色、绿色和红色);c
、m
和 y
(次要的加色性颜色——青色、品红色和黄色);以及 k
和 w
(黑色和白色)。任何其他颜色可以通过其十六进制代码、RGB 或 RGBA 元组(值在 0
和 1
之间)等方式指定。
使用字符串指定样式和颜色只是更一般的指定图表样式和颜色方式的快捷方式,更通用的方式是使用特定的关键字参数。这些参数包括 linestyle
(或 ls
)、linewidth
(或 lw
)、marker
、markerfacecolor
(或 mfc
)、markersize
(或 ms
)等。完整的选项列表可以在 Matplotlib 的参考文档中找到。
此外,当在同一图形中显示多个图表时,每个图表的颜色会按预定义的颜色集轮换,例如蓝色、绿色、红色等。这个循环可以自定义:
In [1]: rcParams['axes.color_cycle'] = ['r', 'k', 'c']
提示
自定义 Matplotlib
rcParams
是 Matplotlib 中的一个全局字典样式的变量,用于存储自定义参数。几乎 Matplotlib 的每个方面都可以在这里进行配置。此外,还可以通过将其保存到名为 matplotlibrc
的 ASCII 文本文件中来指定永久的自定义选项,该文件可以存储在当前目录(用于本地选项)或 ~/.matplotlib
目录下(用于全局选项)。在此文件中,每一行包含一个自定义参数,例如 axes.color_cycle: ['r', 'k', 'c']
。
网格、坐标轴和图例
如果没有图例和坐标轴,图形无法传达任何有用的数据。默认情况下,Matplotlib 会自动显示坐标轴和刻度。刻度的位置可以通过 xticks
和 yticks
设置,可以通过 grid
函数添加网格。x 和 y 坐标的范围可以通过 xlim
和 ylim
指定。坐标轴标签可以通过 xlabel
和 ylabel
设置。此外,可以通过 legend
关键字指定图例;每条线的标签对应于 plot
函数的 label
关键字参数。最后,title
命令会显示图形的名称。以下示例演示了如何使用这些选项:
带坐标轴和图例的正弦和余弦函数
提示
图表叠加
调用不同的绘图函数会更新 Matplotlib 中的同一个图形。这是如何在同一个图形中显示多个图表的方式。要在新窗口中创建一个新图形,我们需要调用 figure()
函数。最后,可以通过使用子图,在同一个窗口中显示多个独立的图形,如我们在本节后面将要看到的那样。
来自 IPython 的交互
使用事件循环集成在 IPython 控制台中创建 Matplotlib 图形,可以通过编程与其进行交互。可以在一个图形中添加新的图表或实时更新它,以下面的示例为例:
In [1]: plot(randn(1000, 2))
Out[1]: [<matplotlib.lines.Line2D at 0x4cf4310>,<matplotlib.lines.Line2D at 0x4cf4450>]
我们首先创建一个包含两个白噪声信号的图形(plot
函数将每一列显示为独立的曲线)。一旦包含该图形的窗口打开,我们就可以在不关闭该窗口的情况下返回到 IPython 控制台。输出Out[1]
包含一个Line2D
对象的列表。实际上,Matplotlib 使用面向对象的方式描述图形。让我们按如下方式检索第一个对象(对应第一个曲线):
In [2]: line = _[0]
在line
变量上使用 Tab 键补全后,显示我们可以用来更新图形的方法列表。例如,要将线条颜色从蓝色更改为红色,我们可以输入以下命令:
In [3]: line.set_color('r')
然后,图形会相应地更新。可能需要强制刷新图形,例如通过平移或缩放。
最后,提一下图形窗口中的编辑按钮,它提供了一个 GUI 来更新一些图形的属性。
绘制多个图表
多个独立的图表可以显示在同一个图形中。我们可以定义一个具有任意行列数的网格,并在每个框内绘制图形。框可以跨越多个行或列(使用subplot2grid
)。例如,下面的示例展示了如何将两个具有不同坐标系的图形并排绘制:
x = linspace(0, 2 * pi, 1000)
y = 1 + 2 * cos(5 * x)
subplot(1,2,1)
plot(x, y)
subplot(1,2,2, polar=True)
polar(x, y)
在同一图形中绘制笛卡尔坐标和极坐标图
subplot
函数同时指定了图形有多少列(第一个参数)和多少行(第二个参数),以及绘制图形的框索引(第三个参数,基于 1 的索引,从左到右、从上到下)。polar=True
关键字参数指定第二个子图包含极坐标图。polar
函数类似于plot
函数,但使用极坐标系,其中包含角度 theta 和半径 r,其中 theta 是角度,r 是半径。
高级图形与图表
在本节中,我们将展示 Matplotlib 提供的与图像和地图相关的更高级图形功能。我们还将看看其他一些图形库。
图像处理
一幅彩色的 N x M 图像可以表示为一个 N x M x 3 的 NumPy 数组,代表红色、绿色和蓝色通道的三个 N x M 矩阵。然后,可以使用 NumPy 和 SciPy 高效地实现图像处理算法,并通过 Matplotlib 进行可视化。此外,PIL 包(Python Imaging Library)提供了基本的图像处理功能。
加载图片
Matplotlib 的imread
函数从硬盘中打开一个 PNG 图像,并返回一个 N x M x 3(如果存在 alpha 透明度通道,则为 N x M x 4)形状的 NumPy 数组。如果安装了 PIL,它还可以读取其他格式。PIL 还提供了open
函数来读取任何格式的图像(BMP、GIF、JPEG、TIFF 等)。
在下面的示例中,我们从远程 URL 下载一个 PNG 图像,并使用imread
加载它:
In [1]: import urllib2
In [2]: png = urllib2.urlopen('http://ipython.rossant.net/squirrel.png')
In [3]: im = imread(png)
In [4]: im.shape
Out[4]: (300, 300, 3)
imread
函数接受图像文件名或 Python 文件类对象(如这里使用的urlopen
返回的缓冲区)。imread
函数返回的对象是一个三维 NumPy 数组。
我们还可以使用 PIL 读取图像。我们可以直接使用Image.open
打开图像文件,或者使用Image.fromarray
函数将 NumPy 数组转换为 PIL 图像,如下所示:
In [5]: from PIL import Image
In [6]: img = Image.fromarray((im * 255).astype('uint8'))
fromarray
函数接受一个包含无符号 8 位整数的数组,值范围在0
到255
之间。这就是为什么我们需要将浮动点值的 NumPy 数组转换为所需的数据类型。相反,要将 PIL 图像转换为 NumPy 数组,我们可以使用array
函数im = array(img)
。
显示图像
Matplotlib 的imshow
函数从 NumPy 数组中显示图像,如以下示例所示:
In [7]: imshow(im)
在笔记本中使用 Matplotlib 显示图像
imshow
函数还接受二维 NumPy 数组(灰度图像)。从0
到1
的标量值到实际像素颜色的映射可以通过颜色映射来指定。颜色映射是一个线性渐变的颜色,定义了0
到1
之间任何值的颜色。Matplotlib 中有很多预定义的颜色映射,完整的列表可以在此找到:www.scipy.org/Cookbook/Matplotlib/Show_colormaps
要在imshow
中指定颜色映射,我们可以使用cmap=get_cmap(name)
关键字参数,其中name
是颜色映射的名称。
使用 PIL
基本的图像处理操作,如旋转、裁剪、滤波、复制粘贴、几何变换等,都是 PIL 提供的。例如,要旋转图像,我们可以使用以下命令:
In [9]: imshow(array(img.rotate(45.)))
使用 PIL 旋转图像
在这里,我们将图像逆时针旋转 45 度,并将图像从 PIL 转换回 NumPy 以进行显示。
高级图像处理 – 颜色量化
PIL 提供了基本的图像处理功能,而 SciPy 可用于更高级的算法。
这里我们将展示一个名为颜色量化的高级图像处理算法的小示例。该算法的原理是减少图像的颜色数量,同时保持图像的大部分视觉结构。在本示例中,我们将使用scipy.cluster
包实现该算法。我们将使用 k-means 算法将颜色值分组为少量的簇,并将每个像素分配给其所属组的颜色。以下是代码:
In [10]: from scipy.cluster.vq import *M = im[:,:,0].ravel()centroids, _ = kmeans(M, 4)qnt, _ = vq(M, centroids)clustered = centroids[reshape(qnt, (300, 300))]
我们仅取红色通道,并使用ravel
函数将图像展平,以便平等处理所有像素(也就是说,我们得到一个一维向量,而不是二维矩阵)。然后,kmeans
函数在颜色空间中寻找聚类,并返回质心颜色。最后,vq
函数将每个像素分配到其质心索引,并通过质心颜色(centroids
)对质心索引(在qnt
中)进行花式索引,得到最终的聚类图像。由于该算法的输出是灰度图像,我们需要指定一个颜色映射。我们将使用一组曾经流行的颜色,如下所示:
In [11]: cmap = matplotlib.colors.ListedColormap([(0,.2,.3),(.85,.1,.13),(.44,.6,.6),(1.,.9,.65)])
In [12]: imshow(clustered, cmap=cmap)
使用 SciPy 进行颜色量化
在这里,ListedColormap
函数创建了一个具有离散颜色集的自定义颜色映射。
最后,我们可以使用 Matplotlib 的imsave
函数将生成的图像保存为 PNG 文件,如下所示:
In [13]: imsave('squirrelama.png', clustered, cmap=cmap)
地图
地图是复杂但重要的图形类型。基础地图工具包(需要单独安装)为 Matplotlib 带来了地理能力。它非常强大,而在本节中我们仅仅是略微触及表面。具体来说,我们将继续使用城市示例,在天球仪上绘制人口密度图。
首先,我们按如下方式获取城市的位置和人口:
In [6]: locations = data[['Longitude','Latitude']].as_matrix()
In [7]: population = data.Population
接下来,我们通过指定投影类型和地图边界来初始化世界地图,如下所示:
In [8]: from mpl_toolkits.basemap import Basemap
In [9]: m = Basemap(projection='mill', llcrnrlat=-65, urcrnrlat=85,llcrnrlon=-180, urcrnrlon=180)
将地球表面投影到平面上的方法有很多种,选择某种投影方式取决于具体应用。在这里,我们使用米勒圆柱投影。其他关键字参数给出了左下角和右上角的纬度和经度。
下一步是生成一个二维图像,显示世界人口密度。为此,我们需要将城市的地理坐标投影到我们的地图上,如下所示:
In [10]: x, y = m(locations[:,0],locations[:,1])
调用函数m(long, lat)
可以获取地理位置的(x, y)
坐标,给定经纬度。为了生成密度图,我们还需要地图边界的坐标,如下所示:
In [11]: x0, y0 = m(-180, -65)
In [12]: x1, y1 = m(180, 85)
现在,让我们生成密度图。我们将使用histogram2d
函数,该函数从一组点生成一个二维直方图。在这里,每个点对应一个城市。我们还将为每个城市使用一个权重,代表其人口。对于没有人口的城市需要特别处理,我们将这些城市的权重设置为1000
,如下所示:
In [13]: weights = population.copy()
In [14]: weights[isnan(weights)] = 1000
In [15]: h, _, _ = histogram2d(x, y, weights=weights,bins=(linspace(x0, x1, 500), linspace(y0, y1, 500)))
现在,h
变量包含了一个 500 x 500 网格上每个小矩形的总人口数,覆盖了整个平面图。为了生成密度图,我们可以对log(h)
应用高斯滤波(相当于一种核密度估计),这可以通过 SciPy 实现。使用对数运算在值跨越多个数量级时非常有用。我们还需要处理零值(对应空白区域),因为零的对数是未定义的:
In [16]: h[h == 0] = 1
In [17]: import scipy.ndimage.filters
In [18]: z = scipy.ndimage.filters.gaussian_filter(log(h.T), 1)
该滤波器应用于函数log(h.T)
,因为h
变量的坐标系与地图的坐标系相比是转置的。此外,我们在这里使用了1
的滤波值。
最后,我们展示了密度图以及海岸线,如下所示:
In [19]: m.drawcoastlines()
In [20]: m.imshow(z, origin='lower', extent=[x0,x1,y0,y1],cmap=get_cmap('Reds'))
使用 Matplotlib.basemap 的世界人口密度图
3D 图
Matplotlib 包括一个名为mplot3d
的 3D 工具包,可以用于基本的 3D 图,如 3D 曲线、表面图等。举个例子,我们先创建一个表面图。我们首先需要导入mplot3d
工具包,方法如下:
In [1]: from mpl_toolkits.mplot3d import Axes3D
然后,我们使用以下命令创建表面图的 x、y 和 z 坐标:
In [2]: # we create a (X, Y) gridX = linspace(-5, 5, 50)Y = XX, Y = meshgrid(X, Y)# we compute the Z valuesR = sqrt(X**2 + Y**2)Z = sin(R)
NumPy 的函数meshgrid
返回一个网格中所有点的坐标,这个网格覆盖了由X
和Y
向量定义的矩形区域。最后,我们创建一个 3D 画布并绘制表面图,如下所示:
In [3]: ax = gca(projection='3d')surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1,cmap=mpl.cm.coolwarm, linewidth=0)
Matplotlib 的函数gca
返回当前的坐标轴实例,我们在这里指定该实例应使用 3D 投影。在plot_surface
函数中,rstride
和cstride
关键字参数分别表示表面的行和列步长,而cmap
是色图,linewidth
是线框的宽度。以下截图显示了结果:
使用 mplot3D 的表面图
动画
Matplotlib 能够创建动画,并通过 FFmpeg 或 MEncoder 将其导出为 MP4 视频。其基本思路是创建一个图形,并编写一个函数在定时间隔内更新它。动画模块的文档可以在matplotlib.org/api/animation_api.html
找到。此外,Jake Vanderplas 制作的教程可以在jakevdp.github.com/blog/2012/08/18/matplotlib-animation-tutorial/
获得。
其他可视化包
Matplotlib 并不是 Python 中唯一的可视化包。这里是一些类似的库:
-
Chaco:这是一个替代 Matplotlib 的库(
code.enthought.com/chaco/
) -
PyQwt:这是一个基于 PyQt 的绘图库(
pyqwt.sourceforge.net/
) -
PyQtGraph:这个包也是基于 PyQt 的,提供 2D 和 3D 绘图功能(
www.pyqtgraph.org/
) -
Visvis:这个包基于 OpenGL,提供了面向对象的绘图接口(
code.google.com/p/visvis/
) -
Mayavi:这个包提供了 3D 交互式可视化功能,如曲线、表面、网格、体积渲染等(
code.enthought.com/projects/mayavi/
) -
PyOpenGL:这个 Python 库提供对流行的 OpenGL 库的原始访问,提供低级别的硬件加速 2D/3D 图形功能(
pyopengl.sourceforge.net/
) -
Galry:这是一个基于 PyOpenGL 的高性能交互式可视化包,旨在处理非常大的数据集,包含数千万甚至数亿个数据点(
rossant.github.com/galry/
)
图形用户界面(GUI)
曾几何时,人机交互只能通过命令行界面进行。如今,大多数普通计算机用户比起使用键盘和黑屏闪烁的光标,更习惯使用鼠标和图形窗口。因此,任何开发者在某个时刻可能都需要编写一个图形界面,哪怕是最简单的界面,以便让非开发者用户能够方便地与程序进行交互。
GUI 可以轻松地集成到任何 Python 包中。Python 有很多图形工具包,大多数是本地或 C++ 图形库的封装。著名的工具包包括 Qt、wxWidgets、Tkinter、GTK 等。在本书的示例中,我们将使用 Qt。
GUI 编程可能是一个很难的课题,需要深入了解操作系统的底层细节、多线程编程,以及一些关于人机交互的基本概念。在本书中,我们将展示一个 "Hello World" 示例,介绍 PyQt 的基本知识。我们还将展示如何通过 IPython 交互式地操作 GUI。
设置 IPython 以进行交互式 GUI 编程
IPython 实现了一个循环集成系统,允许在命令行界面显示图形窗口而不阻塞控制台。在创建 GUI 时,这非常有用,因为可以通过命令行动态地与窗口进行交互。
%gui
魔法命令激活事件循环集成。我们需要提供要使用的图形库的名称。可能的名称有 wx
、qt
、gtk
和 tk
。在这里我们将使用 Qt。所以我们可以输入 %gui qt
。然后,IPython 中将自动启动主 Qt 应用程序。另一种方法是使用 ipython --gui qt
启动 IPython。
本节中的示例需要使用 PyQt4 或 PySide。我们假设已经安装了 PyQt4,但如果只安装了 PySide,您只需在导入语句中将 PyQt4
替换为 PySide
。这两个库提供的 Qt 绑定 API 几乎是相同的。
一个 "Hello World" 示例
在这个"Hello World"示例中,我们将显示一个窗口,里面有一个按钮,点击按钮会触发一个消息框。我们还将展示如何从 IPython 控制台与窗口进行交互。
要定义一个窗口,我们需要创建一个从QWidget
基类派生的类。QWidget
是所有 Qt 窗口和控件的基类,也称为小部件。以下是"Hello World"示例的代码:
from PyQt4 import QtGuiclass HelloWorld(QtGui.QWidget):def __init__(self):super(HelloWorld, self).__init__()# create the buttonself.button = QtGui.QPushButton('Click me', self)self.button.clicked.connect(self.clicked)# create the layoutvbox = QtGui.QVBoxLayout()vbox.addWidget(self.button)self.setLayout(vbox)# show the windowself.show()def clicked(self):msg = QtGui.QMessageBox(self)msg.setText("Hello World !")msg.show()
大部分工作发生在HelloWorld
小部件的构造函数中。我们首先需要调用父类的构造函数。接着,我们执行若干步骤来显示按钮:
-
我们首先创建一个按钮,类似于
QPushButton
类的实例。第一个参数是按钮的文本,第二个参数是父级小部件的实例(self
)。每个特定的控件和小部件都是由从QWidget
基类派生的类定义的,并且可以在QtGui
命名空间中找到。 -
我们定义了一个回调方法,当用户点击按钮时,该方法会被调用。
clicked
属性是一个 Qt 信号,一旦用户点击按钮就会发出。我们将这个信号连接到HelloWorld
小部件的clicked
方法(称为槽)。信号和槽是 Qt 使不同小部件彼此通信的方式。当某些事件发生时,信号会被触发,连接到这些信号的槽会在信号触发时被调用。任何小部件都包含许多预定义的信号,也可以创建自定义信号。 -
然后,我们需要将新创建的按钮放置在窗口中的某个位置。我们首先需要创建一个
QVBoxLayout
小部件,它是一个容器小部件,包含一个垂直排列的小部件堆栈。在这里,我们仅将按钮放入其中,使用addWidget
方法。我们还指定这个框是窗口的布局。通过这种方式,主窗口包含了这个框,而这个框又包含了我们的按钮。 -
最后,我们需要使用命令
self.show()
来显示窗口。
在clicked
方法中,我们创建一个QMessageBox
小部件,它默认表示一个包含文本和单个OK按钮的对话框。setText
方法指定文本,show
方法显示窗口。
现在,假设与 Qt 的事件循环集成已经在 IPython 中激活,无论是通过%gui qt
还是ipython --gui qt
,我们可以通过以下命令显示窗口:
In [1]: window = HelloWorld()
窗口随后会出现,且在窗口打开时,IPython 控制台仍然可用。
一个基本的 Qt 对话框
点击按钮会显示一个对话框,其中包含Hello World。
此外,我们还可以从 IPython 控制台动态地与窗口进行交互。例如,以下命令将显示 Hello World 对话框,就像我们点击了按钮一样:
In [2]: window.clicked()
这个功能在设计复杂窗口和调试时特别方便。
总结
在本章中,我们发现了 IPython、Matplotlib 和其他一些包提供的图形化功能。我们可以创建图表、图形、直方图、地图,显示和处理图像,图形用户界面等等。图形也可以非常容易地集成到笔记本中。图形的各个方面都可以进行自定义。这些原因解释了为什么这些工具在科学和工程领域中非常受欢迎,在这些领域中,数据可视化在大多数应用中扮演着核心角色。
在下一章中,我们将介绍一些加速 Python 代码的技巧。
第五章. 高性能与并行计算
一个反复出现的观点是,Python 不适合高性能数值计算,因为它是一种动态语言并且是解释型的,速度较慢。像 C 这样的编译型低级语言通常可以快几个数量级。我们在第三章,使用 IPython 进行数值计算中提出了第一个反驳,向量化的概念。对 NumPy 数组的操作几乎可以和 C 一样快,因为缓慢的 Python 循环会被快速的 C 循环透明替换。然而,有时某些复杂算法可能无法进行向量化,或者实现起来很困难。在这种情况下,幸运的是,除了抛弃所有的 Python 代码并重新用 C 编写一遍外,还有其他解决方案。我们将在本章中介绍其中的一些解决方案。
首先,可以利用现代计算机中普遍存在的多个核心。标准的 Python 进程通常只在单个核心上运行,但可以将任务分布到多个核心,甚至多个计算机上并行处理。使用 IPython 实现这一点特别简单。MPI 也可以通过几行代码轻松使用。
另一个流行的解决方案是首先检测 Python 算法中时间关键部分,然后用 C 代码替换它。通常,只有 Python 代码中的一小部分负责大部分算法的运行时间,因此可以将其余的代码保持为 Python 代码。Cython 是一个外部包,它使得这一任务比看起来要容易得多:它提供了一个 Python 的超集,经过编译后,可以无缝地集成到 Python 代码中。与 IPython 一起使用时,它尤其方便。
在本章结束时,我们将讨论以下内容:
-
如何通过 IPython 将独立的函数分布到多个核心
-
如何轻松地从 IPython 使用 MPI
-
如何使用单元魔法将 Python 代码转换为 C 代码
-
如何在 Cython 中使用 NumPy 数组使你的代码快几个数量级
交互式任务并行化
在本节中,我们将学习如何通过 IPython 将任务分配到不同的核心上。
Python 中的并行计算
Python 对并行计算特性的原生支持还有很大改进空间。一个长期存在的问题是,CPython 实现了 全局解释器锁 (GIL),根据官方 CPython 文档的描述,它是:
"...一个互斥锁,防止多个原生线程同时执行 Python 字节码。"
GIL 是必需的,因为 CPython 的内存管理不是线程安全的,但一个主要的缺点是,它会阻止多线程的 CPython 程序充分利用多核处理器。
注意
Python 的 GIL
有兴趣的读者可以在以下参考资料中找到有关 Python GIL 的更多信息:
-
wiki.python.org/moin/GlobalInterpreterLock
-
www.dabeaz.com/python/UnderstandingGIL.pdf
NumPy 中的一些线性代数函数可能通过释放 GIL 来利用多核处理器,前提是 NumPy 已经与适当的库(如 ATLAS、MKL 等)一起编译。否则,使用不同的 进程 而不是不同的 线程 来分配任务,是使用 Python 进行并行计算的典型方式。由于进程不共享同一内存空间,因此需要实现某种进程间通信,例如,使用 Python 的原生 multiprocessing 模块。一种更强大但更复杂的解决方案是使用 消息传递接口 (MPI)。
IPython 特别适合这两种解决方案,我们将在本节中讨论它们。它提供了一个强大而通用的并行计算架构。多个 IPython 引擎可以运行在不同的核心和/或不同的计算机上。独立任务可以轻松且均匀地分配,得益于 负载均衡。数据可以从一个引擎传输到另一个引擎,这使得通过 IPython 实现复杂的分布式算法成为可能。
并行计算是一个特别困难的话题,我们这里只覆盖最基础的内容。
在多个核心上分配任务
IPython 的并行计算功能非常丰富且高度可定制,但我们这里只展示最简单的使用方式。此外,我们将重点讨论并行计算的交互式使用,因为这正是 IPython 的精髓所在。
在一台计算机上分配代码到多个核心的步骤如下:
-
启动多个 IPython 引擎(通常每个处理器一个)。
-
创建一个
Client
对象,它充当这些引擎的代理。 -
使用客户端在引擎上启动任务并获取结果。
任务可以同步或异步启动:
-
使用 同步(或阻塞)任务时,客户端在任务启动后会阻塞,并在任务完成时返回结果。
-
使用 异步(非阻塞)任务时,客户端在任务启动后立即返回一个
ASyncResult
对象。该对象可以用来异步轮询任务状态,并在任务完成后随时获取结果。
启动引擎
启动引擎的最简单方法是在系统 shell 中调用 ipcluster start
命令。默认情况下,该命令会在本地机器的每个核心上启动一个引擎。可以通过 -n
选项指定引擎的数量,例如,ipcluster start -n 2
启动两个引擎。你可以通过 ipcluster -h
和 ipcluster start -h
查看其他可用选项。此外,Notebook 中有一个名为 Clusters 的面板,可以通过 Web 界面启动和停止引擎。
创建一个 Client 实例
客户端用于将任务发送到引擎。在 IPython 控制台或笔记本中,我们首先需要从parallel
子包中导入Client
类。
In [1]: from IPython.parallel import Client
下一步是创建一个Client
实例。
In [2]: rc = Client()
IPython 会自动检测正在运行的引擎。要检查正在运行的引擎数目,我们可以执行以下操作:
In [3]: rc.ids
Out[3]: [0, 1]
客户端的ids
属性提供了运行中引擎的标识符。在这里,本地计算机上有两个正在运行的引擎(它具有双核处理单元)。
使用并行魔法
从 IPython 发送任务到引擎的最简单方法是使用%px
魔法。它在引擎上执行单个 Python 命令。
In [4]: import os
In [5]: %px print(os.getpid())
[stdout:0] 6224
[stdout:1] 3736
默认情况下,命令会在所有运行中的引擎上执行,并且是同步模式。有几种方法可以指定要针对哪个引擎进行操作。
第一个可能性是使用%pxconfig
魔法命令:
In [6]: %pxconfig --targets 1
In [7]: %px print(os.getpid())
3736
--targets
选项接受一个索引或切片对象,例如,::2
表示所有偶数索引的引擎。这里,我们只指定第二个引擎。所有后续对%px
的调用将在指定的目标上执行。
一个等效的方法是使用%%px
单元魔法:
In [8]: %%px --targets :-1print(os.getpid())
[stdout:0] 6224
%%px
的选项适用于整个单元,这在笔记本中尤其方便。
另一个可用的选项是阻塞模式。默认情况下,%px
魔法假设是阻塞模式。要启用非阻塞模式,我们可以使用--noblock
选项。
In [9]: %%px --noblockimport timetime.sleep(1)os.getpid()
Out[9]: <AsyncResult: execute>
任务随后会异步执行。%pxresult
魔法命令会阻塞解释器,直到任务完成,并返回结果。
In [10]: %pxresult
Out[1:12]: 3736
并行映射
内置的map
函数按元素依次将 Python 函数应用于序列。IPython 提供了一个并行的map
函数,它语义上等价,但将不同的任务分派到不同的引擎上。这是将任务分发到多个核心的最简单方法。
创建视图
要使用它,我们首先需要使用Client
实例获取一个引擎的视图。视图表示一个或多个引擎,并通过在客户端上的索引语法获取。例如,要获取所有引擎的视图,我们可以使用以下命令:
In [11]: v = rc[:]
然后,视图可以用于在引擎上启动任务。此外,我们还可以使用sync_imports()
方法在引擎上导入包:
In [12]: with v.sync_imports():import time
importing time on engine(s)
同步映射
让我们定义以下简单函数:
In [13]: def f(x):time.sleep(1)return x * x
该函数接受一个数字,并在返回其平方之前等待一秒钟。要在零到九之间的所有数字上同步执行该函数,并使用我们的两个引擎(即使用两个 CPU),我们可以使用v.map_sync()
方法:
In [14]: v.map_sync(f, range(10))
Out[14]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
我们在几秒钟后获得一组结果。这里,每个引擎处理了五个任务,共计 10 个任务:
In [15]: %timeit -n 1 -r 1 v.map_sync(f, range(10))
1 loops, best of 1: 5.02 s per loop
In [16]: %timeit -n 1 -r 1 map(f, range(10))
1 loops, best of 1: 10 s per loop
异步映射
要异步执行该函数并传递参数列表,我们可以使用v.map()
方法:
In [17]: r = v.map(f, range(10))
In [18]: r.ready(), r.elapsed
Out[18]: False, 2.135
In [19]: r.get()
Out[19]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
In [20]: r.elapsed, r.serial_time
Out[20]: (5.023, 10.008)
r
变量是一个ASyncResult
对象,具有多个属性和方法,可以用来轮询任务的进度、已用时间,并获取任务的结果。elapsed
属性返回任务开始以来的已用时间。serial_time
属性仅在任务完成后可用,返回所有任务在所有引擎上累计花费的时间。ready()
方法随时返回一个值,表示任务是否完成。get()
方法会阻塞直到任务完成,并返回结果。
一个实际的例子 – 蒙特卡罗模拟
为了说明 IPython 提供的并行计算可能性,我们将考虑一个新的例子。我们想使用蒙特卡罗模拟来估算圆周率π。原理是,如果在边长为 1 的正方形内随机且均匀地采样n个点,那么当n趋近于无穷大时,距离一个固定角落小于 1 的点的比例趋近于π/4。下图说明了这一事实:
使用蒙特卡罗模拟估算圆周率π
这是一个特定的蒙特卡罗模拟例子,它通过重复进行大量的随机实验,最后取平均值来估算某些难以通过确定性方法获得的量。蒙特卡罗模拟广泛应用于科学、工程和金融领域。它们特别适合并行化,因为通常只是独立执行相同的函数很多次。
在这里,我们将使用这个随机实验来估算圆周率π。使用这种方法获得的精度较低,而且还有许多方法更加高效且精确。但这个示例足以介绍 IPython 的并行计算特性。
首先,我们将编写执行模拟的 Python 代码。sample
函数在立方体内生成n个点,并返回位于四分之一圆盘内的点的数量。
In [1]: def sample(n):return (rand(n) ** 2 + rand(n) ** 2 <= 1).sum()
由于括号内的n长向量是一个掩码数组(即它包含布尔值),它的和就是True
值的数量,也就是与 0 的欧几里得距离小于 1 的点的数量。
现在,要估算圆周率π,我们只需要将sample(n)
乘以4/n
即可:
In [2]: n = 1000000.
In [3]: 4 * sample(n) / n
Out[3]: 3.142184
由于圆周率π的真实值是 3.1415926535...,我们看到在一百万个点的情况下(对于这次特定的代码执行)有两个正确的数字。接下来我们将把这个任务分配到多个核心上。假设已经启动了多个引擎,例如使用ipcluster start
,下面是如何并行化代码:
In [4]: from IPython.parallel import Clientrc = Client()v = rc[:]with v.sync_imports():from numpy.random import rand
In [5]: 4 * sum(v.map_sync(sample, [n] * len(v))) / (n * len(v))
Out[5]: 3.141353
这里,len(v)
是引擎的数量。我们使用相同的参数 n
调用样本函数 len(v)
次。所有结果的总和是红点的总数,点的总数是 n * len(v)
。最后,我们使用前述公式得到 Pi 的估计。
使用 IPython 中的 MPI
MPI 是一种著名的标准化消息传递系统,特别适用于并行计算。我们假设 MPI 实现已经安装在您的系统上(如 Open-MPI,www.open-mpi.org
),以及用于从 Python 使用 MPI 的 mpi4py 包 (mpi4py.scipy.org
)。关于如何安装 MPI 的信息可以在这些网站上找到。
注意
Windows 上的 MPI
如果您使用的是 Windows,一种可能性是安装 Microsoft 的 MPI 实现,该实现可在 HPC Pack 中找到 (www.microsoft.com/en-us/download/details.aspx?id=36045
)。此外,您可能对 Python Tools for Visual Studio (pytools.codeplex.com
) 感兴趣,它可以将 Visual Studio 转变为 Python IDE。它提供了对 IPython 的原生支持,并专门设计用于 MPI 的高性能计算。
首先,我们需要为 MPI 创建一个特定的 IPython 配置文件。在 shell 中输入以下命令:
ipython profile create --parallel --profile=mpi
接下来,编辑文件 IPYTHONDIR/profile_mpi/ipcluster_config.py
(IPYTHONDIR
通常为 ~/.ipython
)并添加以下行:
c.IPClusterEngines.engine_launcher_class = 'MPIEngineSetLauncher'
现在,要启动带有四个引擎的集群,请在 shell 中输入以下命令:
ipcluster start -n 4 --profile=mpi
要在 IPython 中使用 MPI,我们首先需要通过 mpi4py 编写一个使用 MPI 的函数。在这个例子中,我们将在四个核心上并行计算从 1 到 16 的所有整数的总和。让我们在名为 psum.py
的文件中编写以下代码:
from mpi4py import MPI
import numpy as np# This function will be executed on all processes.
def psum(a):# "a" only contains a subset of all integers.# They are summed locally on this process. locsum = np.sum(a)# We allocate a variable that will contain the final result, that
# is the sum of all our integers.rcvBuf = np.array(0.0,'d')# We use a MPI reduce operation:# * locsum is combined from all processes# * these local sums are summed with the MPI.SUM operation# * the result (total sum) is distributed back to all processes in# the rcvBuf variableMPI.COMM_WORLD.Allreduce([locsum, MPI.DOUBLE],[rcvBuf, MPI.DOUBLE],op=MPI.SUM)return rcvBuf
最后,在 IPython 中可以交互地使用此函数,如下所示:
In [1]: from IPython.parallel import Client
In [2]: c = Client(profile='mpi')
In [3]: view = c[:]
In [4]: view.activate() # enable magics
In [5]: view.run('psum.py') # the script is run on all processes
In [6]: view.scatter('a', np.arange(16)) # this array is scattered across processes
In [7]: %px totalsum = psum(a) # psum is executed on all processes
Parallel execution on engines: [0, 1, 2, 3]
In [8]: view['totalsum']
Out[8]: [120.0, 120.0, 120.0, 120.0]
关于如何在 IPython 中使用 MPI 的更多详细信息可以在官方 IPython 文档中的以下网页找到(这个例子来自于这里):
ipython.org/ipython-doc/stable/parallel/parallel_mpi.html
IPython 的高级并行计算特性
我们仅覆盖了 IPython 中可用的并行计算特性的基础知识。更高级的特性包括以下内容:
-
动态负载均衡
-
在引擎之间推送和拉取对象
-
在不同计算机上运行引擎,可选地使用 SSH 隧道
-
在 Amazon EC2 集群中使用 StarCluster 运行 IPython
-
将所有请求和结果存储在数据库中
-
使用有向无环图 (DAG) 管理任务依赖关系
这些特性远超出本书的范围。有兴趣的读者可以在官方 IPython 文档中找到关于所有这些特性的详细信息。
在 IPython 中使用 C 语言和 Cython
将独立任务分配到多个核心是利用现代计算机并行能力的最简单方式,从而将总执行时间减少一倍或更多。然而,有些算法无法轻易拆分成独立的子任务。此外,某些算法可能在 Python 中非常缓慢,因为它涉及到无法向量化的嵌套循环。在这种情况下,一个非常有趣的选择是将代码中的一个小而关键的部分用 C 语言编写,以显著减少 Python 的开销。这个解决方案并不涉及任何并行计算特性,但仍然可以显著提高 Python 脚本的效率。此外,没有什么能阻止同时使用这两种技术:部分 C 编译和使用 IPython 进行并行计算。
Cython 包允许在不显式转换为 C 代码的情况下编译部分 Python 代码;它提供了在 Python 中调用 C 函数和定义 C 类型的扩展语法。相关的代码会被自动转换成 C 编译,并可以透明地从 Python 中使用。在某些情况下,当只能使用纯 Python 代码,并且由于算法的特殊性质无法使用 NumPy 进行向量化时,速度提升可能会非常显著,达到几个数量级。
在本节中,我们将学习如何在 IPython 中交互式地使用 Cython。我们还将查看一个纯 Python 函数实现的数值算法示例,该算法可以通过 Cython 编译,执行速度比原来快超过 300 倍。
安装和配置 Cython
Cython 包的安装比其他包稍微困难一些。原因是使用 Cython 需要编译 C 代码,这显然需要一个 C 编译器(例如流行的 GNU C 编译器 gcc)。在 Linux 上,gcc 已经可以使用,或者通过包管理器轻松安装,例如在 Ubuntu 或 Debian 上使用 sudo apt-get install build-essential
。在 OS X 上,可以选择安装 Apple XCode。在 Windows 上,可以安装 MinGW(www.mingw.org
),它是 gcc 的一个开源发行版。然后,Cython 可以像安装其他包一样进行安装(见 第一章,开始使用 IPython)。更多信息可以在 wiki.cython.org/Installing
找到。
注意
在 Windows 上配置 MinGW 和 Cython
在 Windows 上,根据 MinGW 的版本,编译 Cython 代码时可能会出现错误信息。要修复此 bug,您可能需要打开 C:\Python27\Lib\distutils\cygwinccompiler.py
(或根据您的具体配置类似路径),然后将所有 -mno-cygwin
的出现替换为空字符串 ""
。
此外,确保 C:\MinGW\bin
在 PATH
环境变量中。最后,可能需要编辑(或创建)文件 C:\Python27\Lib\distutils\distutils.cfg
并添加以下几行代码:
[build]
compiler = mingw32
你可以在 wiki.cython.org/InstallingOnWindows
上找到更多信息。
在 IPython 中使用 Cython
使用 Cython 时,代码通常写在 .pyx
文件中,该文件通过 Cython 转换为 C 代码。然后,生成的 C 程序由 C 编译器编译成 .so
文件(在 Linux 上)或 .pyd
文件(在 Windows 上),可以在 Python 中正常导入。
这个过程通常涉及一个 distutils setup.py
脚本,该脚本指定了需要编译的文件和不同的编译器选项。由于这个步骤并不特别困难,我们在这里不会详细介绍。相反,我们将展示如何在 IPython 中轻松使用 Cython。其优点在于 Cython 和 C 编译过程会在后台自动完成,无需手动编写 setup.py
脚本。IPython 笔记本在这里尤其有用,因为它比控制台更方便编写多行代码。
在这里,我们将展示如何使用 %%cython
单元魔法从 IPython 中执行 Cython 代码。第一步是加载 cythonmagic
扩展。
In [1]: %load_ext cythonmagic
然后,%%cython
单元魔法允许编写会自动编译的 Cython 代码。单元中定义的函数将在交互式会话中可用,并且可以像普通 Python 函数一样使用。
In [2]: %%cythondef square(x):return x * x
In [3]: square(10)
Out[3]: 100
在这里,调用 square(10)
涉及调用一个编译的 C 函数,该函数计算数字的平方。
使用 Cython 加速纯 Python 算法
在这里,我们将看到如何将一个包含嵌套循环的纯 Python 算法转换为 Cython,从而实现大约 10 倍的速度提升。这个算法是厄拉托斯特尼筛法,一个用于寻找小于固定数字的所有素数的千年算法。这个非常经典的算法的过程是从 2 到 n 之间的所有整数开始,逐步去除已找到的素数的倍数。在算法结束时,剩下的就是素数。我们将用 Python 实现这个算法,并展示如何将其转换为 Cython。
纯 Python 版本
在纯 Python 中,该算法仅由十几行代码组成。这个实现可以通过多种方式进行改进和简化(实际上可以用一行代码实现!),但对于这个示例来说,它已经足够,因为我们主要关注的是纯 Python 和 Cython 版本的相对执行时间。
In [1]: def primes1(n):primes = [False, False] + [True] * (n - 2)i = 2# The exact code from here to the end of the function# will be referred as #SIEVE# in the next examples.while i < n:# we do not deal with composite numbersif not primes[i]:i += 1continuek = i * i# mark multiples of i as composite numberswhile k < n:primes[k] = Falsek += ii += 1return [i for i in xrange(2, n) if primes[i]]
In [2]: primes(20)
Out[2]: [2, 3, 5, 7, 11, 13, 17, 19]
primes
变量包含布尔值,用于指示关联的索引是否是素数。我们初始化时,只将0
和1
标记为合数(非素数),并使用“如果且仅如果一个正整数恰好有两个正除数,则它是素数”这一定义。然后,在每次对i
的迭代中,我们会标记更多的数为合数,而不会改变素数。每个i
代表一个素数,k
的迭代则会将所有i
的倍数标记为合数。最后,我们返回所有True
的索引,也就是所有小于n
的素数。
现在,让我们来看一下这个函数的执行时间:
In [3]: n = 10000
In [4]: %timeit primes1(n)
100 loops, best of 3: 5.54 ms per loop
我们将尝试使用 Cython 加速这个函数。
初级 Cython 转换
作为第一次尝试,我们将直接使用相同的 Cython 代码。
In [5]: %load_ext cythonmagic
In [6]: %%cythondef primes2(n):primes = [False, False] + [True] * (n - 2)i = 2#SIEVE#: see full code above
In [7]: timeit primes2(n)
100 loops, best of 3: 3.25 ms per loop
我们仅通过在单元格顶部添加%%cython
,就实现了 70%的速度提升,但如果我们为 Cython 提供类型信息,性能可以更好。
添加 C 类型
上一个例子中的速度提升比较温和,因为局部变量是动态类型的 Python 变量。这意味着,由于 Python 的动态特性,其开销仍然是与纯 C 代码相比导致性能差异的重要原因。我们可以通过使用cdef
关键字将 Python 变量转换为 C 变量,从而提高性能。
In [8]: %%cythondef primes3(int n):primes = [False, False] + [True] * (n - 2)cdef int i = 2cdef int k = 0#SIEVE#: see full code above
相较于初级版本,有三个变化:n
参数被静态声明为整数,局部变量i
和k
现在声明为 C 整数变量。因此,性能提升更加明显:
In [9]: timeit primes3(n)
1000 loops, best of 3: 538 us per loop
仅通过使用%%cython
魔法和一些类型声明,这个函数现在比纯 Python 版本快了 10 倍。通过更合适的数据结构,结果可能还会得到进一步的优化。
通常,知道哪些代码部分在转换为 Cython 后能显著提升性能,需要对 Python 的内部机制有一定了解,更重要的是,需要进行广泛的性能分析。Python 循环(尤其是嵌套循环)、Python 函数调用以及在紧密循环中操作高层数据结构,都是 Cython 优化的经典目标。
使用 NumPy 和 Cython
在本节中,我们将展示如何将 NumPy 数组与 Cython 代码结合使用。我们还将看到如何通过将 Python 函数转换为 C 函数,显著优化在紧密循环中对 Python 函数的调用。
Python 版本
在这里,我们将通过一个随机过程模拟的示例来演示,具体是布朗运动。该过程描述了一个粒子从x=0
出发,在每个离散时间步长上进行+dx
或-dx
的随机步伐,其中dx
是一个小常数。这种类型的过程在金融、经济、物理学、生物学等领域中都很常见。
这个特定的过程可以通过 NumPy 的cumsum()
和rand()
函数非常高效地模拟。然而,更复杂的过程可能需要进行模拟,例如一些模型要求在位置达到某个阈值时进行瞬时跳跃。在这些情况下,向量化不是一种选择,因此手动循环是不可避免的。
In [1]: def step():return sign(rand(1) - .5)def sim1(n):x = zeros(n)dx = 1./nfor i in xrange(n - 1):x[i+1] = x[i] + dx * step()return x
step
函数返回一个随机的+1
或-1
值。它使用了 NumPy 的sign()
和rand()
函数。在sim1()
函数中,轨迹首先被初始化为一个全为零的 NumPy 向量。然后,在每次迭代中,会向轨迹中添加一个新的随机步长。then
函数返回完整的轨迹。以下是一个轨迹的示例:
In [2]: plot(sim1(10000))
布朗运动的模拟
让我们看看这个函数的执行时间。
In [3]: n = 10000
In [4]: timeit sim1(n)
1 loops, best of 3: 249 ms per loop
Cython 版本
对于 Cython 版本,我们将做两件事。首先,我们将为所有局部变量以及包含轨迹的 NumPy 数组添加 C 类型。同时,我们将把step()
函数转换为一个纯 C 函数,该函数不调用任何 NumPy 函数。我们将调用在 C 标准库中定义的纯 C 函数。
In [4]: %%cythonimport numpy as npcimport numpy as npDTYPE = np.doublectypedef np.double_t DTYPE_t# We redefine step() as a pure C function, using only# the C standard library.from libc.stdlib cimport rand, RAND_MAXfrom libc.math cimport roundcdef double step():return 2 * round(float(rand()) / RAND_MAX) - 1def sim2(int n):# Local variables should be defined as C variables.cdef int icdef double dx = 1\. / ncdef np.ndarray[DTYPE_t, ndim=1] x = np.zeros(n, dtype=DTYPE)for i in range(n - 1):x[i+1] = x[i] + dx * step()return x
我们首先需要导入标准的 NumPy 库,以及一个特殊的 C 库,也叫做NumPy
,它是 Cython 包的一部分,通过cimport
引入。我们定义了 NumPy 的数据类型double
和相应的 C 数据类型double_t
,使用ctypedef
。这允许在编译时而非执行时定义x
数组的确切类型,从而大幅提高速度。x
的维度数也在sim2()
函数内指定。所有局部变量都被定义为 C 类型的 C 变量。
step()
函数已被完全重写。现在它是一个纯 C 函数(使用cdef
定义)。它使用 C 标准库中的rand()
函数,该函数返回一个介于 0 和RAND_MAX
之间的随机数。math
库中的round()
函数也被用来生成一个随机的+1
或-1
值。
让我们来检查一下sim2()
函数的执行时间:
In [5]: timeit sim2(n)
1000 loops, best of 3: 670 us per loop
Cython 版本比 Python 版本快 370 倍。这一剧烈的速度提升的主要原因在于 Cython 版本仅使用纯 C 代码。所有变量都是 C 变量,之前需要调用 Python 函数的step
函数,现在只需要调用纯 C 函数,这大大减少了循环中 Python 的开销。
加速 Python 代码的更高级选项
Cython 还可以用来将现有的 C 代码或库与 Python 接口,但我们在这里不讨论这种用例。
除了 Cython 之外,还有其他一些加速 Python 代码的包。SciPy.weave
(www.scipy.org/Weave
) 是 SciPy 的一个子包,允许将 C/C++ 代码嵌入到 Python 代码中。Numba (numba.pydata.org/
) 使用即时编译的 LLVM 技术,通过动态和透明地编译纯 Python 代码,从而显著加速其执行。它与 NumPy 数组有很好的兼容性。安装时需要 llvmpy 和 meta。
相关项目包括 Theano (deeplearning.net/software/theano/
),它通过在 CPU 或显卡上透明地编译数学表达式,使得在数组上定义、优化和评估这些表达式变得非常高效。同样,Numexpr (code.google.com/p/numexpr/
) 可以编译数组表达式,并利用矢量化 CPU 指令和多核处理器的优势。
Blaze (blaze.pydata.org/
) 是一个仍处于早期开发阶段的项目,旨在将所有这些动态编译技术整合到一个统一的框架中。它还将通过允许类型和形状的异质性、缺失值、标记维度(如在 Pandas 中)等,扩展多维数组的概念。由 NumPy 的创建者开发,它有望在不久的将来成为 Python 计算社区的一个核心项目。
最后,PyOpenCL (mathema.tician.de/software/pyopencl
) 和 PyCUDA (mathema.tician.de/software/pycuda
) 是 OpenCL 和 CUDA 的 Python 封装库。这些库实现了类似 C 的低级语言,可以在现代显卡上编译,利用其大规模并行架构。事实上,显卡包含数百个专用核心,可以非常高效地处理大量元素的函数(单指令多数据 (SIMD) 模式)。与纯 C 代码相比,速度提升可能达到一个数量级以上。OpenCL 是一种开放标准语言,而 CUDA 是由 Nvidia 公司拥有的专有语言。CUDA 代码仅能在 Nvidia 显卡上运行,而 OpenCL 则得到大多数显卡和大多数 CPU 的支持。在后者的情况下,相同的代码会在 CPU 上编译,并利用多核处理器和矢量化指令。
摘要
本章介绍了两种加速 Python 代码的方法:通过将 Python 代码转换为低级 C 代码来绕过 Python 的开销,或者通过将 Python 代码分布到多个计算单元上,利用多核处理器的优势。两种方法甚至可以同时使用。IPython 大大简化了这些技术。虽然并行计算和 Cython 可以在没有 IPython 的情况下使用,但它们需要更多的样板代码。
在下一章中,我们将探讨一些高级选项来定制 IPython。
第六章. 自定义 IPython
IPython 可以定制和扩展以满足高级用途。在本章结束时,你将了解:
-
如何创建和使用自定义配置文件
-
如何在高级应用中使用 IPython 扩展
-
如何在笔记本中使用不同的语言
-
如何创建你自己的扩展
-
如何在前端使用丰富的表示形式
-
如何将 IPython 嵌入到 Python 代码中
IPython 配置文件
配置文件是特定于本地计算机上某个用户的,包含 IPython 偏好设置以及历史记录、临时文件、日志文件等。默认情况下,存在一个名为默认配置文件的配置文件。要手动创建它,我们可以在系统 shell 中运行以下命令:
ipython profile create
要指定配置文件的名称,我们可以使用 ipython profile create name
命令。
配置文件的位置
配置文件通常存储在 ~/.ipython
或 ~/.config/ipython
中,其中 ~
是当前用户的主目录。这个目录通常被称为IPython 目录,有时也称为 IPYTHONDIR
。要找到配置文件的确切位置,我们可以运行 ipython locate
命令来定位 IPython 配置目录,或者运行 ipython locate profile default
来定位特定配置文件目录,其中 default
是配置文件的名称。配置文件名称通常存储在 IPython 配置文件夹中的名为 profile_name
的文件夹中。
默认情况下,IPython 使用默认配置文件启动。要在运行 IPython 时指定不同的配置文件,我们可以使用 --profile
命令行参数,例如:
ipython --profile=name
IPython 配置文件
在每个配置文件中,都有一个名为 ipython_config.py
的特殊配置文件。这个 Python 脚本是用于指定各种选项的占位符。它包含一个完整的模板,包含了大多数可能的选项,并且有详细文档,因此修改起来应该很简单。
例如,要在配置文件中自动启用pylab模式以及 qt
事件循环集成系统,以下几行应出现在相应的 ipython_config.py
文件中:
# Enable GUI event loop integration ('qt', 'wx', 'gtk', 'glut',
# 'pyglet','osx').
c.InteractiveShellApp.gui = 'qt'# Pre-load matplotlib and numpy for interactive use, selecting a
# particular matplotlib backend and loop integration.
c.InteractiveShellApp.pylab = 'qt'# If true, an 'import *' is done from numpy and pylab, when using # pylab
c.InteractiveShellApp.pylab_import_all = True
在 IPython 启动时加载脚本
你可以在每次启动 IPython 时自动加载一些 Python 脚本,只需将它们放入 IPYTHONDIR/startup/
文件夹中。如果你希望每次 IPython 启动时加载模块或执行一些脚本,这个方法会很有用。
IPython 扩展
IPython 扩展允许在 IPython 中实现完全自定义的行为。它们可以通过简单的魔法命令手动加载,或者在 IPython 启动时自动加载。
多个扩展已原生包含在 IPython 中。它们本质上允许从 IPython 执行非 Python 代码。例如,cythonmagic
扩展提供了 %%cython
单元魔法,用于直接在 IPython 中编写 Cython 代码,就像我们在第五章,高性能和并行计算中看到的那样。类似的内置扩展还包括 octavemagic
和 rmagic
,用于在 IPython 中执行 Octave 和 R 代码。它们在笔记本中尤其有用。
第三方模块也可以实现自己的扩展,正如我们在本节中看到的逐行分析模块。最后,我们将展示如何创建新的扩展。
示例 – 逐行分析
line_profiler
和 memory_profiler
包是逐行分析器,它们提供非常精确的代码段细节,显示哪些部分的代码耗时过长或使用了过多内存。它们提供的魔法命令可以手动与 IPython 集成。首先,我们需要安装这些包,例如使用 easy_install
、pip
或 Christoph Gohlke 的网页(Windows 用户)。在 Windows 上需要 psutil
包,它可以在同一个网页上找到。
要激活这两个包中实现的魔法命令,我们需要编辑 IPython 配置文件,并添加以下几行:
c.TerminalIPythonApp.extensions = ['line_profiler','memory_profiler'
]
此时,lprun
、mprun
和 memit
魔法命令可供使用。当要分析的函数定义在文件中而非交互式会话中时,逐行分析器效果最佳,因为此时分析器可以在分析报告中显示每一行的内容。
作为示例,让我们创建一个脚本 myscript.py
,使用以下代码:
import numpy as np
import matplotlib.pyplot as plt
def myfun():dx = np.random.randn(1000, 10000)x = np.sum(dx, axis=0)plt.hist(x, bins=np.linspace(-100, 100, 20))
该函数模拟 10,000 次随机游走(布朗运动),每次 1,000 步,并绘制模拟结束时粒子位置的直方图。
现在,我们将加载这个函数到 IPython 中并进行分析。%lprun
魔法命令接受一个 Python 语句以及需要逐行分析的函数列表,函数列表通过 -f
选项指定:
In [1]: from myscript import myfun
In [2]: lprun -f myfun myfun()
Timer unit: 5.13284e-07 sFile: myscript.py
Function: myfun at line 3
Total time: 1.26848 sLine # Hits Time Per Hit % Time Line Contents
==============================================================3 def myfun():4 1 1783801 1783801.0 72.2 dx =
np.random.randn(1000, 1000)5 1 262352 262352.0 10.6 x =
np.cumsum(dx, axis=0)6 1 425142 425142.0 17.2 t =
np.arange(1000)7
np.histogram2d(t, x)
我们可以观察到,大部分执行时间发生在创建 dx
数组的过程中。
%mprun
魔法命令也可以类似地用于内存分析。
逐行分析器在分析复杂的 Python 应用时特别有用。通过简单的魔法命令在 IPython 中交互式地进行分析尤其方便。
创建新扩展
要创建扩展,我们需要在 Python 路径中的目录中创建一个 Python 模块。可以将其放在当前目录,或者放在 IPYTHONDIR/extensions/
中。
扩展实现了一个 load_ipython_extension(ipython)
函数,该函数以当前的 InteractiveShell
实例作为参数(可能还包括 unload_ipython_extension(ipython)
,该函数在卸载扩展时被调用)。此实例可用于注册新的魔法命令、访问用户命名空间、执行代码等。当扩展被加载时,会调用此加载函数,这通常发生在执行 %load_ext
或 %reload_ext
魔法命令时。为了在 IPython 启动时自动加载模块,我们需要将模块名添加到 IPython 配置文件中的 c.TerminalIPythonApp.extensions
列表中。
提示
InteractiveShell 实例
InteractiveShell
实例代表当前活跃的 IPython 解释器。其有用的方法和属性包括 register_magics()
,用于创建新的魔法命令,以及 user_ns
,用于访问用户命名空间。你可以通过 IPython 的自动补全功能交互式地探索实例的所有属性。为此,你需要执行以下命令来获取当前实例:
ip = get_ipython()
示例 – 在 IPython 中执行 C++ 代码
在这个示例中,我们将创建一个新的扩展,以便直接在 IPython 中执行 C++ 代码。这仅是一个教学示例,在实际项目中,使用 Cython 或 SciPy.weave 可能会是一个更好的选择。
该扩展定义了一个新的单元魔法命令 cpp
。其理念是能够直接在单元中编写 C++ 代码,并且自动进行编译和执行。单元的输出将包含代码的标准输出。以下是该扩展如何工作的说明:
-
我们创建了一个新的类,继承自
IPython.core.magic.Magics
-
在这个类中,我们创建了一个带有
cell_magic
装饰器的新方法:它将实现cpp
单元魔法命令 -
该方法接受单元的代码作为输入,将 C++ 代码写入一个临时文件,并调用 g++ 编译器来创建可执行文件
-
该方法随后调用新创建的可执行文件并返回输出
-
在
load_ipython_extension
函数中,我们注册了这个魔法类
以下代码应写入 cppmagic.py
脚本:
import IPython.core.magic as ipym@ipym.magics_class
class CppMagics(ipym.Magics):@ipym.cell_magicdef cpp(self, line, cell=None):"""Compile, execute C++ code, and return the standard output."""# Define the source and executable filenames.source_filename = 'temp.cpp'program_filename = 'temp.exe'# Write the code contained in the cell to the C++ file.with open(source_filename, 'w') as f:f.write(cell)# Compile the C++ code into an executable.compile = self.shell.getoutput("g++ {0:s} -o {1:s}".format(source_filename, program_filename))# Execute the executable and return the output.output = self.shell.getoutput(program_filename)return outputdef load_ipython_extension(ipython):ipython.register_magics(CppMagics)
以下截图展示了如何方便地在 IPython 笔记本中使用这个扩展来编写 C++ 代码:
在 IPython 笔记本中执行 C++ 代码
该代码在 Windows 上有效,并可以很容易地适配到 Unix 系统。
提示
改进这个示例
这个例子可以在很多方面改进:临时文件可以有唯一的名称,并存储在一个特殊的临时目录中,编译错误可以得到很好的处理并重定向到 IPython 等等。感兴趣的读者可以查看内置的 Cython、Octave 和 R 魔法扩展,这些扩展在IPython/extensions/
目录下,与这个例子有些相似。更一般地说,相同的技术可以用来在 IPython 中运行非 Python 代码,甚至可能实现 Python 与其他语言之间的变量共享。
IPython 扩展在笔记本环境中尤为强大,因为它们特别允许在单元格代码中实现任意复杂的行为。
提示
扩展索引
由 IPython 用户创建的 IPython 扩展索引可以在github.com/ipython/ipython/wiki/Extensions-Index
找到。如果你开发了自己的扩展,欢迎在此添加!
前端的丰富表现
笔记本和 Qt 控制台可以显示更丰富的对象表现。它们都能显示位图和 SVG 图像,且笔记本还支持视频、HTML 代码和 LaTeX 数学方程式。通过类显示丰富的对象特别简单:只需要实现一个名为_repr_*_
的方法,其中*
可以是svg
、png
、jpeg
、html
、json
、pretty
或latex
。例如,定义一个类Disc
,并实现 SVG 表示方法:
In [1]: class Disc(object):def __init__(self, size, color= ared'):self.size = sizeself.color = colordef _repr_svg_(self):return """<svg version="1.1"><circle cx="{0:d}" cy="{0:d}" r="{0:d}" fill="{1:s}" /></svg>""".format(self.size, self.color)
该类的构造函数接受一个以像素为单位的半径大小和一个作为字符串的颜色。然后,当该类的实例指向标准输出时,SVG 表示会自动显示在单元格的输出中,如下图所示:
IPython 笔记本中的 SVG 表示
显示对象丰富表现的另一种方式是使用IPython.display
模块。你可以通过标签补全交互式地获取所有受支持的表现列表。例如,下面的截图展示了如何在笔记本中渲染 LaTeX 方程式:
IPython 笔记本中的 LaTeX 方程式
笔记本的丰富显示功能使其特别适合创建教学内容、演示文稿、博客文章、书籍等,因为笔记本可以导出为 HTML 或 PDF 等格式。
然而,在未来版本的 IPython 中,支持自定义 JavaScript 扩展和小部件的笔记本可能会实现更丰富的交互式表现。
嵌入 IPython
即使在标准的 Python 解释器运行脚本时,也可以从任何 Python 脚本启动 IPython。在某些情况下,当你需要在某个时刻与复杂的 Python 程序交互,而又不希望或无法对整个程序使用 IPython 解释器时,这个功能非常有用。例如,在科学计算中,你可能希望在某些自动计算密集型算法执行完后暂停程序,查看数据、绘制一些图表等,然后再继续程序。另一个可能的使用场景是将小部件集成到图形用户界面中,通过 IPython 命令行界面让用户与 Python 环境进行交互。
在程序中集成 IPython 最简单的方法是在你的 Python 程序的任何位置调用 IPython.embed()
(在 import IPython
之后)。你还可以指定自定义选项,包括命令行界面的输入/输出模板、启动/退出消息等。你可以在 ipython.org/ipython-doc/stable/interactive/reference.html#embedding-ipython
找到更多信息。
结束语
到目前为止,你应该已经确信 IPython 的强大功能和灵活性。IPython 不仅本身提供了大量有用的功能,它还允许你在几乎任何方面扩展和定制它。然而,需要注意的是,这个项目仍在不断发展中。尽管它已经创建超过 10 年,但在写本文时,版本 1.0 仍未发布。IPython 的核心功能现在已经相当稳定和成熟。最近的功能——笔记本,预计将在未来几年有重要的发展。预计将在笔记本中创建自定义交互式小部件的功能将成为整个项目的一个重要特点。有关即将到来的开发的更多信息,可以在 github.com/ipython/ipython/wiki/Roadmap:-IPython
和 ipython.org/_static/sloangrant/sloan-grant.html
找到。
最后,IPython 是一个活跃的开源项目,任何人都可以贡献。贡献可以像报告或修复一个 bug 那样简单,但总是非常有用并且受到高度赞赏!相关地,任何人都可以在遵守公共礼仪规则的前提下在线请求帮助。开发人员和最活跃的用户总是愿意提供帮助。以下是一些有用的链接:
-
GitHub 项目页面:
github.com/ipython/ipython
-
Wiki:
github.com/ipython/ipython/wiki
-
用户邮件列表:
mail.scipy.org/mailman/listinfo/ipython-user
-
聊天室:
www.hipchat.com/ghSp7E1uY
摘要
在本章中,我们描述了如何定制和扩展 IPython,特别是通过扩展功能。非 Python 语言也可以通过 IPython 调用,这在笔记本中尤其方便,因为任何代码都可以复制并粘贴到一个单元格中,并在当前的 Python 命名空间中透明地编译和执行。该笔记本还支持丰富的显示功能,并且很快将支持交互式小部件,使其成为目前为止最先进的 Python 交互式编程和计算工具。