IPython-交互式计算和可视化秘籍第二版-一-

51 阅读1小时+

IPython 交互式计算和可视化秘籍第二版(一)

原文:annas-archive.org/md5/62516af4e05f6a3b0523d8aa07fd5acb

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

我们正被来自科学研究、工程学、经济学、政治学、新闻学、商业等多个领域的数字数据洪流所淹没。因此,分析、可视化和利用数据成为了越来越多人的职业。编程、数值计算、数学、统计学和数据挖掘等量化技能构成了数据科学的核心,并在看似无尽的各个领域中愈加受到重视。

我之前的书籍,《Learning IPython for Interactive Computing and Data Visualization》,Packt Publishing,2013 年出版,是一本面向初学者的数据科学与 Python 数值计算入门书籍。这种广泛使用的编程语言也是这些学科最受欢迎的平台之一。

本书继续了这一旅程,展示了 100 多个高级数据科学和数学建模的案例。这些案例不仅涵盖了编程和计算主题,如交互式计算、数值计算、高性能计算、并行计算和交互式可视化,还包括数据分析主题,如统计学、数据挖掘、机器学习、信号处理等多个领域。

本书中的所有代码均在 IPython 笔记本中编写。IPython 是 Python 数据分析平台的核心。最初为了增强默认的 Python 控制台,IPython 现在以其广受好评的笔记本而闻名。这个基于 Web 的交互式计算环境将代码、丰富的文本、图片、数学方程式和图表整合成一个文档。它是进行 Python 数据分析和高性能数值计算的理想入口。

本书是什么

本书包含超过一百个专注的实用案例,回答了在 IPython 上进行数值计算和数据分析的具体问题,涉及内容包括:

  • 如何使用 pandas、PyMC 和 SciPy 探索公共数据集

  • 如何在 IPython 笔记本中创建交互式图表、小部件和图形用户界面

  • 如何创建具有自定义魔法命令的可配置 IPython 扩展

  • 如何在 IPython 中并行分配异步任务

  • 如何使用 OpenMP、MPI、Numba、Cython、OpenCL、CUDA 和 Julia 编程语言加速代码

  • 如何从数据集中估计概率密度

  • 如何在笔记本中开始使用 R 统计编程语言

  • 如何使用 scikit-learn 训练分类器或回归器

  • 如何在高维数据集中找到有趣的投影

  • 如何在图像中检测人脸

  • 如何模拟反应-扩散系统

  • 如何在路网中计算行程

本书的选择是介绍各种不同的主题,而不是深入探讨少数几种方法。目标是让你了解 Python 在数据科学领域令人难以置信的丰富能力。所有方法都在多种真实世界的例子上应用。

本书的每个配方不仅演示了如何应用某个方法,还解释了它如何以及为什么有效。理解这些方法背后的数学概念和思想,而不仅仅是盲目应用它们,是非常重要的。

此外,每个配方都附有许多参考资料,供有兴趣的读者深入了解。由于在线参考资料经常变动,它们将会在本书的网站上保持更新(ipython-books.github.io)。

本书涵盖的内容

本书分为两部分:

第一部分(第 1 到 6 章)涵盖了交互式数值计算、高性能计算和数据可视化中的高级方法。

第二部分(第 7 到 15 章)介绍了数据科学和数学建模中的标准方法。所有这些方法都应用于真实世界的数据。

第一部分 – 高性能交互式计算

第一章,使用 IPython 进行交互式计算简介,简要而深刻地介绍了如何使用 IPython 进行数据分析和数值计算。它不仅涵盖了 Python、NumPy、pandas 和 matplotlib 等常用包,还涉及了 IPython 的高级主题,如笔记本中的交互式小部件、自定义魔法命令、可配置的 IPython 扩展以及新的语言内核。

第二章,交互式计算中的最佳实践,详细讲解了编写可重复、高质量代码的最佳实践:任务自动化、使用 Git 进行版本控制、与 IPython 的工作流、使用 nose 进行单元测试、持续集成、调试以及其他相关主题。这些主题在计算研究和数据分析中的重要性不言而喻。

第三章,精通笔记本,涵盖了与 IPython 笔记本相关的高级主题,特别是笔记本格式、笔记本转换和 CSS/JavaScript 定制。自 IPython 2.0 以来推出的新交互式小部件也得到了广泛的介绍。这些技术使得在笔记本中进行数据分析比以往更加互动。

第四章,性能分析与优化,介绍了使代码更快、更高效的方法:Python 中的 CPU 和内存性能分析,NumPy 的高级优化技术(包括大数组操作),以及使用 HDF5 文件格式和 PyTables 库进行巨型数组的内存映射。这些技术对于大数据分析至关重要。

第五章,高性能计算,介绍了一些使代码高效的高级技术:使用 Numba 和 Cython 加速代码、通过 ctypes 将 C 库封装到 Python 中、使用 IPython、OpenMP 和 MPI 进行并行计算,以及使用 CUDA 和 OpenCL 进行图形处理单元(GPGPU)的通用计算。章节最后介绍了近年来的 Julia 语言,这种语言专为高性能数值计算而设计,并且可以在 IPython 笔记本中轻松使用。

第六章,高级可视化,介绍了一些在样式或编程接口方面超越 matplotlib 的数据可视化库。它还涵盖了在笔记本中使用 Bokeh、mpld3 和 D3.js 进行交互式可视化。章节最后介绍了 Vispy,一个利用图形处理单元(GPU)强大计算能力进行大数据高性能交互式可视化的库。

第二部分 – 数据科学与应用数学的标准方法

第七章,统计数据分析,介绍了获得数据洞察力的方法。它介绍了经典的频率学派和贝叶斯方法,用于假设检验、参数估计和非参数估计、模型推断等。章节中利用了 Python 的 pandas、SciPy、statsmodels 和 PyMC 等库。最后一个示例介绍了统计语言 R,可以轻松地在 IPython 笔记本中使用。

第八章,机器学习,讲解了从数据中学习和做出预测的方法。本章使用 scikit-learn Python 包,阐释了数据挖掘和机器学习中的基本概念,如监督学习和无监督学习、分类、回归、特征选择、特征提取、过拟合、正则化、交叉验证和网格搜索。本章涉及的算法包括逻辑回归、朴素贝叶斯、K 近邻、支持向量机、随机森林等。这些方法被应用到不同类型的数据集:数值数据、图像和文本。

第九章,数值优化,主要讲解如何最小化或最大化数学函数。这个话题在数据科学中无处不在,尤其是在统计学、机器学习和信号处理领域。本章通过 SciPy 展示了一些根求解、最小化和曲线拟合的常用方法。

第十章,信号处理,讲解如何从复杂且嘈杂的数据中提取相关信息。在运行统计和数据挖掘算法之前,有时需要先进行这些步骤。本章介绍了标准的信号处理方法,如傅里叶变换和数字滤波器。

第十一章,图像与音频处理,涵盖了图像和声音的信号处理方法。它介绍了图像过滤、分割、计算机视觉和人脸检测,使用 scikit-image 和 OpenCV。还介绍了音频处理和合成的方法。

第十二章,确定性动力学系统,描述了特定数据类型背后的动态过程。它演示了离散时间动力学系统的模拟技术,以及常微分方程和偏微分方程的模拟方法。

第十三章,随机动力学系统,描述了特定数据类型背后的动态随机过程。它演示了离散时间马尔可夫链、点过程和随机微分方程的模拟技术。

第十四章,图形、几何与地理信息系统,涵盖了图形、社交网络、道路网络、地图和地理数据的分析与可视化方法。

第十五章,符号与数值数学,介绍了 SymPy,这是一种将符号计算引入 Python 的计算机代数系统。本章最后介绍了 Sage,这是另一种基于 Python 的计算数学系统。

本书所需内容

你需要了解本书前一篇的内容,《学习 IPython 进行交互式计算与数据可视化》:Python 编程,IPython 控制台与笔记本,使用 NumPy 进行数值计算,利用 pandas 进行基本数据分析,以及使用 matplotlib 绘图。本书涉及高级科学编程主题,你需要熟悉科学 Python 生态系统。

在第二部分,你需要掌握微积分、线性代数和概率论的基础知识。这些章节介绍了数据科学和应用数学中的不同主题(统计学、机器学习、数值优化、信号处理、动力学系统、图论等)。如果你了解基本概念,如实值函数、积分、矩阵、向量空间、概率等,你将更好地理解这些内容。

安装 Python

安装 Python 有多种方式。我们强烈推荐免费的 Anaconda 发行版(store.continuum.io/cshop/anaconda/)。这个 Python 发行版包含了本书中大部分将要使用的包。它还包括一个强大的打包系统,名为 conda。书籍网站提供了安装 Anaconda 和运行代码示例的所有说明。你应该学习如何安装包(conda install packagename)以及如何使用 conda 创建多个 Python 环境。

本书的代码是为 Python 3 编写的(更准确地说,代码已在 Python 3.4.1、Anaconda 2.0.1、Windows 8.1 64 位环境下进行测试,尽管它在 Linux 和 Mac OS X 上也可以运行),但它同样适用于 Python 2.7。我们会在需要时提到任何兼容性问题。由于 NumPy 在大多数情况下都承担了繁重的计算任务,所以这些问题在本书中很少出现。NumPy 的接口在 Python 2 和 Python 3 之间没有变化。

如果你不确定应该使用哪个 Python 版本,选择 Python 3。只有在你确实需要的情况下才选择 Python 2(例如,如果你必须使用一个不支持 Python 3 的 Python 包,或者你的部分用户群体仍然使用 Python 2)。我们在第二章《交互式计算最佳实践》中详细讨论了这个问题。

使用 Anaconda 时,你可以通过 conda 环境同时安装 Python 2 和 Python 3。这样你就可以轻松运行本书中需要 Python 2 的几个食谱。

GitHub 仓库

本书配有主页和两个 GitHub 仓库:

主要 GitHub 仓库是你可以找到的地方:

  • 查找所有的代码示例,格式为 IPython 笔记本

  • 查找所有最新的参考文献

  • 查找最新的安装说明

  • 通过问题跟踪器报告勘误、不准确或错误

  • 通过 Pull Requests 提出修复建议

  • 通过 Pull Requests 添加笔记、评论或进一步的参考资料

  • 通过 Pull Requests 添加新食谱

在线参考文献列表是一个特别重要的资源。它包含了许多关于本书涵盖主题的教程、课程、书籍和视频链接。

你也可以通过我的网站(cyrille.rossant.net)和我的 Twitter 账号(@cyrillerossant)跟进本书的最新动态。

本书适合谁阅读

本书面向学生、研究人员、教师、工程师、数据科学家、分析师、记者、经济学家以及对数据分析和数值计算感兴趣的爱好者。

对于熟悉科学 Python 生态系统的读者,会发现许多资源可以帮助他们在 IPython 中进行高性能交互式计算时提升技能。

需要为领域特定应用实现算法的读者会喜欢这本书中对数据分析和应用数学各种主题的介绍。

对于刚接触 Python 数值计算的读者,应该先从本书的前传《Learning IPython for Interactive Computing and Data Visualization》入手,作者是Cyrille Rossant,由Packt Publishing出版,2013 年。第二版计划于 2015 年发布。

约定

本书中包含了多种文本样式,用以区分不同类型的信息。以下是这些样式的示例及其含义解释。

文字中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名如下所示:“笔记本可以通过%run notebook.ipynb在交互式会话中运行。”

代码块如下所示:

def do_complete(self, code, cursor_pos):
    return {'status': 'ok',
            'cursor_start': ...,
            'cursor_end': ...,
            'matches': [...]}

所有命令行输入或输出如下所示:

from IPython import embed
embed()

新术语重要词汇以粗体显示。例如,在屏幕上、菜单或对话框中的词语,通常呈现为这样:“最简单的选项是从笔记本仪表板中的Clusters选项卡启动它们。”

注意

警告或重要提示会以这种框的形式呈现。

提示

提示和技巧以这种方式呈现。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对本书的看法——您喜欢或不喜欢的内容。读者的反馈对我们开发更有价值的书籍至关重要,帮助我们提供真正对您有帮助的书籍。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书名。

如果您在某个主题领域具有专业知识,并且有兴趣撰写或参与书籍创作,请参考我们的作者指南,网址为:www.packtpub.com/authors

客户支持

现在您已经是 Packt 书籍的自豪拥有者,我们有许多资源帮助您最大限度地利用您的购买。

下载示例代码

您可以从自己账户的www.packtpub.com下载所有购买的 Packt 书籍的示例代码文件。如果您从其他地方购买本书,可以访问www.packtpub.com/support并注册,文件将直接通过电子邮件发送给您。

下载彩色图片

我们还为您提供了一份包含本书中截图/图表彩色图片的 PDF 文件。彩色图片有助于您更好地理解输出的变化。您可以通过以下链接下载该文件:www.packtpub.com/sites/default/files/downloads/4818OS_ColoredImages.pdf

勘误

虽然我们已尽一切努力确保内容的准确性,但错误仍然会发生。如果您在我们的书籍中发现错误——可能是文本或代码的错误——我们将非常感激您能将其报告给我们。通过这样做,您不仅能为其他读者避免困扰,还能帮助我们改进书籍的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata进行报告,选择您的书籍,点击勘误 提交 表单链接,并填写勘误详细信息。您的勘误一旦验证通过,我们将接受您的提交,并将勘误上传到我们的网站,或者添加到该书籍现有勘误列表中的勘误部分。任何现有勘误可以通过访问www.packtpub.com/support并选择您的书名来查看。

盗版

网络上盗版版权材料的问题在所有媒体中都很普遍。在 Packt,我们非常重视对版权和许可的保护。如果您在互联网上遇到任何形式的非法复制品,请立即提供相关地址或网站名称,以便我们采取相应措施。

如发现疑似盗版内容,请通过<copyright@packtpub.com>与我们联系,并提供相关链接。

感谢您的帮助,保护我们的作者权益,使我们能够为您提供有价值的内容。

问题

如果您在书籍的任何部分遇到问题,可以通过<questions@packtpub.com>与我们联系,我们将尽力解决。

第一章:IPython 互动计算概述

在本章中,我们将讨论以下主题:

  • 介绍 IPython 笔记本

  • 开始在 IPython 中进行探索性数据分析

  • 介绍 NumPy 中的多维数组以进行快速数组计算

  • 创建带有自定义魔法命令的 IPython 扩展

  • 掌握 IPython 的配置系统

  • 为 IPython 创建一个简单的内核

引言

本书面向熟悉 Python、IPython 和科学计算的中高级用户。在本章中,我们将简要回顾一下本书中将使用的基础工具:IPython、笔记本、pandas、NumPy 和 matplotlib。

在本章节中,我们将对 IPython 和 Python 科学计算栈进行广泛的概述,以支持高性能计算和数据科学。

什么是 IPython?

IPython是一个开源平台,用于互动和并行计算。它提供了强大的交互式命令行和基于浏览器的笔记本。笔记本将代码、文本、数学表达式、内联图形、交互式图表及其他丰富的媒体内容结合在一个可共享的 Web 文档中。该平台为互动科学计算和数据分析提供了理想的框架,IPython 已成为研究人员、数据科学家和教师的必备工具。

IPython 可以与 Python 编程语言一起使用,但该平台也支持其他多种语言,如 R、Julia、Haskell 或 Ruby。该项目的架构确实是与语言无关的,包含了消息协议和交互式客户端(包括基于浏览器的笔记本)。这些客户端与内核相连,后者实现了核心的交互式计算功能。因此,该平台对于使用 Python 以外语言的技术和科学社区也非常有用。

2014 年 7 月,Jupyter 项目由 IPython 开发者宣布。该项目将专注于 IPython 中与语言无关的部分(包括笔记本架构),而 IPython 这个名称将保留用于 Python 内核。在本书中,为了简便起见,我们将使用 IPython 这个术语来指代平台或 Python 内核。

Python 作为科学计算环境的简要历史回顾

Python 是一种高级通用编程语言,由 Guido van Rossum 于 1980 年代末期构思(其名称灵感来源于英国喜剧Monty Python's Flying Circus)。这种易于使用的语言是许多脚本程序的基础,这些程序将不同的软件组件(胶水语言)连接在一起。此外,Python 还自带一个极其丰富的标准库(内置电池理念),涵盖了字符串处理、互联网协议、操作系统接口等多个领域。

在 1990 年代末,Travis Oliphant 和其他人开始构建用于处理 Python 中的数值数据的高效工具:Numeric、Numarray,最终是NumPySciPy,实现了许多数值计算算法,也是建立在 NumPy 之上的。在 2000 年代初,John Hunter 创建了matplotlib,将科学图形带到了 Python。与此同时,Fernando Perez 创建了 IPython,以提高 Python 中的交互性和生产力。所有基本工具都在这里,将 Python 打造成一个出色的开源高性能框架,用于科学计算和数据分析。

值得注意的是,Python 作为科学计算平台是在原本不是为此目的设计的编程语言的基础上逐步构建起来的。这个事实可能解释了平台的一些细微不一致或弱点,但这并不妨碍它成为当今最受欢迎的科学计算开源框架之一。(您也可以参考cyrille.rossant.net/whats-wrong-with-scientific-python/。)

用于数值计算和数据分析的值得注意的竞争性开源平台包括 R(专注于统计)和 Julia(一个年轻的、高级的语言,专注于高性能和并行计算)。我们将在本书中简要介绍这两种语言,因为它们可以从 IPython 笔记本中使用。

在 2000 年代末,Wes McKinney 创建了用于操作和分析数值表和时间序列的pandas。与此同时,IPython 开发人员开始着手开发受数学软件如SageMapleMathematica启发的笔记本客户端。最终,2011 年 12 月发布的 IPython 0.12 引入了现在已经流行的基于 HTML 的笔记本。

2013 年,IPython 团队获得了 Sloan 基金会的资助和微软的捐赠,以支持笔记本的开发。2014 年初发布的 IPython 2.0 带来了许多改进和期待已久的功能。

IPython 2.0 有什么新功能?

这里是 IPython 2.0(接替 v1.1)带来的变化的简要总结:

  • 笔记本带有一个新的模态用户界面

    • 编辑模式下,我们可以通过输入代码或文本来编辑单元格。

    • 命令模式下,我们可以通过移动单元格、复制或删除它们、更改它们的类型等来编辑笔记本。在这种模式下,键盘映射到一组快捷键,让我们能够高效地执行笔记本和单元格操作。

  • 笔记本小部件是基于 JavaScript 的 GUI 小部件,可以与 Python 对象动态交互。这一重要功能极大地扩展了 IPython 笔记本的可能性。在笔记本中编写 Python 代码不再是与内核唯一可能的交互方式。JavaScript 小部件,更一般地说,任何基于 JavaScript 的交互元素,现在都可以实时与内核交互。

  • 现在,我们可以通过仪表板在不同的子文件夹中打开笔记本,并使用相同的服务器。一个 REST API 将本地 URI 映射到文件系统。

  • 现在,笔记本已被签名,以防止在打开笔记本时执行不受信任的代码。

  • 仪表板现在包含一个正在运行的标签,显示运行中的内核列表。

  • 提示框现在在按下Shift + Tab时显示,而不是Tab

  • 可以通过%run notebook.ipynb在交互式会话中运行笔记本。

  • 不推荐使用%pylab魔法命令,建议使用%matplotlib inline(将图形嵌入到笔记本中)和import matplotlib.pyplot as plt。主要原因是%pylab通过导入大量变量使交互命名空间变得杂乱无章。此外,它可能会影响笔记本的可重复性和可重用性。

  • Python 2.6 和 3.2 不再支持。IPython 现在需要 Python 2.7 或>=3.3。

IPython 3.0 和 4.0 的路线图

计划于 2014 年底/2015 年初发布的 IPython 3.0 和 4.0 应该有助于使用非 Python 内核,并为笔记本提供多用户功能。

参考资料

以下是一些参考资料:

介绍 IPython 笔记本

笔记本是 IPython 的旗舰功能。这个基于 Web 的交互式环境将代码、富文本、图像、视频、动画、数学公式和图表集成到一个文档中。这个现代化的工具是 Python 中高性能数值计算和数据科学的理想门户。整本书都是在笔记本中编写的,每个教程的代码都可以在本书的 GitHub 仓库中的笔记本里找到,地址是:github.com/ipython-books/cookbook-code

在本教程中,我们介绍了 IPython 及其笔记本。在准备工作中,我们还给出了有关安装 IPython 和 Python 科学计算栈的一般说明。

准备工作

本章中你将需要 Python、IPython、NumPy、pandas 和 matplotlib。与 SciPy 和 SymPy 一起,这些库构成了 Python 科学计算栈的核心(www.scipy.org/about.html)。

注意

你可以在本书的 GitHub 仓库 github.com/ipython-books/cookbook-code 上找到完整的详细安装说明。

我们这里只给出这些说明的总结;请参考上面的链接以获取更详细的更新信息。

如果你刚开始接触 Python 中的科学计算,最简单的选择是安装一个一体化的 Python 发行版。最常见的发行版有:

我们强烈推荐使用 Anaconda。这些发行版包含了你开始所需的一切。你还可以根据需要安装其他包。你可以在之前提到的链接中找到所有安装说明。

注意

本书假设你已经安装了 Anaconda。如果你使用其他发行版,可能无法获得我们的支持。

另外,如果你敢于挑战,可以手动安装 Python、IPython、NumPy、pandas 和 matplotlib。你可以在以下网站找到所有安装说明:

注意

Python 2 还是 Python 3?

尽管 Python 3 是目前最新版本,但许多人仍在使用 Python 2。Python 3 引入了一些不兼容的变更,这些变更减缓了它的普及。如果你刚刚开始进行科学计算,完全可以选择 Python 3。本书中的所有代码都是为 Python 3 编写的,但也可以在 Python 2 中运行。我们将在第二章中详细介绍这个问题,交互式计算中的最佳实践

一旦你安装了一个全功能的 Python 发行版(我们强烈推荐使用 Anaconda)或 Python 和所需的包,你就可以开始了!本书中的大多数示例都使用了 IPython 笔记本工具。这个工具让你能够通过浏览器访问 Python。我们在《学习 IPython 进行交互式计算与数据可视化》一书中介绍了笔记本的基础内容。你也可以在 IPython 的官网找到更多信息(ipython.org/ipython-doc/stable/notebook/index.html)。

要运行 IPython 笔记本服务器,请在终端(也叫命令提示符)中输入 ipython notebook。默认情况下,您的浏览器会自动打开,并加载 127.0.0.1:8888 地址。然后,你可以在仪表盘中创建一个新的笔记本,或者打开一个已有的笔记本。默认情况下,笔记本服务器会在当前目录下启动(即你执行命令的目录)。它会列出该目录下所有的笔记本(文件扩展名为 .ipynb)。

注意

在 Windows 上,你可以通过按下 Windows 键和 R,然后在提示符中输入 cmd,最后按 Enter 打开命令提示符。

如何做...

  1. 我们假设已经安装了带有 IPython 的 Python 发行版,并且现在处于 IPython 笔记本中。我们在一个单元格中输入以下命令,并按 Shift + Enter 进行计算:

    In [1]: print("Hello world!")
    Hello world!
    

    如何做...

    IPython 笔记本的截图

    笔记本包含一系列线性的单元格输出区域。一个单元格中包含 Python 代码,可能有一行或多行。代码的输出显示在对应的输出区域中。

  2. 现在,我们进行一个简单的算术操作:

    In [2]: 2+2
    Out[2]: 4
    

    操作的结果会显示在输出区域。让我们更具体地说明一下。输出区域不仅显示单元格中任何命令打印的文本,还会显示最后返回对象的文本表示。这里,最后返回的对象是 2+2 的结果,也就是 4

  3. 在下一个单元格中,我们可以通过 _(下划线)特殊变量来恢复上一个返回对象的值。实际上,将对象赋值给命名变量(如 myresult = 2+2)可能更为方便。

    In [3]: _ * 3
    Out[3]: 12
    
  4. IPython 不仅接受 Python 代码,还接受 shell 命令。这些命令由操作系统定义(主要是 Windows、Linux 和 Mac OS X)。我们需要在单元格中输入!,然后输入 shell 命令。在这里,假设是 Linux 或 Mac OS X 系统,我们可以获得当前目录中所有笔记本的列表:

    In [4]: !ls *.ipynb
    notebook1.ipynb  ...
    

    在 Windows 上,你应将 ls 替换为 dir

  5. IPython 附带了一些魔法命令库。这些命令是常见操作的便捷快捷方式,所有魔法命令都以 %(百分号字符)开头。我们可以通过 %lsmagic 获取所有魔法命令的列表:

    In [5]: %lsmagic
    Out[5]: Available line magics:
    %alias  %alias_magic  %autocall  %automagic  %autosave  %bookmark  %cd  %clear  %cls  %colors  %config  %connect_info  %copy  %ddir  %debug  %dhist  %dirs  %doctest_mode  %echo  %ed  %edit  %env  %gui  %hist  %history  %install_default_config  %install_ext  %install_profiles  %killbgscripts  %ldir  %less  %load  %load_ext  %loadpy  %logoff  %logon  %logstart  %logstate  %logstop  %ls  %lsmagic  %macro  %magic  %matplotlib  %mkdir  %more  %notebook  %page  %pastebin  %pdb  %pdef  %pdoc  %pfile  %pinfo  %pinfo2  %popd  %pprint  %precision  %profile  %prun  %psearch  %psource  %pushd  %pwd  %pycat  %pylab  %qtconsole  %quickref  %recall  %rehashx  %reload_ext  %ren  %rep  %rerun  %reset  %reset_selective  %rmdir  %run  %save  %sc  %store  %sx  %system  %tb  %time  %timeit  %unalias  %unload_ext  %who  %who_ls  %whos  %xdel  %xmode
    
    Available cell magics:
    %%!  %%HTML  %%SVG  %%bash  %%capture  %%cmd  %%debug  %%file  %%html  %%javascript  %%latex  %%perl  %%powershell  %%prun  %%pypy  %%python  %%python3  %%ruby  %%script  %%sh  %%svg  %%sx  %%system  %%time  %%timeit  %%writefile
    

    单元魔法命令以%%为前缀;它们涉及整个代码单元。

  6. 例如,%%writefile 单元魔法命令可以让我们轻松创建文本文件。此魔法命令接受文件名作为参数。单元中的所有剩余行都将直接写入该文本文件。在这里,我们创建一个文件test.txt并写入Hello world!

    In [6]: %%writefile test.txt
            Hello world!
    Writing test.txt
    In [7]: # Let's check what this file contains.
            with open('test.txt', 'r') as f:
                print(f.read())
    Hello world!
    
  7. 正如我们在%lsmagic的输出中看到的,IPython 中有许多魔法命令。我们可以通过在命令后添加?来获取有关任何命令的更多信息。例如,要获取有关%run魔法命令的帮助,我们可以输入%run?,如下所示:

    In [9]: %run?
    Type:        Magic function
    Namespace:   IPython internal
    ...
    Docstring:
    Run the named file inside IPython as a program.
    [full documentation of the magic command...]
    
  8. 我们已经介绍了 IPython 和笔记本的基础知识。现在让我们转向笔记本的丰富显示和交互功能。到目前为止,我们只创建了代码单元(包含代码)。IPython 支持其他类型的单元。在笔记本工具栏中,有一个下拉菜单可以选择单元的类型。在代码单元之后,最常见的单元类型是Markdown 单元

    Markdown 单元包含用Markdown格式化的丰富文本,这是一种流行的纯文本格式化语法。该格式支持普通文本、标题、粗体、斜体、超文本链接、图像、LaTeX(一种为数学适配的排版系统)中的数学方程式、代码、HTML 元素以及其他功能,如下所示:

    ### New paragraph 
    This is *rich* **text** with [links](http://ipython.org),
    equations:
    
    $$\hat{f}(\xi) = \int_{-\infty}^{+\infty} f(x)\,
                     \mathrm{e}^{-i \xi x}$$
    
    code with syntax highlighting: 
    ```python
    
    print("Hello world!")
    
    ```py 
    and images: 
    ![This is an image](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4e0cf8e076ec4e918316b8be68638c0f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771345740&x-signature=lCkUUDIHV1mU5HFq3WySzUOTrOc%3D)
    

    运行一个 Markdown 单元(例如,按 Shift + Enter)将显示输出,如下图所示:

    如何操作...

    在 IPython 笔记本中使用 Markdown 进行丰富文本格式化

    提示

    LaTeX 方程式使用 MathJax 库渲染。我们可以使用$...$输入内联方程式,使用$$...$$输入显示的方程式。我们还可以使用如 equationeqnarrayalign 等环境。这些功能对于科学用户非常有用。

    通过结合代码单元和 Markdown 单元,我们可以创建一个独立的交互式文档,结合计算(代码)、文本和图形。

  9. IPython 还带有一个复杂的显示系统,允许我们在笔记本中插入丰富的网页元素。在这里,我们展示如何在笔记本中添加 HTML、SVG可缩放矢量图形)甚至 YouTube 视频。

    首先,我们需要导入一些类:

    In [11]: from IPython.display import HTML, SVG, YouTubeVideo
    

    我们使用 Python 动态创建 HTML 表格,并将其显示在笔记本中:

    In [12]: HTML('''
             <table style="border: 2px solid black;">
             ''' + 
             ''.join(['<tr>' + 
                      ''.join(['<td>{row},{col}</td>'.format(
                                     row=row, col=col
                                     ) for col in range(5)]) +
                      '</tr>' for row in range(5)]) +
             '''
             </table>
             ''')
    

    如何操作...

    笔记本中的 HTML 表格

    类似地,我们可以动态创建 SVG 图形:

    In [13]: SVG('''<svg width="600" height="80">''' + 
             ''.join(['''<circle cx="{x}" cy="{y}" r="{r}"
                                 fill="red"
                                 stroke-width="2"
                                 stroke="black">
                         </circle>'''.format(x=(30+3*i)*(10-i),
                                             y=30,
                                             r=3.*float(i)
                                             ) for i in range(10)]) + 
             '''</svg>''')
    

    如何做到这一点...

    笔记本中的 SVG

    最后,我们通过将 YouTube 视频的标识符传递给 YoutubeVideo 来显示 YouTube 视频:

    In [14]: YouTubeVideo('j9YpkSX7NNM')
    

    如何做到这一点...

    笔记本中的 YouTube

  10. 现在,我们展示了 IPython 2.0+ 中的最新交互式功能,即 JavaScript 小部件。在这里,我们创建了一个下拉菜单来选择视频:

    In [15]: from collections import OrderedDict
             from IPython.display import (display,
                                          clear_output,
                                          YouTubeVideo)
             from IPython.html.widgets import DropdownWidget
    In [16]: # We create a DropdownWidget, with a dictionary
             # containing the keys (video name) and the values
             # (Youtube identifier) of every menu item.
             dw = DropdownWidget(values=OrderedDict([
                               ('SciPy 2012', 'iwVvqwLDsJo'),
                               ('PyCon 2012', '2G5YTlheCbw'),
                               ('SciPy 2013', 'j9YpkSX7NNM')]
                                                    )
                                 )
    
             # We create a callback function that displays the 
             # requested Youtube video.
             def on_value_change(name, val):
                 clear_output()
                 display(YouTubeVideo(val))
    
             # Every time the user selects an item, the 
             # function `on_value_change` is called, and the 
             # `val` argument contains the value of the selected 
             # item.
             dw.on_trait_change(on_value_change, 'value')
    
             # We choose a default value.
             dw.value = dw.values['SciPy 2013']
    
             # Finally, we display the widget.
             display(dw)
    

    如何做到这一点...

    笔记本中的交互式小部件

IPython 2.0 的交互式功能为笔记本带来了全新的维度,我们可以期待未来会有更多的发展。

还有更多……

笔记本以结构化文本文件(JSON 格式)保存,这使得它们易于共享。以下是一个简单笔记本的内容:

{
 "metadata": {
  "name": ""
 },
 "nbformat": 3,
 "nbformat_minor": 0,
 "worksheets": [
  {
   "cells": [
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "print(\"Hello World!\")"
     ],
     "language": "python",
     "metadata": {},
     "outputs": [
      {
       "output_type": "stream",
       "stream": "stdout",
       "text": [
        "Hello World!\n"
       ]
      }
     ],
     "prompt_number": 1
    }
   ],
   "metadata": {}
  }
 ]
}

IPython 附带了一个特殊工具 nbconvert,它将笔记本转换为其他格式,如 HTML 和 PDF (ipython.org/ipython-doc/stable/notebook/index.html)。

另一个在线工具 nbviewer 允许我们直接在浏览器中呈现公开可用的笔记本,访问地址为 nbviewer.ipython.org

我们将在后续章节中讨论这些可能性,特别是在 第三章,精通笔记本

这里是关于笔记本的一些参考资料:

另见

  • 在 IPython 中开始数据探索性分析 配方

在 IPython 中开始探索性数据分析

在本配方中,我们将介绍 IPython 在数据分析中的应用。大部分内容已在 学习 IPython 进行交互式计算和数据可视化 一书中涵盖,但我们将在这里回顾基础知识。

我们将下载并分析关于蒙特利尔自行车轨道的出勤数据集。这个示例主要受到 Julia Evans 的演讲启发(可以在 nbviewer.ipython.org/github/jvns/talks/blob/master/mtlpy35/pistes-cyclables.ipynb 中查看)。具体来说,我们将介绍以下内容:

  • 使用 pandas 进行数据处理

  • 使用 matplotlib 进行数据可视化

  • 使用 IPython 2.0+ 的交互式小部件

如何操作...

  1. 第一步是导入我们在此示例中将要使用的科学计算包,即 NumPy、pandas 和 matplotlib。我们还指示 matplotlib 将图形渲染为 notebook 中的内联图像:

    In [1]: import numpy as np
            import pandas as pd
            import matplotlib.pyplot as plt
            %matplotlib inline
    
  2. 现在,我们创建一个新的 Python 变量 url,它包含一个 CSV逗号分隔值)数据文件的地址。该标准文本格式用于存储表格数据:

    In [2]: url = "http://donnees.ville.montreal.qc.ca/storage/f/2014-01-20T20%3A48%3A50.296Z/2013.csv"
    
  3. pandas 定义了一个 read_csv() 函数,可以读取任何 CSV 文件。在这里,我们传入文件的 URL。pandas 会自动下载并解析文件,然后返回一个 DataFrame 对象。我们需要指定一些选项,以确保日期能够正确解析:

    In [3]: df = pd.read_csv(url, index_col='Date',
                             parse_dates=True, dayfirst=True)
    
  4. df 变量包含一个 DataFrame 对象,这是 pandas 特定的数据结构,包含二维表格数据。head(n) 方法显示表格的前 n 行。在 notebook 中,pandas 会以 HTML 表格的形式显示 DataFrame 对象,如以下截图所示:

    In [4]: df.head(2)
    

    如何操作...

    DataFrame 的前几行

    在这里,每一行包含每一天全年在城市各个轨道上的自行车数量。

  5. 我们可以通过 describe() 方法获得表格的一些摘要统计信息:

    In [5]: df.describe()
    

    如何操作...

    DataFrame 的摘要统计信息

  6. 让我们展示一些图形。我们将绘制两条轨道的每日出勤数据。首先,选择两列数据,Berri1PierDup。然后,我们调用 plot() 方法:

    In [6]: df[['Berri1', 'PierDup']].plot()
    

    如何操作...

  7. 现在,我们进入稍微复杂一点的分析。我们将查看所有轨道的出勤情况与星期几的关系。我们可以通过 pandas 很容易地获取星期几:DataFrame 对象的 index 属性包含表格中所有行的日期。这个索引有一些与日期相关的属性,包括 weekday

    In [7]: df.index.weekday
    Out[7]: array([1, 2, 3, 4, 5, 6, 0, 1, 2, ..., 0, 1, 2])
    

    然而,我们希望将 0 到 6 之间的数字替换为星期几的名称(例如星期一、星期二等)。这可以很容易地做到。首先,我们创建一个 days 数组,包含所有星期几的名称。然后,我们通过 df.index.weekday 对其进行索引。这个操作将索引中的每个整数替换为 days 中对应的名称。第一个元素是 Monday,索引为 0,因此 df.index.weekday 中的每个 0 会被替换为 Monday,以此类推。我们将这个新的索引赋给 DataFrame 中的新列 Weekday

    In [8]: days = np.array(['Monday', 'Tuesday', 'Wednesday', 
                             'Thursday', 'Friday', 'Saturday', 
                             'Sunday'])
            df['Weekday'] = days[df.index.weekday]
    
  8. 为了按星期几获取出勤情况,我们需要按星期几对表格元素进行分组。groupby() 方法可以帮助我们做到这一点。分组后,我们可以对每个组中的所有行进行求和:

    In [9]: df_week = df.groupby('Weekday').sum()
    In [10]: df_week
    

    如何实现...

    使用 pandas 对数据进行分组

  9. 我们现在可以在图形中显示这些信息了。我们首先需要使用 ix(索引操作)按星期几重新排序表格。然后,我们绘制表格,指定线条宽度:

    In [11]: df_week.ix[days].plot(lw=3)
             plt.ylim(0);  # Set the bottom axis to 0.
    

    如何实现...

  10. 最后,让我们说明 IPython 2.0 中新的交互式功能。我们将绘制一个平滑的轨迹出勤情况与时间的关系图(滚动平均)。其思路是计算某一天邻近区域的平均值。邻域越大,曲线越平滑。我们将在笔记本中创建一个交互式滑块,以实时调整这个参数。我们只需要在绘图函数上方添加 @interact 装饰器:

    In [12]: from IPython.html.widgets import interact
             @interact
             def plot(n=(1, 30)):
                 pd.rolling_mean(df['Berri1'], n).dropna().plot()
                 plt.ylim(0, 8000)
                 plt.show()
    

    如何实现...

    笔记本中的交互式小部件

还有更多内容...

pandas 是加载和操作数据集的正确工具。对于更高级的分析(信号处理、统计和数学建模),通常需要其他工具和方法。我们将在本书的第二部分中讲解这些步骤,从第七章开始,统计数据分析

下面是一些关于使用 pandas 进行数据操作的参考资料:

  • 《学习 IPython 进行互动计算和数据可视化》Packt Publishing,我们之前的书籍

  • Python 数据分析O'Reilly Media,由 pandas 的创建者 Wes McKinney 编写

  • pandas 的文档可通过 pandas.pydata.org/pandas-docs/stable/ 获取

另见

  • 介绍 NumPy 中的多维数组用于快速数组计算 示例

介绍 NumPy 中的多维数组用于快速数组计算

NumPy 是科学 Python 生态系统的主要基础。这个库提供了一种用于高性能数值计算的特定数据结构:多维数组。NumPy 背后的原理是:Python 作为一种高级动态语言,使用起来更方便,但比起 C 这样的低级语言要慢。NumPy 在 C 中实现了多维数组结构,并提供了一个便捷的 Python 接口,从而将高性能与易用性结合在一起。许多 Python 库都使用了 NumPy。例如,pandas 就是建立在 NumPy 之上的。

在这个示例中,我们将说明多维数组的基本概念。有关该主题的更全面的讲解可以在*《学习 IPython 进行互动计算和数据可视化》*一书中找到。

如何实现...

  1. 让我们导入内置的 random Python 模块和 NumPy:

    In [1]: import random
            import numpy as np
    

    我们使用 %precision 魔法命令(在 IPython 中定义)来仅显示 Python 输出中的三位小数。这只是为了减少输出文本中的数字位数。

    In [2]: %precision 3
    Out[2]: u'%.3f'
    
  2. 我们生成两个 Python 列表,xy,每个列表包含 100 万个介于 0 和 1 之间的随机数:

    In [3]: n = 1000000
            x = [random.random() for _ in range(n)]
            y = [random.random() for _ in range(n)]
    In [4]: x[:3], y[:3]
    Out[4]: ([0.996, 0.846, 0.202], [0.352, 0.435, 0.531])
    
  3. 让我们计算所有这些数字的逐元素和:x 的第一个元素加上 y 的第一个元素,以此类推。我们使用 for 循环在列表推导中进行操作:

    In [5]: z = [x[i] + y[i] for i in range(n)]
            z[:3]
    Out[5]: [1.349, 1.282, 0.733]
    
  4. 这个计算需要多长时间?IPython 定义了一个方便的 %timeit 魔法命令,可以快速评估单个语句所花费的时间:

    In [6]: %timeit [x[i] + y[i] for i in range(n)]
    1 loops, best of 3: 273 ms per loop
    
  5. 现在,我们将使用 NumPy 执行相同的操作。NumPy 处理多维数组,因此我们需要将列表转换为数组。np.array() 函数正好完成了这个操作:

    In [7]: xa = np.array(x)
            ya = np.array(y)
    In [8]: xa[:3]
    Out[8]: array([ 0.996,  0.846,  0.202])
    

    xaya 数组包含与我们原始列表 xy 相同的数字。这些列表是 list 内建类的实例,而我们的数组是 ndarray NumPy 类的实例。这些类型在 Python 和 NumPy 中的实现方式非常不同。在这个例子中,我们将看到,使用数组代替列表可以大幅提高性能。

  6. 现在,为了计算这些数组的逐元素和,我们不再需要做 for 循环。在 NumPy 中,添加两个数组意味着逐个元素地将数组的元素相加。这是线性代数中的标准数学符号(对向量和矩阵的操作):

    In [9]: za = xa + ya
            za[:3]
    Out[9]: array([ 1.349,  1.282,  0.733])
    

    我们看到,z 列表和 za 数组包含相同的元素(xy 中数字的和)。

  7. 让我们将这个 NumPy 操作的性能与原生 Python 循环进行比较:

    In [10]: %timeit xa + ya
    100 loops, best of 3: 10.7 ms per loop
    

    我们观察到,在 NumPy 中,这个操作比在纯 Python 中快了一个数量级以上!

  8. 现在,我们将计算其他内容:xxa 中所有元素的和。尽管这不是逐元素操作,但 NumPy 在这里仍然非常高效。纯 Python 版本使用内建的 sum() 函数对可迭代对象求和,而 NumPy 版本使用 np.sum() 函数对 NumPy 数组求和:

    In [11]: %timeit sum(x)  # pure Python
             %timeit np.sum(xa)  # NumPy
    100 loops, best of 3: 17.1 ms per loop
    1000 loops, best of 3: 2.01 ms per loop
    

    我们在这里也观察到令人印象深刻的加速。

  9. 最后,让我们执行最后一个操作:计算我们两个列表中任意一对数字之间的算术距离(我们只考虑前 1000 个元素,以保持计算时间合理)。首先,我们用两个嵌套的 for 循环在纯 Python 中实现这个操作:

    In [12]: d = [abs(x[i] - y[j]) 
                  for i in range(1000) for j in range(1000)]
    In [13]: d[:3]
    Out[13]: [0.230, 0.037, 0.549]
    
  10. 现在,我们使用 NumPy 实现,提出两个稍微高级的概念。首先,我们考虑一个二维数组(或矩阵)。这就是我们如何处理两个索引,ij。其次,我们使用广播在 2D 数组和 1D 数组之间执行操作。我们将在*它是如何工作的...*部分提供更多细节。

    In [14]: da = np.abs(xa[:1000,None] - ya[:1000])
    In [15]: da
    Out[15]: array([[ 0.23 ,  0.037,  ...,  0.542,  0.323,  0.473],
                     ...,
                    [ 0.511,  0.319,  ...,  0.261,  0.042,  0.192]])
    In [16]: %timeit [abs(x[i] - y[j]) 
                      for i in range(1000) for j in range(1000)]
             %timeit np.abs(xa[:1000,None] - ya[:1000])
    1 loops, best of 3: 292 ms per loop
    100 loops, best of 3: 18.4 ms per loop
    

    在这里,我们再次观察到显著的加速。

它是如何工作的...

NumPy 数组是一个均匀的数据块,按照多维有限网格组织。数组中的所有元素都具有相同的数据类型,也叫做 dtype(整数、浮动小数等)。数组的形状是一个 n 元组,表示每个维度的大小。

一维数组是一个向量;它的形状只是组件的数量。

二维数组是一个矩阵;它的形状是*(行数,列数)*。

下图展示了一个包含 24 个元素的 3D(3, 4, 2)数组的结构:

如何运作...

一个 NumPy 数组

Python 中的切片语法与 NumPy 中的数组索引非常匹配。此外,我们可以通过在索引中使用 Nonenp.newaxis 为现有数组添加一个额外的维度。我们在之前的例子中使用了这一技巧。

可以对具有相同形状的 NumPy 数组执行逐元素的算术操作。然而,广播机制放宽了这一条件,它允许在特定条件下对具有不同形状的数组进行操作。特别是,当一个数组的维度比另一个数组少时,它可以被虚拟地拉伸以匹配另一个数组的维度。这就是我们如何计算 xaya 中任何两个元素之间的成对距离的方式。

数组操作为什么比 Python 循环快得多?有几个原因,我们将在第四章,性能分析与优化中详细回顾这些原因。我们在这里可以先说:

  • 在 NumPy 中,数组操作是通过 C 循环而非 Python 循环实现的。由于 Python 是解释型语言且具有动态类型的特性,它通常比 C 慢。

  • NumPy 数组中的数据存储在 RAM 中一个连续的内存块中。这一特性使得 CPU 周期和缓存的使用更为高效。

还有更多内容...

显然,这个话题还有很多内容可以讨论。我们之前的书籍 Learning IPython for Interactive Computing and Data Visualization 包含了更多关于基本数组操作的细节。本书将经常使用数组数据结构。特别地,第四章,性能分析与优化,涵盖了使用 NumPy 数组的高级技巧。

这里有一些参考文献:

另见

  • 开始进行 IPython 中的探索性数据分析 的教程

  • 第四章中的理解 NumPy 的内部机制以避免不必要的数组复制性能分析与优化

创建带有自定义魔法命令的 IPython 扩展

尽管 IPython 提供了各种各样的魔法命令,但在某些情况下,我们需要在新的魔法命令中实现自定义功能。在本教程中,我们将展示如何创建行魔法和单元魔法,并且如何将它们集成到 IPython 扩展中。

如何做...

  1. 让我们从 IPython 魔法系统中导入一些函数:

    In [1]: from IPython.core.magic import (register_line_magic, 
                                            register_cell_magic)
    
  2. 定义一个新的行魔法非常简单。首先,我们创建一个接受行内容的函数(不包括前面的 %-前缀名称)。这个函数的名称就是魔法的名称。然后,我们使用@register_line_magic装饰这个函数:

    In [2]: @register_line_magic
            def hello(line):
                if line == 'french':
                    print("Salut tout le monde!")
                else:
                    print("Hello world!")
    In [3]: %hello
    Hello world!
    In [4]: %hello french
    Salut tout le monde!
    
  3. 让我们创建一个稍微有用一些的%%csv单元魔法,它解析 CSV 字符串并返回一个 pandas DataFrame 对象。这次,函数的参数是紧随%%csv之后的字符(位于第一行)和单元格的内容(从单元格的第二行到最后一行)。

    In [5]: import pandas as pd
            #from StringIO import StringIO  # Python 2
            from io import StringIO  # Python 3
    
            @register_cell_magic
            def csv(line, cell):
                # We create a string buffer containing the
                # contents of the cell.
                sio = StringIO(cell)
                # We use pandas' read_csv function to parse
                # the CSV string.
                return pd.read_csv(sio)
    In [6]: %%csv
            col1,col2,col3
            0,1,2
            3,4,5
            7,8,9
    Out[6]:
        col1  col2  col3
    0     0     1     2
    1     3     4     5
    2     7     8     9
    

    我们可以通过 _ 访问返回的对象。

    In [7]: df = _
            df.describe()
    Out[7]:
               col1      col2      col3
    count  3.000000  3.000000  3.000000
    mean   3.333333  4.333333  5.333333
    ...
    min    0.000000  1.000000  2.000000
    max    7.000000  8.000000  9.000000
    
  4. 我们描述的方法在交互式会话中非常有用。如果我们想在多个笔记本中使用相同的魔法,或者想要分发它,那么我们需要创建一个实现自定义魔法命令的IPython 扩展。第一步是创建一个 Python 脚本(这里是csvmagic.py),它实现了魔法功能。我们还需要定义一个特殊的函数load_ipython_extension(ipython)

    In [8]: %%writefile csvmagic.py
            import pandas as pd
            #from StringIO import StringIO  # Python 2
            from io import StringIO  # Python 3
            def csv(line, cell):
                sio = StringIO(cell)
                return pd.read_csv(sio)
    
            def load_ipython_extension(ipython):
                """This function is called when the extension 
                is loaded. It accepts an 
                IPython InteractiveShell instance.
                We can register the magic with the
                `register_magic_function` method."""
                ipython.register_magic_function(csv, 'cell')
    Overwriting csvmagic.py
    
  5. 一旦扩展模块创建完成,我们需要将其导入到 IPython 会话中。我们可以通过 %load_ext 魔法命令做到这一点。在这里,加载我们的扩展立即在交互式 shell 中注册我们的%%csv魔法函数:

    In [9]: %load_ext csvmagic
    In [10]: %%csv
             col1,col2,col3
             0,1,2
             3,4,5
             7,8,9
    Out[10]:
       col1  col2  col3
    0     0     1     2
    1     3     4     5
    2     7     8     9
    

它是如何工作的...

IPython 扩展是一个 Python 模块,它实现了顶级的load_ipython_extension(ipython)函数。当 %load_ext 魔法命令被调用时,模块会被加载,且会调用load_ipython_extension(ipython)函数。这个函数会将当前的InteractiveShell实例作为参数传入。这个对象实现了几个方法,我们可以用来与当前的 IPython 会话进行交互。

InteractiveShell 类

一个交互式 IPython 会话由InteractiveShell类的(单例)实例表示。这个对象处理历史记录、交互式命名空间以及会话中大多数可用功能。

在交互式 shell 中,我们可以通过get_ipython()函数获取当前的InteractiveShell实例。

InteractiveShell的所有方法列表可以在参考 API 中找到(请参见本教程末尾的链接)。以下是最重要的属性和方法:

  • user_ns: 用户命名空间(一个字典)。

  • push(): 推送(或注入)Python 变量到交互命名空间中。

  • ev(): 评估用户命名空间中的 Python 表达式。

  • ex(): 执行用户命名空间中的 Python 语句。

  • run_cell(): 运行一个单元格(以字符串形式给出),可能包含 IPython 魔法命令。

  • safe_execfile(): 安全地执行一个 Python 脚本。

  • system(): 执行一个系统命令。

  • write(): 写入一个字符串到默认输出。

  • write_err(): 写入一个字符串到默认的错误输出。

  • register_magic_function(): 注册一个独立的函数作为 IPython魔法函数。我们在本食谱中使用了此方法。

加载扩展

使用%load_ext时,Python 扩展模块需要是可导入的。这里,我们的模块位于当前目录。在其他情况下,它必须在 Python 路径中。它还可以存储在~\.ipython\extensions中,系统会自动将其加入 Python 路径。

为了确保我们的魔法命令在 IPython 配置文件中自动定义,我们可以指示 IPython 在启动新的交互式 Shell 时自动加载我们的扩展。为此,我们需要打开~/.ipython/profile_default/ipython_config.py文件,并将'csvmagic'加入到c.InteractiveShellApp.extensions列表中。csvmagic模块需要是可导入的。通常会创建一个Python 包,该包实现 IPython 扩展,并定义自定义魔法命令。

还有更多...

存在许多第三方扩展和魔法命令,特别是cythonmagicoctavemagicrmagic,它们都允许我们在单元格中插入非 Python 代码。例如,使用cythonmagic,我们可以在单元格中创建一个 Cython 函数,并在笔记本的其余部分中导入它。

这里有一些参考资料:

另请参见

  • 掌握 IPython 的配置系统食谱

掌握 IPython 的配置系统

IPython 实现了一个真正强大的配置系统。这个系统贯穿整个项目,但也可以被 IPython 扩展使用,甚至可以在全新的应用程序中使用。

在本食谱中,我们展示了如何使用这个系统来编写一个可配置的 IPython 扩展。我们将创建一个简单的魔法命令来显示随机数。这个魔法命令带有可配置的参数,用户可以在他们的 IPython 配置文件中进行设置。

如何做到这一点...

  1. 我们在 random_magics.py 文件中创建了一个 IPython 扩展。让我们先导入一些对象。

    注意

    确保将步骤 1-5 中的代码放在一个名为 random_magics.py 的外部文本文件中,而不是笔记本的输入中!

    from IPython.utils.traitlets import Int, Float, Unicode, Bool
    from IPython.core.magic import (Magics, magics_class, line_magic)
    import numpy as np
    
  2. 我们创建了一个从 Magics 派生的 RandomMagics 类。这个类包含一些可配置参数:

    @magics_class
    class RandomMagics(Magics):
        text = Unicode(u'{n}', config=True)
        max = Int(1000, config=True)
        seed = Int(0, config=True)
    
  3. 我们需要调用父类的构造函数。然后,我们用一个种子初始化一个随机数生成器:

        def __init__(self, shell):
            super(RandomMagics, self).__init__(shell)
            self._rng = np.random.RandomState(self.seed or None)
    
  4. 我们创建了一个%random行魔法,它显示一个随机数:

        @line_magic
        def random(self, line):
            return self.text.format(n=self._rng.randint(self.max))
    
  5. 最后,当扩展加载时,我们注册这个魔法:

    def load_ipython_extension(ipython):
        ipython.register_magics(RandomMagics)
    
  6. 让我们在笔记本中测试我们的扩展:

    In [1]: %load_ext random_magics
    In [2]: %random
    Out[2]: '635'
    In [3]: %random
    Out[3]: '47'
    
  7. 我们的魔法命令有一些可配置参数。这些变量是用户在 IPython 配置文件中或启动 IPython 时在控制台中配置的。要在终端中配置这些变量,我们可以在系统 shell 中输入以下命令:

    ipython --RandomMagics.text='Your number is {n}.' --RandomMagics.max=10 --RandomMagics.seed=1
    
    

    在此会话中,我们得到以下行为:

    In [1]: %load_ext random_magics
    In [2]: %random
    Out[2]: u'Your number is 5.'
    In [3]: %random
    Out[3]: u'Your number is 8.'
    
  8. 要在 IPython 配置文件中配置变量,我们必须打开 ~/.ipython/profile_default/ipython_config.py 文件,并添加以下行:

    c.RandomMagics.text = 'random {n}'
    

    启动 IPython 后,我们得到以下行为:

    In [4]: %random
    Out[4]: 'random 652'
    

它是如何工作的...

IPython 的配置系统定义了几个概念:

  • 用户配置文件 是一组特定于用户的参数、日志和命令历史记录。用户在处理不同项目时可以有不同的配置文件。一个 xxx 配置文件存储在 ~/.ipython/profile_xxx 中,其中 ~ 是用户的主目录。

    • 在 Linux 上,路径通常是/home/yourname/.ipython/profile_xxx

    • 在 Windows 上,路径通常是 C:\Users\YourName\.ipython\profile_xxx

  • 配置对象,或者说 Config,是一个特殊的 Python 字典,包含键值对。Config 类继承自 Python 的 dict

  • HasTraits 类是一个可以拥有特殊 trait 属性的类。Traits 是复杂的 Python 属性,具有特定类型和默认值。此外,当 trait 的值发生变化时,回调函数会自动且透明地被调用。这个机制允许一个类在 trait 属性发生变化时得到通知。

  • Configurable 类是所有希望受益于配置系统的类的基类。一个 Configurable 类可以拥有可配置的属性。这些属性在类定义中直接指定了默认值。Configurable 类的主要特点是其 trait 的默认值可以通过配置文件逐个类地被覆盖。然后,Configurables 的实例可以随意更改这些值。

  • 配置文件 是一个包含 Configurable 类参数的 Python 或 JSON 文件。

Configurable 类和配置文件支持继承模型。一个 Configurable 类可以从另一个 Configurable 类派生并重写其参数。类似地,一个配置文件可以被包含在另一个文件中。

配置项

这里是一个简单的 Configurable 类的示例:

from IPython.config.configurable import Configurable
from IPython.utils.traitlets import Float

class MyConfigurable(Configurable):
    myvariable = Float(100.0, config=True)

默认情况下,MyConfigurable 类的实例将其 myvariable 属性设置为 100。现在,假设我们的 IPython 配置文件包含以下几行:

c = get_config()
c.MyConfigurable.myvariable = 123.

然后,myvariable 属性将默认为 123。实例化后可以自由更改此默认值。

get_config() 函数是一个特殊的函数,可以在任何配置文件中使用。

此外,Configurable 参数可以在命令行界面中指定,正如我们在本配方中所看到的那样。

这个配置系统被所有 IPython 应用程序使用(特别是 consoleqtconsolenotebook)。这些应用程序有许多可配置的属性。你将在配置文件中找到这些属性的列表。

魔法命令

Magics 类继承自 Configurable 类,可以包含可配置的属性。此外,可以通过 @line_magic@cell_magic 装饰的方法来定义魔法命令。与前面的配方中使用函数魔法不同,定义类魔法的优势在于我们可以在多个魔法调用之间保持状态(因为我们使用的是类而不是函数)。

还有更多内容...

这里有一些参考资料:

参见

  • 创建带有自定义魔法命令的 IPython 扩展 配方

为 IPython 创建一个简单的内核

为 IPython 开发的架构,以及将成为 Project Jupyter 核心的架构,正在变得越来越独立于语言。客户端与内核之间的解耦使得可以用任何语言编写内核。客户端通过基于套接字的消息协议与内核通信。因此,可以用任何支持套接字的语言编写内核。

然而,消息协议是复杂的。从头编写一个新的内核并不简单。幸运的是,IPython 3.0 提供了一个轻量级的内核语言接口,可以用 Python 包装。

此接口还可用于在 IPython 笔记本(或其他客户端应用程序,如控制台)中创建完全定制的体验。通常,Python 代码必须在每个代码单元中编写;但是,我们可以为任何领域特定语言编写一个内核。我们只需编写一个接受代码字符串作为输入(代码单元的内容)的 Python 函数,并发送文本或丰富数据作为输出。我们还可以轻松实现代码完成和代码检查。

我们可以想象许多有趣的交互式应用程序,远远超出了 IPython 最初用例的范围。这些应用程序对于非程序员终端用户(如高中学生)可能特别有用。

在此配方中,我们将创建一个简单的图形计算器。计算器透明地由 NumPy 和 matplotlib 支持。我们只需在代码单元中编写函数,如 y = f(x),即可获取这些函数的图形。

准备工作

此配方已在 IPython 3.0 开发版本上进行了测试。它应该可以在 IPython 3.0 最终版本上无或最小更改地运行。我们将所有有关包装器内核和消息协议的引用都放在此配方的结尾。

如何操作……

注意

警告:此配方仅适用于 IPython >= 3.0!

  1. 首先,我们创建一个 plotkernel.py 文件。该文件将包含我们自定义内核的实现。让我们导入一些模块:

    注意

    请确保将步骤 1-6 的代码放在名为 plotkernel.py 的外部文本文件中,而不是笔记本的输入中!

    from IPython.kernel.zmq.kernelbase import Kernel
    import numpy as np
    import matplotlib.pyplot as plt
    from io import BytesIO
    import urllib, base64
    
  2. 我们编写一个函数,返回 matplotlib 图形的 base64 编码的 PNG 表示:

    def _to_png(fig):
        """Return a base64-encoded PNG from a 
        matplotlib figure."""
        imgdata = BytesIO()
        fig.savefig(imgdata, format='png')
        imgdata.seek(0)
        return urllib.parse.quote(
            base64.b64encode(imgdata.getvalue()))
    
  3. 现在,我们编写一个函数,解析具有 y = f(x) 形式的代码字符串,并返回一个 NumPy 函数。这里,f 是一个可以使用 NumPy 函数的任意 Python 表达式:

    _numpy_namespace = {n: getattr(np, n) 
                        for n in dir(np)}
    def _parse_function(code):
        """Return a NumPy function from a string 'y=f(x)'."""
        return lambda x: eval(code.split('=')[1].strip(),
                              _numpy_namespace, {'x': x})
    
  4. 对于我们的新包装器内核,我们创建一个派生自 Kernel 的类。我们需要提供一些元数据字段:

    class PlotKernel(Kernel):
        implementation = 'Plot'
        implementation_version = '1.0'
        language = 'python'  # will be used for
                             # syntax highlighting
        language_version = ''
        banner = "Simple plotting"
    
  5. 在此类中,我们实现了一个 do_execute() 方法,接受代码作为输入并向客户端发送响应:

    def do_execute(self, code, silent,
                   store_history=True,
                   user_expressions=None,
                   allow_stdin=False):
    
        # We create the plot with matplotlib.
        fig = plt.figure(figsize=(6,4), dpi=100)
        x = np.linspace(-5., 5., 200)
        functions = code.split('\n')
        for fun in functions:
            f = _parse_function(fun)
            y = f(x)
            plt.plot(x, y)
        plt.xlim(-5, 5)
    
        # We create a PNG out of this plot.
        png = _to_png(fig)
    
        if not silent:
            # We send the standard output to the client.
            self.send_response(self.iopub_socket,
                'stream', {
                    'name': 'stdout', 
                    'data': 'Plotting {n} function(s)'. \
                                format(n=len(functions))})
    
            # We prepare the response with our rich data
            # (the plot).
            content = {
                'source': 'kernel',
    
                # This dictionary may contain different
                # MIME representations of the output.
                'data': {
                    'image/png': png
                },
    
                # We can specify the image size
                # in the metadata field.
                'metadata' : {
                      'image/png' : {
                        'width': 600,
                        'height': 400
                      }
                    }
            }        
    
            # We send the display_data message with the
            # contents.
            self.send_response(self.iopub_socket,
                'display_data', content)
    
        # We return the execution results.
        return {'status': 'ok',
                'execution_count': self.execution_count,
                'payload': [],
                'user_expressions': {},
               }
    
  6. 最后,在文件末尾添加以下行:

    if __name__ == '__main__':
        from IPython.kernel.zmq.kernelapp import IPKernelApp
        IPKernelApp.launch_instance(kernel_class=PlotKernel)
    
  7. 我们的内核准备就绪!下一步是告诉 IPython 此新内核已可用。为此,我们需要创建一个内核规范 kernel.json 文件,并将其放置在 ~/.ipython/kernels/plot/ 中。此文件包含以下行:

    {
     "argv": ["python", "-m",
              "plotkernel", "-f",
              "{connection_file}"],
     "display_name": "Plot",
     "language": "python"
    }
    

    plotkernel.py 文件需要 Python 能够导入。例如,我们可以简单地将其放在当前目录中。

  8. 在 IPython 3.0 中,我们可以从 IPython 笔记本仪表板启动具有此内核的笔记本。笔记本界面的右上角有一个下拉菜单,其中包含可用内核的列表。选择绘图内核以使用它。

  9. 最后,在由我们定制的绘图内核支持的新笔记本中,我们只需简单地编写数学方程 y = f(x)。相应的图形将显示在输出区域。这里是一个例子:如何操作……

    我们自定义绘图包装器内核的示例

如何运行……

我们将在第三章*《精通笔记本》*中提供更多关于 IPython 和笔记本架构的细节。这里我们仅做一个简要总结。请注意,这些细节可能会在未来版本的 IPython 中发生变化。

内核和客户端运行在不同的进程中。它们通过在网络套接字上实现的消息协议进行通信。目前,这些消息采用 JSON 编码,这是一种结构化的基于文本的文档格式。

我们的内核接收来自客户端(例如,笔记本)的代码。每当用户发送一个单元格的代码时,do_execute()函数就会被调用。

内核可以通过self.send_response()方法将消息发送回客户端:

  • 第一个参数是套接字,这里是IOPub套接字

  • 第二个参数是消息类型,在这里是stream,用于返回标准输出或标准错误,或者是display_data,用于返回富数据

  • 第三个参数是消息的内容,表示为一个 Python 字典

数据可以包含多种 MIME 表示:文本、HTML、SVG、图片等。由客户端来处理这些数据类型。特别是,HTML 笔记本客户端知道如何在浏览器中呈现这些类型。

该函数返回一个包含执行结果的字典。

在这个示例中,我们始终返回ok状态。在生产代码中,最好检测到错误(例如函数定义中的语法错误),并返回错误状态。

所有消息协议的细节可以在本食谱末尾给出的链接中找到。

还有更多...

包装内核可以实现可选方法,特别是用于代码补全和代码检查。例如,为了实现代码补全,我们需要编写以下方法:

def do_complete(self, code, cursor_pos):
    return {'status': 'ok',
            'cursor_start': ...,
            'cursor_end': ...,
            'matches': [...]}

当用户请求代码补全且光标位于代码单元格中的某个cursor_pos位置时,会调用此方法。在该方法的响应中,cursor_startcursor_end字段表示代码补全应覆盖的输出区间。matches字段包含建议的列表。

这些细节可能会随着 IPython 3.0 的发布而发生变化。你可以在以下参考资料中找到所有最新的信息:

第二章:交互式计算中的最佳实践

在本章中,我们将涵盖以下主题:

  • 选择(或不选择)Python 2 和 Python 3

  • 使用 IPython 进行高效的交互式计算工作流

  • 学习分布式版本控制系统 Git 的基础

  • 使用 Git 分支的典型工作流

  • 进行可重现交互式计算实验的十条技巧

  • 编写高质量的 Python 代码

  • 使用 nose 编写单元测试

  • 使用 IPython 调试代码

引言

这是关于交互式计算中良好实践的特别章节。如果本书的其余部分讨论的是内容,那么本章讨论的是形式。它描述了如何高效且正确地使用本书所讨论的工具。我们将在讨论可重现计算实验之前,先介绍版本控制系统 Git 的基本要点(特别是在 IPython notebook 中)。

我们还将讨论一些软件开发中的一般主题,例如代码质量、调试和测试。关注这些问题可以极大地提高我们最终产品的质量(例如软件、研究和出版物)。我们这里只是略微涉及,但你将找到许多参考资料,帮助你深入了解这些重要主题。

选择(或不选择)Python 2 和 Python 3

在这第一个食谱中,我们将简要讨论一个横向且有些平凡的话题:Python 2 还是 Python 3?

Python 3 自 2008 年推出以来,许多 Python 用户仍然停留在 Python 2。通过改进了 Python 2 的多个方面,Python 3 打破了与之前版本的兼容性。因此,迁移到 Python 3 可能需要投入大量精力。

即使没有太多破坏兼容性的变化,一个在 Python 2 中运行良好的程序可能在 Python 3 中完全无法运行。例如,你的第一个 Hello World Python 2 程序在 Python 3 中无法运行;print "Hello World!" 在 Python 3 中会引发 SyntaxError。实际上,print 现在是一个函数,而不是一个语句。你应该写成 print("Hello World!"),这在 Python 2 中也能正常工作。

无论你是开始一个新项目,还是需要维护一个旧的 Python 库,选择 Python 2 还是 Python 3 的问题都会出现。在这里,我们提供一些论点和提示,帮助你做出明智的决策。

注意

当我们提到 Python 2 时,我们特别指的是 Python 2.6 或 Python 2.7,因为这些 Python 2.x 的最后版本比 Python 2.5 及更早版本更接近 Python 3。支持 Python 2.5+ 和 Python 3.x 同时运行更为复杂。

同样,当我们提到 Python 3 或 Python 3.x 时,我们特别指的是 Python 3.3 或更高版本。

如何做到……

首先,Python 2 和 Python 3 之间有什么区别?

Python 3 相对于 Python 2 的主要差异

这里是一些差异的部分列表:

  • print 不再是一个语句,而是一个函数(括号是必须的)。

  • 整数除法会返回浮点数,而不是整数。

  • 一些内置函数返回的是迭代器或视图,而不是列表。例如,range 在 Python 3 中的行为类似于 Python 2 中的 xrange,而后者在 Python 3 中已经不存在。

  • 字典不再有 iterkeys()iteritems()itervalues() 方法了。你应该改用 keys()items()values() 函数。

  • 以下是来自官方 Python 文档的引用:

    “你曾经以为自己了解的二进制数据和 Unicode,现在都变了。”

  • 使用 % 进行字符串格式化已不推荐使用;请改用 str.format

  • exec 是一个函数,而不是一个语句。

Python 3 带来了许多关于语法和标准库内容的改进和新特性。你将在本食谱末尾的参考资料中找到更多细节。

现在,你的项目基本上有两种选择:坚持使用单一分支(Python 2 或 Python 3),或同时保持与两个分支的兼容性。

Python 2 还是 Python 3?

自然,许多人会偏爱 Python 3;它代表着未来,而 Python 2 是过去。为什么还要去支持一个已被废弃的 Python 版本呢?不过,这里有一些可能需要保持 Python 2 兼容性的情况:

  • 你需要维护一个用 Python 2 编写的大型项目,而更新到 Python 3 会花费太高(即使存在半自动化的更新工具)。

  • 你的项目有一些依赖项无法与 Python 3 一起使用。

    注意

    本书中我们将使用的大多数库支持 Python 2 和 Python 3。并且本书的代码兼容这两个分支。

  • 你的最终用户所使用的环境不太支持 Python 3。例如,他们可能在某个大型机构工作,在许多服务器上部署新的 Python 版本成本太高。

在这种情况下,你可以选择继续使用 Python 2,但这意味着你的代码可能在不久的将来变得过时。或者,你可以选择 Python 3 和它那一堆崭新的功能,但也有可能会把 Python 2 的用户抛在后头。你也可以用 Python 2 编写代码,并为 Python 3 做好准备。这样,你可以减少将来迁移到 Python 3 时所需的修改。

幸运的是,你不一定要在 Python 2 和 Python 3 之间做出选择。实际上,有方法可以同时支持这两个版本。即使这可能比单纯选择一个分支多花一些工作,但在某些情况下它可能会非常有趣。不过,需要注意的是,如果选择这种做法,你可能会错过许多仅支持 Python 3 的特性。

同时支持 Python 2 和 Python 3

支持两个分支的基本方法有两种:使用 2to3 工具,或者编写在两个分支中都能正常运行的代码。

使用 2to3 工具

2to3 是标准库中的一个程序,能够自动将 Python 2 代码转换为 Python 3。例如,运行 2to3 -w example.py 可以将单个 Python 2 模块迁移到 Python 3。你可以在 docs.python.org/2/library/2to3.html 找到更多关于 2to3 工具的信息。

你可以配置安装脚本,使得 2to3 在用户安装你的包时自动运行。Python 3 用户将自动获得转换后的 Python 3 版本的包。

这个解决方案要求你的程序有一个坚实的测试套件,并且有一个持续集成系统,能够测试 Python 2 和 Python 3(请参阅本章稍后的单元测试食谱)。这是确保你的代码在两个版本中都能正常工作的方式。

编写在 Python 2 和 Python 3 中都能运行的代码。

你还可以编写既能在 Python 2 中运行,又能在 Python 3 中运行的代码。如果从头开始一个新项目,这个解决方案会更简单。一个广泛使用的方法是依赖一个轻量且成熟的模块,名为 six,由 Benjamin Peterson 开发。这个模块只有一个文件,因此你可以轻松地将其与包一起分发。无论何时你需要使用一个仅在某个 Python 分支中支持的函数或特性时,都需要使用 six 中实现的特定函数。这个函数要么包装,要么模拟相应的功能,因此它能在两个分支中正常工作。你可以在 pythonhosted.org/six/ 上找到关于 six 的更多信息。

这种方法要求你改变一些习惯。例如,在 Python 2 中迭代字典的所有项时,你会写如下代码:

for k, v in d.iteritems():
    # ...

现在,不再使用前面的代码,而是使用 six 编写以下代码:

from six import iteritems
for k, v in iteritems(d):
    # ...

Python 2 中字典的 iteritems() 方法在 Python 3 中被 items() 替代。six 模块的 iteritems 函数根据 Python 版本内部调用一个方法。

提示

下载示例代码

你可以从你的账号中下载所有已购买的 Packt 图书的示例代码文件,网址为 www.packtpub.com。如果你是在其他地方购买的此书,可以访问 www.packtpub.com/support 注册并直接将文件通过电子邮件发送给你。

还有更多...

正如我们所看到的,关于 Python 2 或 Python 3 的问题,有很多选择可以考虑。简而言之,你应该考虑以下选项:

  • 请仔细决定是否真的需要支持 Python 2:

    • 如果是,请通过避免使用 Python 2 专有的语法或特性,为 Python 3 准备好你的代码。你可以使用 six、2to3 或类似的工具。

    • 如果没有必要,坚决使用 Python 3。

  • 在所有情况下,确保你的项目拥有一个坚实的测试套件、出色的代码覆盖率(接近 100%),并且有一个持续集成系统,能够在你正式支持的所有 Python 版本中测试你的代码。

这里有几个相关的参考资料:

另见

  • 编写高质量 Python 代码 这篇食谱

  • 使用 nose 编写单元测试 这篇食谱

使用 IPython 进行高效的交互式计算工作流

有多种方法可以使用 IPython 进行交互式计算。其中一些在灵活性、模块化、可重用性和可复现性方面更为优秀。我们将在本节中回顾和讨论这些方法。

任何交互式计算工作流都基于以下循环:

  • 编写一些代码

  • 执行它

  • 解释结果

  • 重复

这个基本循环(也叫做读取-求值-打印循环,或称REPL)在进行数据或模型模拟的探索性研究时特别有用,或者在逐步构建复杂算法时也很有用。一个更经典的工作流(编辑-编译-运行-调试循环)通常是编写一个完整的程序,然后进行全面分析。这种方法通常比较繁琐。更常见的做法是通过做小规模实验、调整参数来迭代地构建算法解决方案,这正是交互式计算的核心。

集成开发环境IDEs),提供了全面的软件开发设施(如源代码编辑器、编译器和调试器),广泛应用于经典工作流。然而,在交互式计算方面,存在一些替代 IDE 的工具。我们将在这里进行回顾。

如何实现...

以下是几个可能的交互式计算工作流程,按复杂度递增排列。当然,IPython 是所有这些方法的核心。

IPython 终端

IPython 是 Python 中交互式计算的事实标准。IPython 终端(ipython命令)提供了一个专门为 REPL 设计的命令行界面。它比本地 Python 解释器(python命令)更强大。IPython 终端是一个便捷的工具,用于快速实验、简单的 Shell 交互以及查找帮助。忘记了 NumPy 的 savetxt函数的输入参数吗?只需在 IPython 中输入numpy.savetxt?(当然,你首先需要使用import numpy)。有些人甚至把 IPython 终端当作(复杂的)计算器来使用!

然而,当单独使用时,终端很快变得有限。主要问题是终端不是一个代码编辑器,因此输入超过几行的代码可能会变得不方便。幸运的是,有多种方法可以解决这个问题,下面的章节将详细介绍这些方法。

IPython 与文本编辑器

非文本编辑器问题的最简单解决方案或许并不令人意外,那就是结合使用 IPython 和文本编辑器。在这种工作流程中,%run魔法命令成为了核心工具:

  • 在你喜欢的文本编辑器中编写一些代码,并将其保存在myscript.py Python 脚本文件中。

  • 在 IPython 中,假设你处于正确的目录,输入%run myscript.py

  • 脚本被执行。标准输出会实时显示在 IPython 终端中,并且会显示可能的错误。脚本中定义的顶级变量在脚本执行完毕后,可以在 IPython 终端中访问。

  • 如果需要在脚本中进行代码更改,请重复此过程。

提示

通过适当的键盘快捷键,IPython-文本编辑器工作流程可以变得更加高效。例如,你可以自动化你的文本编辑器,当按下F8时,在正在运行的 IPython 解释器中执行以下命令:

%run <CURRENT_FILE_NAME>

这里描述了这种方法(在 Windows 上,使用 Notepad++和 AutoHotKey):

cyrille.rossant.net/python-ide-windows/

使用一个好的文本编辑器,这个工作流程可以非常高效。由于每次执行%run时都会重新加载脚本,因此你的更改会自动生效。当你的脚本导入了其他 Python 模块并且你修改了这些模块时,情况会变得更复杂,因为这些模块不会随着%run被重新加载。你可以使用深度重新加载来解决这个问题:

import myscript
from IPython.lib.deepreload import reload as dreload
dreload(myscript)

myscript中导入的模块将会被重新加载。一个相关的 IPython 魔法命令是%autoreload(你首先需要执行%load_ext autoreload)。此命令会尝试自动重新加载交互命名空间中导入的模块,但并不总是成功。你可能需要显式地重新加载已更改的模块,使用reload(module)(在 Python 2 中)或imp.reload(module)(Python 3 中)。

IPython 笔记本

IPython 笔记本在高效的交互式工作流程中起着核心作用。它是代码编辑器和终端的精心设计的结合,将两者的优点融为一体,提供一个统一的环境。

你可以在笔记本的单元格中开始编写所有代码。你可以在同一地方编写、执行和测试代码,从而提高生产力。你可以在 Markdown 单元格中加入长注释,并使用 Markdown 标题来结构化你的笔记本。

一旦你的一部分代码足够成熟且不再需要进一步修改,你可以(并且应该)将它们重构为可重用的 Python 组件(函数、类和模块)。这将清理你的笔记本,并促进代码的未来重用。需要强调的是,不断将代码重构为可重用组件非常重要。IPython 笔记本当前不容易被第三方代码重用,并且它们并不针对这一点进行设计。笔记本适合进行初步分析和探索性研究,但它们不应该阻止你定期清理并将代码重构为 Python 组件。

笔记本的一个主要优势是,它们能为你提供一份文档,记录你在代码中所做的一切。这对可重复研究极为有用。笔记本保存在人类可读的 JSON 文档中,因此它们与 Git 等版本控制系统相对兼容。

集成开发环境

集成开发环境(IDEs)特别适用于经典的软件开发,但它们也可以用于交互式计算。一款好的 Python IDE 将强大的文本编辑器(例如,包含语法高亮和自动补全功能的编辑器)、IPython 终端和调试器结合在统一的环境中。

对于大多数平台,有多个商业和开源 IDE。Eclipse/PyDev 是一个流行的(尽管略显笨重的)开源跨平台环境。Spyder 是另一个开源 IDE,具有良好的 IPython 和 matplotlib 集成。PyCharm 是众多支持 IPython 的商业环境之一。

微软的 Windows IDE,Visual Studio,有一个名为 Python Tools for Visual StudioPTVS)的开源插件。这个工具为 Visual Studio 带来了 Python 支持。PTVS 原生支持 IPython。你不一定需要付费版本的 Visual Studio;你可以下载一个免费的包,将 PTVS 和 Visual Studio 打包在一起。

还有更多…

以下是一些 Python IDE 的链接:

另见

  • 学习分布式版本控制系统 Git 的基础 方法

  • 使用 IPython 调试代码 的方法

学习分布式版本控制系统 Git 的基础

使用 分布式版本控制系统 在当今已经变得非常自然,如果你正在阅读本书,你可能已经在使用某种版本控制系统。然而,如果你还没有,请认真阅读这个方法。你应该始终为你的代码使用版本控制系统。

准备工作

著名的分布式版本控制系统包括 GitMercurialBazaar。在这一章中,我们选择了流行的 Git 系统。你可以从 git-scm.com 下载 Git 程序和 Git GUI 客户端。在 Windows 上,你也可以安装 msysGitmsysgit.github.io)和 TortoiseGitcode.google.com/p/tortoisegit/)。

注意

与 SVN 或 CVS 等集中式系统相比,分布式系统通常更受欢迎。分布式系统允许本地(离线)更改,并提供更灵活的协作系统。

支持 Git 的在线服务商包括 GitHubgithub.com)、Bitbucketbitbucket.org)、Google codecode.google.com)、Gitoriousgitorious.org)和 SourceForgesourceforge.net)。在撰写本书时,所有这些网站创建账户都是免费的。GitHub 提供免费的无限制公共仓库,而 Bitbucket 提供免费的无限制公共和私有仓库。GitHub 为学术用户提供特殊功能和折扣(github.com/edu)。将你的 Git 仓库同步到这样的网站,在你使用多台计算机时特别方便。

你需要安装 Git(可能还需要安装 GUI)才能使用此方法(参见 git-scm.com/downloads)。我们还建议你在以下这些网站之一创建账户。GitHub 是一个很受欢迎的选择,特别是因为它用户友好的网页界面和发达的社交功能。GitHub 还提供了非常好的 Windows 客户端(windows.github.com)和 Mac OS X 客户端(mac.github.com)。我们在本书中使用的大多数 Python 库都是在 GitHub 上开发的。

如何操作…

我们将展示两种初始化仓库的方法。

创建一个本地仓库

这种方法最适合开始在本地工作时使用。可以通过以下步骤实现:

  1. 在开始一个新项目或计算实验时,最先要做的就是在本地创建一个新文件夹:

    $ mkdir myproject
    $ cd myproject
    
    
  2. 我们初始化一个 Git 仓库:

    $ git init
    
    
  3. 让我们设置我们的姓名和电子邮件地址:

    $ git config --global user.name "My Name"
    $ git config --global user.email "me@home"
    
    
  4. 我们创建一个新文件,并告诉 Git 跟踪它:

    $ touch __init__.py
    $ git add __init__.py
    
    
  5. 最后,让我们创建我们的第一次提交:

    $ git commit -m "Initial commit."
    
    

克隆一个远程仓库

当仓库需要与 GitHub 等在线提供商同步时,这种方法最好。我们来执行以下步骤:

  1. 我们在在线提供商的网页界面上创建了一个新的仓库。

  2. 在新创建项目的主页面上,我们点击克隆按钮并获取仓库 URL,然后在终端输入:

    $ git clone /path/to/myproject.git
    
    
  3. 我们设置我们的姓名和电子邮件地址:

    $ git config --global user.name "My Name"
    $ git config --global user.email "me@home"
    
    
  4. 让我们创建一个新文件并告诉 Git 跟踪它:

    $ touch __init__.py
    $ git add __init__.py
    
    
  5. 我们创建我们的第一次提交:

    $ git commit -m "Initial commit."
    
    
  6. 我们将本地更改推送到远程服务器:

    $ git push origin
    
    

当我们拥有一个本地仓库(通过第一种方法创建)时,我们可以使用 git remote add 命令将其与远程服务器同步。

它是如何工作的…

当你开始一个新项目或新的计算实验时,在你的计算机上创建一个新文件夹。你最终会在这个文件夹中添加代码、文本文件、数据集和其他资源。分布式版本控制系统会跟踪你在项目发展过程中对文件所做的更改。它不仅仅是一个简单的备份,因为你对任何文件所做的每个更改都会保存相应的时间戳。你甚至可以随时恢复到之前的状态;再也不用担心破坏你的代码了!

具体来说,你可以随时通过执行提交来拍摄项目的快照。该快照包括所有已暂存(或已跟踪)的文件。你完全控制哪些文件和更改将被跟踪。使用 Git,你可以通过 git add 将文件标记为下次提交的暂存文件,然后用 git commit 提交你的更改。git commit -a 命令允许你提交所有已经被跟踪的文件的更改。

在提交时,你需要提供一个消息来描述你所做的更改。这样可以使仓库的历史更加详细和富有信息。

你应该多频繁地提交?

答案是非常频繁的。Git 只有在你提交更改时才会对你的工作负责。在两次提交之间发生的内容可能会丢失,所以你最好定期提交。此外,提交是快速且便宜的,因为它们是本地的;也就是说,它们不涉及与外部服务器的任何远程通信。

Git 是一个分布式版本控制系统;你的本地仓库不需要与外部服务器同步。然而,如果你需要在多台计算机上工作,或者如果你希望拥有远程备份,你应该进行同步。与远程仓库的同步可以通过 git push(将你的本地提交发送到远程服务器)、git fetch(下载远程分支和对象)或 git pull(同步远程更改到本地仓库)来完成。

还有更多…

本教程中展示的简化工作流是线性的。然而,在实际操作中,Git 的工作流通常是非线性的;这就是分支的概念。我们将在下一个教程中描述这个概念,使用 Git 分支的典型工作流

这里有一些关于 Git 的优秀参考资料:

另见

  • Git 分支的典型工作流 方案

Git 分支的典型工作流

像 Git 这样的分布式版本控制系统是为复杂的、非线性的工作流设计的,这类工作流通常出现在交互式计算和探索性研究中。一个核心概念是分支,我们将在本方案中讨论这一点。

准备工作

你需要在本地 Git 仓库中工作才能进行此方案(见前一方案,学习分布式版本控制系统 Git 的基础知识)。

如何执行…

  1. 我们创建一个名为 newidea 的新分支:

    $ git branch newidea
    
    
  2. 我们切换到这个分支:

    $ git checkout newidea
    
    
  3. 我们对代码进行更改,例如,创建一个新文件:

    $ touch newfile.py
    
    
  4. 我们添加此文件并提交我们的更改:

    $ git add newfile.py
    $ git commit -m "Testing new idea."
    
    
  5. 如果我们对更改感到满意,我们将该分支合并到 master 分支(默认分支):

    $ git checkout master
    $ git merge newidea
    
    

    否则,我们删除该分支:

    $ git checkout master
    $ git branch -d newidea
    
    

其他感兴趣的命令包括:

  • git status:查看仓库的当前状态

  • git log:显示提交日志

  • git branch:显示现有的分支并突出当前分支

  • git diff:显示提交或分支之间的差异

暂存

可能发生的情况是,当我们正在进行某项工作时,需要在另一个提交或另一个分支中进行其他更改。我们可以提交尚未完成的工作,但这并不理想。更好的方法是将我们正在工作的副本暂存到安全位置,以便稍后恢复所有未提交的更改。它是如何工作的:

  1. 我们使用以下命令保存我们的未提交更改:

    $ git stash
    
    
  2. 我们可以对仓库进行任何操作:检出一个分支、提交更改、从远程仓库拉取或推送等。

  3. 当我们想要恢复未提交的更改时,输入以下命令:

    $ git stash pop
    
    

我们可以在仓库中有多个暂存的状态。有关暂存的更多信息,请使用 git stash --help

它是如何工作的…

假设为了测试一个新想法,你需要对多个文件中的代码进行非琐碎的修改。你创建了一个新的分支,测试你的想法,并最终得到了修改过的代码版本。如果这个想法是死路一条,你可以切换回原始的代码分支。然而,如果你对这些修改感到满意,你可以合并它到主分支。

这种工作流的优势在于,主分支可以独立于包含新想法的分支进行发展。当多个协作者在同一个仓库中工作时,这特别有用。然而,这也是一种很好的习惯,尤其是当只有一个贡献者时。

合并并不总是一个简单的操作,因为它可能涉及到两个分歧的分支,且可能存在冲突。Git 会尝试自动解决冲突,但并不总是成功。在这种情况下,你需要手动解决冲突。

合并的替代方法是重基(rebasing),当你在自己的分支上工作时,如果主分支发生了变化,重基将非常有用。将你的分支重基到主分支上,可以让你将分支点移到一个更近期的点。这一过程可能需要你解决冲突。

Git 分支是轻量级对象。创建和操作它们的成本很低。它们是为了频繁使用而设计的。掌握所有相关概念和git命令(尤其是 checkoutmergerebase)非常重要。前面的食谱中包含了许多很好的参考资料。

还有更多……

许多人曾思考过有效的工作流。例如,一个常见但复杂的工作流,叫做 git-flow,可以在 nvie.com/posts/a-successful-git-branching-model/ 中找到描述。然而,在小型和中型项目中,使用一个更简单的工作流可能更为适宜,比如 scottchacon.com/2011/08/31/github-flow.html 中描述的工作流。后者详细阐述了这个食谱中展示的简化示例。

与分支相关的概念是分叉(forking)。同一个仓库可以在不同的服务器上有多个副本。假设你想为存储在 GitHub 上的 IPython 代码做贡献。你可能没有权限修改他们的仓库,但你可以将其复制到你的个人账户中——这就是所谓的分叉。在这个副本中,你可以创建一个分支,并提出一个新功能或修复一个 bug。然后,你可以提出一个拉取请求(pull request),请求 IPython 的开发者将你的分支合并到他们的主分支。他们可以审核你的修改,提出建议,并最终决定是否合并你的工作(或不合并)。GitHub 就是围绕这个想法构建的,因此提供了一种清晰、现代的方式来协作开发开源项目。

在合并 pull 请求之前进行代码审查,有助于提高协作项目中的代码质量。当至少两个人审查任何一段代码时,合并错误代码或不正确代码的可能性就会降低。

当然,关于 Git 还有很多要说的。版本控制系统通常是复杂且功能强大的,Git 也不例外。掌握 Git 需要时间和实验。之前的食谱中包含了许多优秀的参考资料。

这里有一些关于分支和工作流的进一步参考资料:

另见

  • 学习分布式版本控制系统 Git 的基础 食谱

进行可重现的互动计算实验的十个技巧

在这篇食谱中,我们提出了十个技巧,帮助你进行高效且可重现的互动计算实验。这些更多是指导性建议,而非绝对规则。

首先,我们将展示如何通过减少重复性任务的时间、增加思考核心工作的时间来提高生产力。

其次,我们将展示如何在计算工作中实现更高的可重现性。值得注意的是,学术研究要求实验可重现,以便任何结果或结论可以被其他研究者独立验证。方法中的错误或操控往往会导致错误的结论,从而产生有害的后果。例如,在 2010 年 Carmen Reinhart 和 Kenneth Rogoff 发表的经济学研究论文《债务时期的增长》中,计算错误部分导致了一项存在全球影响力的 flawed 研究,对政策制定者产生了影响(请见 en.wikipedia.org/wiki/Growth_in_a_Time_of_Debt)。

如何做…

  1. 仔细而一致地组织你的目录结构。具体的结构并不重要,重要的是在整个项目中保持文件命名规范、文件夹、子文件夹等的一致性。以下是一个简单的例子:

    • my_project/

      • data/

      • code/

        • common.py

        • idea1.ipynb

        • idea2.ipynb

      • figures/

      • notes/

      • README.md

  2. 使用轻量级标记语言(如 Markdown (daringfireball.net/projects/markdown/) 或 reStructuredText (reST))在文本文件中写下笔记。所有与项目、文件、数据集、代码、图形、实验室笔记本等相关的元信息应写入文本文件。

  3. 与此相关,在代码中记录所有非平凡的内容,包括注释、文档字符串等。你可以使用文档生成工具,如 Sphinx (sphinx-doc.org)。然而,在你工作时,不要花费太多时间记录不稳定和前沿的代码;它可能频繁变化,且你的文档很快就会过时。编写代码时要确保它易于理解,无需过多注释(为变量和函数命名合理,使用 Pythonic 编程模式等)。另请参见下一个章节,编写高质量的 Python 代码

  4. 对于所有基于文本的文件,使用分布式版本控制系统,如 Git,但不要用于二进制文件(除非是非常小的文件且确实需要)。每个项目应使用一个版本库。将版本库同步到远程服务器上,使用免费或付费的托管服务提供商(如 GitHub 或 Bitbucket)或你自己的服务器(你的主办机构可能能够为你设置一个)。使用特定的系统来存储和共享二进制数据文件,例如 figshare.comdatadryad.org

  5. 首先在 IPython 笔记本中编写所有交互式计算代码,只有在代码成熟和稳定时,才将其重构为独立的 Python 组件。

  6. 为了完全可重现性,确保记录下整个软件堆栈中所有组件的确切版本(操作系统、Python 发行版、模块等)。一种选择是使用虚拟环境,如 virtualenvconda

  7. 使用 Python 的原生 pickle 模块、dill (pypi.python.org/pypi/dill) 或 Joblib (pythonhosted.org/joblib/) 缓存长时间计算的中间结果。Joblib 特别实现了一个 NumPy 友好的 memoize 模式(不要与 memorize 混淆),该模式允许你缓存计算密集型函数的结果。还可以查看 ipycache IPython 扩展 (github.com/rossant/ipycache);它在笔记本中实现了一个 %%cache 单元魔法。

    注意

    在 Python 中保存持久化数据

    对于纯内部使用,你可以使用 Joblib、NumPy 的savesavez函数来保存数组,使用 pickle 来保存任何其他 Python 对象(尽量选择原生类型,如列表和字典,而非自定义类)。对于共享用途,建议使用文本文件来保存小型数据集(少于 10k 个数据点),例如,使用 CSV 格式存储数组,使用 JSON 或 YAML 格式存储高度结构化的数据。对于较大的数据集,你可以使用 HDF5(请参见第四章中的使用 HDF5 和 PyTables 操作大型数组使用 HDF5 和 PyTables 操作大型异构表格的配方)。

  8. 在开发并测试大数据集上的算法时,先在数据的小部分上运行并进行比较,再转向整个数据集。

  9. 在批量运行任务时,使用并行计算来充分利用你的多核处理单元,例如,使用IPython.parallel、Joblib、Python 的多处理包,或任何其他并行计算库。

  10. 尽可能使用 Python 函数或脚本来自动化你的工作。对于用户公开的脚本,使用命令行参数,但在可能的情况下,更倾向于使用 Python 函数而非脚本。在 Unix 系统中,学习终端命令以提高工作效率。对于 Windows 或基于 GUI 的系统上的重复性任务,使用自动化工具,如 AutoIt(www.autoitscript.com/site/autoit…)或 AutoHotKey(www.autohotkey.com)。学习你经常使用程序的快捷键,或者创建你自己的快捷键。

提示

例如,你可以创建一个键盘快捷键来启动当前目录下的 IPython 笔记本服务器。以下链接包含一个 AutoHotKey 脚本,可以在 Windows 资源管理器中实现这一操作:

cyrille.rossant.net/start-an-ipython-notebook-server-in-windows-explorer/

它是如何工作的…

本文档中的建议最终旨在优化你的工作流程,涵盖人类时间、计算机时间和质量。使用一致的约定和结构来编写代码,可以让你更轻松地组织工作。记录所有内容可以节省每个人的时间,包括(最终)你自己!如果明天你被公交车撞了(我真心希望你不会),你应该确保你的替代者能够迅速接手,因为你的文档写得非常认真细致。(你可以在en.wikipedia.org/wiki/Bus_factor找到更多关于“公交车因子”的信息。)

使用分布式版本控制系统和在线托管服务可以让你在多个地点协同工作同一个代码库,且无需担心备份问题。由于你可以回溯代码历史,因此几乎不可能无意间破坏代码。

IPython 笔记本是一个用于可重复交互计算的出色工具。它让你可以详细记录工作过程。此外,IPython 笔记本的易用性意味着你无需担心可重复性;只需在笔记本中进行所有交互式工作,将其放入版本控制中,并定期提交。不要忘记将你的代码重构为独立的可重用组件。

确保优化你在电脑前花费的时间。当处理一个算法时,经常会发生这样的循环:你做了一点修改,运行代码,获取结果,再做另一个修改,依此类推。如果你需要尝试很多修改,你应该确保执行时间足够快(不超过几秒钟)。在实验阶段,使用高级优化技术未必是最佳选择。你应该缓存结果,在数据子集上尝试算法,并以较短的时间运行模拟。当你想测试不同的参数值时,也可以并行启动批处理任务。

最后,极力避免重复任务。对于日常工作中频繁发生的任务,花时间将其自动化是值得的。虽然涉及 GUI 的任务更难自动化,但借助 AutoIt 或 AutoHotKey 等免费工具,还是可以实现自动化的。

还有更多…

以下是一些关于计算可重复性的参考资料:

  • 高效的可重复科学工作流程,Trevor Bekolay 的演讲, 可在bekolay.org/scipy2013-workflow/找到。

  • 可重复计算研究的十条简单规则Sandve 等PLoS 计算生物学2013 年,可在dx.doi.org/10.1371/journal.pcbi.1003285找到。

  • Konrad Hinsen 的博客,khinsen.wordpress.com

  • Software Carpentry 是一个为科学家举办研讨会的志愿者组织;这些研讨会涵盖了科学编程、交互式计算、版本控制、测试、可重复性和任务自动化等内容。你可以在software-carpentry.org找到更多信息。

另见

  • 高效的交互式计算工作流程与 IPython 配方

  • 编写高质量的 Python 代码 配方

编写高质量的 Python 代码

编写代码很容易,编写高质量的代码则要困难得多。质量不仅体现在实际代码(变量名、注释、文档字符串等)上,还包括架构(函数、模块、类等)。通常,设计一个良好的代码架构比实现代码本身更具挑战性。

在本配方中,我们将提供一些如何编写高质量代码的建议。这是学术界一个特别重要的话题,因为越来越多没有软件开发经验的科学家需要编程。

本教程末尾给出的参考资料包含了比我们在此提到的更多细节。

如何实现...

  1. 花时间认真学习 Python 语言。查看标准库中所有模块的列表——你可能会发现你已经实现的某些函数已经存在。学会编写 Pythonic 代码,不要将其他语言(如 Java 或 C++)的编程习惯直接翻译到 Python 中。

  2. 学习常见的 设计模式;这些是针对软件工程中常见问题的通用可复用解决方案。

  3. 在代码中使用断言(assert 关键字)来防止未来的 bug (防御性编程)。

  4. 采用自下而上的方法开始编写代码;编写实现专注任务的独立 Python 函数。

  5. 不要犹豫定期重构你的代码。如果你的代码变得过于复杂,思考如何简化它。

  6. 尽量避免使用类。如果可以使用函数代替类,请选择函数。类只有在需要在函数调用之间存储持久状态时才有用。尽量让你的函数保持 纯净(没有副作用)。

  7. 一般来说,优先使用 Python 原生类型(列表、元组、字典和 Python collections 模块中的类型)而不是自定义类型(类)。原生类型能带来更高效、更可读和更具可移植性的代码。

  8. 在函数中选择关键字参数而不是位置参数。参数名称比参数顺序更容易记住。它们使你的函数自文档化。

  9. 小心命名你的变量。函数和方法的名称应以动词开头。变量名应描述它是什么。函数名应描述它做什么。命名的正确性至关重要,不能被低估。

  10. 每个函数都应该有一个描述其目的、参数和返回值的文档字符串,如下例所示。你还可以查看像 NumPy 这样的流行库中所采用的约定。重要的是在你的代码中保持一致性。你可以使用 Markdown 或 reST 等标记语言:

    def power(x, n):
        """Compute the power of a number.
    
        Arguments:
          * x: a number.
          * n: the exponent.
    
        Returns:
           * c: the number x to the power of n.
    
        """
        return x ** n
    
  11. 遵循(至少部分遵循)Guido van Rossum 的 Python 风格指南,也叫 Python 增强提案第 8 号PEP8),可在 www.python.org/dev/peps/pe… 查阅。这是一本长篇指南,但它能帮助你编写可读性强的 Python 代码。它涵盖了许多小细节,如操作符之间的空格、命名约定、注释和文档字符串。例如,你会了解到将代码行限制在 79 个字符以内(如果有助于提高可读性,可以例外为 99 个字符)是一个良好的实践。这样,你的代码可以在大多数情况下(如命令行界面或移动设备上)正确显示,或者与其他文件并排显示。或者,你可以选择忽略某些规则。总的来说,在涉及多个开发人员的项目中,遵循常见的指南是有益的。

  12. 你可以通过pep8 Python 包自动检查代码是否符合 PEP8 中的大多数编码风格规范。使用pip install pep8进行安装,并通过pep8 myscript.py执行。

  13. 使用静态代码分析工具,如 Pylint(www.pylint.org)。它可以让你静态地找到潜在的错误或低质量的代码,即在不运行代码的情况下进行检查。

  14. 使用空行来避免代码杂乱(参见 PEP8)。你也可以通过显著的注释来标记一个较长 Python 模块的各个部分,例如:

    # Imports
    # -------
    import numpy
    
    # Utility functions
    # -----------------
    def fun():
        pass
    
  15. 一个 Python 模块不应包含超过几百行代码。一个模块的代码行数过多可能意味着你需要将其拆分成多个模块。

  16. 将重要的项目(包含数十个模块)组织为子包,例如:

    • core/

    • io/

    • utils/

    • __init__.py

  17. 看看主要的 Python 项目是如何组织的。例如,IPython 的代码就很有条理,按照具有明确职责的子包层次结构进行组织。阅读这些代码本身也非常有启发性。

  18. 学习创建和分发新 Python 包的最佳实践。确保你了解 setuptools、pip、wheels、virtualenv、PyPI 等工具。此外,我们强烈建议你认真研究 conda(conda.pydata.org),这是由 Continuum Analytics 开发的一个强大且通用的打包系统。打包是 Python 中一个混乱且快速发展的领域,因此请只阅读最新的参考资料。在*更多内容…*部分中有一些参考资料。

它是如何工作的…

编写可读的代码意味着其他人(或者几年后你自己)会更快地理解它,也更愿意使用它。这也有助于 bug 追踪。

模块化代码也更容易理解和重用。将程序的功能实现为独立的函数,并按照包和模块的层次结构进行组织,是实现高质量代码的绝佳方式。

使用函数而不是类可以更容易地保持代码的松耦合。意大利面式代码(Spaghetti code)真的很难理解、调试和重用。

在处理一个新项目时,可以在自下而上的方法和自上而下的方法之间交替进行。自下而上的方法让你在开始思考程序的整体架构之前先对代码有一定的经验。然而,仍然要确保通过思考组件如何协同工作来知道自己最终的目标。

还有更多内容…

已经有很多关于如何编写优美代码的文章——请参阅以下参考资料。你可以找到许多关于这个主题的书籍。在接下来的教程中,我们将介绍确保代码不仅看起来漂亮,而且能够按预期工作的标准技术:单元测试、代码覆盖率和持续集成。

这里有一些参考资料:

另见

  • 《进行可重复交互式计算实验的十个技巧》 配方

  • 使用 nose 编写单元测试 配方

使用 nose 编写单元测试

手动测试对确保我们的软件按预期工作并且不包含关键性错误至关重要。然而,手动测试存在严重限制,因为每次更改代码时,可能会引入新的缺陷。我们不可能在每次提交时都手动测试整个程序。

现如今,自动化测试已经成为软件工程中的标准实践。在这个配方中,我们将简要介绍自动化测试的重要方面:单元测试、测试驱动开发、测试覆盖率和持续集成。遵循这些实践对于开发高质量的软件是绝对必要的。

准备工作

Python 有一个原生的单元测试模块(unittest),你可以直接使用。还有其他第三方单元测试包,如 py.test 或 nose,我们在这里选择了 nose。nose 使得编写测试套件变得稍微容易一些,并且拥有一个外部插件库。除非用户自己想运行测试套件,否则他们不需要额外的依赖。你可以通过 pip install nose 安装 nose。

如何实现...

在这个示例中,我们将为一个从 URL 下载文件的函数编写单元测试。即使在没有网络连接的情况下,测试套件也应能够运行并成功通过。我们通过使用一个模拟的 HTTP 服务器来欺骗 Python 的 urllib 模块,从而解决这一问题。

注意

本食谱中使用的代码片段是为 Python 3 编写的。要使它们在 Python 2 中运行,需要做一些更改,我们在代码中已标明了这些更改。Python 2 和 Python 3 的版本都可以在本书的网站上找到。

你可能对requests模块也感兴趣;它为 HTTP 请求提供了一个更简单的 API(docs.python-requests.org/en/latest/)。

  1. 我们创建了一个名为datautils.py的文件,里面包含以下代码:

    In [1]: %%writefile datautils.py
    # Version 1.
    import os
    from urllib.request import urlopen  # Python 2: use urllib2
    
    def download(url):
        """Download a file and save it in the current folder.
        Return the name of the downloaded file."""
        # Get the filename.
        file = os.path.basename(url)
        # Download the file unless it already exists.
        if not os.path.exists(file):
            with open(file, 'w') as f:
                f.write(urlopen(url).read())
        return file
    Writing datautils.py
    
  2. 我们创建了一个名为test_datautils.py的文件,里面包含以下代码:

    In [2]: %%writefile test_datautils.py
    # Python 2: use urllib2
    from urllib.request import (HTTPHandler, install_opener, 
                                build_opener, addinfourl)
    import os
    import shutil
    import tempfile
    from io import StringIO  # Python 2: use StringIO
    from datautils import download
    
    TEST_FOLDER = tempfile.mkdtemp()
    ORIGINAL_FOLDER = os.getcwd()
    
    class TestHTTPHandler(HTTPHandler):
        """Mock HTTP handler."""
        def http_open(self, req):
            resp = addinfourl(StringIO('test'), '',
                              req.get_full_url(), 200)
            resp.msg = 'OK'
            return resp
    
    def setup():
        """Install the mock HTTP handler for unit tests."""
        install_opener(build_opener(TestHTTPHandler))
        os.chdir(TEST_FOLDER)
    
    def teardown():
        """Restore the normal HTTP handler."""
        install_opener(build_opener(HTTPHandler))
        # Go back to the original folder.
        os.chdir(ORIGINAL_FOLDER)
        # Delete the test folder.
        shutil.rmtree(TEST_FOLDER)
    
    def test_download1():
        file = download("http://example.com/file.txt")
        # Check that the file has been downloaded.
        assert os.path.exists(file)
        # Check that the file contains the contents of
        # the remote file.
        with open(file, 'r') as f:
            contents = f.read()
        print(contents)
        assert contents == 'test'
    Writing test_datautils.py
    
  3. 现在,为了启动测试,我们在终端中执行以下命令:

    $ nosetests
    .
    Ran 1 test in 0.042s
    OK
    
    
  4. 我们的第一个单元测试通过了!现在,让我们添加一个新的测试。我们在test_datautils.py文件的末尾添加一些代码:

    In [4]: %%writefile test_datautils.py -a
    
            def test_download2():
                file = download("http://example.com/")
                assert os.path.exists(file)
    Appending to test_datautils.py
    
  5. 我们使用nosetests命令再次启动测试:

    $ nosetests
    .E
    ERROR: test_datautils.test_download2
    Traceback (most recent call last):
     File "datautils.py", line 12, in download
     with open(file, 'wb') as f:
    IOError: [Errno 22] invalid mode ('wb') or filename: ''
    Ran 2 tests in 0.032s
    FAILED (errors=1)
    
    
  6. 第二个测试失败了。在实际应用中,我们可能需要调试程序。这应该不难,因为错误被隔离在一个单独的测试函数中。在这里,通过检查回溯错误和代码,我们发现错误是由于请求的 URL 没有以正确的文件名结尾。因此,推断的文件名os.path.basename(url)为空。我们通过以下方法来修复这个问题:将datautils.py中的download函数替换为以下函数:

    In [6]: %%file datautils.py
    # Version 2.
    import os
    from urllib.request import urlopen  # Python 2: use urllib2
    
    def download(url):
        """Download a file and save it in the current folder.
        Return the name of the downloaded file."""
        # Get the filename.
        file = os.path.basename(url)
        # Fix the bug, by specifying a fixed filename if the
        # URL does not contain one.
        if not file:
            file = 'downloaded'
        # Download the file unless it already exists.
        if not os.path.exists(file):
            with open(file, 'w') as f:
                f.write(urlopen(url).read())
        return file
    Overwriting datautils.py
    
  7. 最后,让我们再次运行测试:

    $ nosetests
    ..
    Ran 2 tests in 0.036s
    OK
    
    

提示

默认情况下,nosetests会隐藏标准输出(除非发生错误)。如果你希望标准输出显示出来,可以使用nosetests --nocapture

它是如何工作的...

每个名为xxx.py的 Python 模块应该有一个对应的test_xxx.py模块。这个测试模块包含执行并测试xxx.py模块中功能的函数(单元测试)。

根据定义,一个给定的单元测试必须专注于一个非常具体的功能。所有单元测试应该是完全独立的。将程序编写为一组经过充分测试、通常是解耦的单元,迫使你编写更易于维护的模块化代码。

然而,有时你的模块函数在运行之前需要一些预处理工作(例如,设置环境、创建数据文件或设置 Web 服务器)。单元测试框架可以处理这些事情;只需编写setup()teardown()函数(称为fixtures),它们将分别在测试模块开始和结束时被调用。请注意,测试模块运行前后的系统环境状态应该完全相同(例如,临时创建的文件应在teardown中删除)。

这里,datautils.py 模块包含一个名为 download 的函数,该函数接受一个 URL 作为参数,下载文件并将其保存到本地。这个模块还带有一个名为 test_datautils.py 的测试模块。你应该在你的程序中使用相同的约定(test_<modulename> 作为 modulename 模块的测试模块)。这个测试模块包含一个或多个以 test_ 为前缀的函数。nose 就是通过这种方式自动发现项目中的单元测试。nose 也接受其他类似的约定。

提示

nose 会运行它在你的项目中找到的所有测试,但你当然可以更精确地控制要运行的测试。键入 nosetests --help 可以获取所有选项的列表。你也可以查阅 nose.readthedocs.org/en/latest/testing.html 上的文档。

测试模块还包含 setupteardown 函数,这些函数会被 nose 自动识别为测试夹具。在 setup 函数中创建一个自定义的 HTTP 处理程序对象。该对象会捕获所有 HTTP 请求,即使是那些具有虚构 URL 的请求。接着,setup 函数会进入一个测试文件夹(该文件夹是通过 tempfile 模块创建的),以避免下载的文件和现有文件之间可能的冲突。一般来说,单元测试不应该留下任何痕迹;这也是我们确保测试完全可重复的方式。同样,teardown 函数会删除测试文件夹。

提示

在 Python 3.2 及更高版本中,你还可以使用 tempfile.TemporaryDirectory 来创建一个临时目录。

第一个单元测试从一个虚拟 URL 下载文件,并检查它是否包含预期的内容。默认情况下,如果单元测试没有抛出异常,则视为通过。这时,assert 语句就非常有用,如果语句为 False,则会抛出异常。nose 还提供了方便的例程和装饰器,用于精确地确定某个单元测试期望通过或失败的条件(例如,它应该抛出某个特定的异常才算通过,或者它应该在 X 秒内完成等)。

提示

NumPy 提供了更多方便的类似 assert 的函数(请参见 docs.scipy.org/doc/numpy/reference/routines.testing.html)。这些函数在处理数组时特别有用。例如,np.testing.assert_allclose(x, y) 会断言 xy 数组几乎相等,精度可以指定。

编写完整的测试套件需要时间。它对你代码的架构提出了严格(但良好的)约束。这是一次真正的投资,但从长远来看总是值得的。此外,知道你的项目有一个完整的测试套件支持,真是让人放心。

首先,从一开始就考虑单元测试迫使你思考模块化架构。对于一个充满相互依赖的单体程序,编写单元测试是非常困难的。

其次,单元测试使得发现和修复 bug 变得更加容易。如果在程序中引入了更改后,某个单元测试失败,隔离并重现 bug 就变得非常简单。

第三,单元测试帮助你避免回归,即已修复的 bug 在后续版本中悄然重现。当你发现一个新 bug 时,应该为它编写一个特定的失败单元测试。修复它时,让这个测试通过。现在,如果这个 bug 后来再次出现,这个单元测试将会失败,你就可以立即解决它。

假设你编写了一个多层次的复杂程序,每一层的基础上都有一个基于第 n层的n+1层。有了一套成功的单元测试作为第 n层的保障,你就能确信它按预期工作。当你在处理n+1层时,你可以专注于这一层,而不必总是担心下面一层是否有效。

单元测试并不是全部,它只关注独立的组件。为了确保程序中各组件的良好集成,还需要进一步的测试层级。

还有更多...

单元测试是一个广泛的话题,我们在这个配方中仅仅触及了表面。这里提供了一些进一步的信息。

测试覆盖率

使用单元测试是好事,但测量测试覆盖率更好:它量化了我们的代码有多少被你的测试套件覆盖。Ned Batchelder 的coverage模块(nedbatchelder.com/code/coverage/)正是做这件事。它与 nose 非常好地集成。

首先,使用pip install coverage安装 coverage。然后使用以下命令运行你的测试套件:

$ nosetests --with-cov --cover-package datautils

该命令指示 nose 仅为datautils包启动测试套件并进行覆盖率测量。

coveralls.io服务将测试覆盖率功能引入持续集成服务器(参见单元测试与持续集成部分)。它与 GitHub 无缝集成。

带有单元测试的工作流

注意我们在这个例子中使用的特定工作流。在编写download函数后,我们创建了第一个通过的单元测试。然后我们创建了第二个失败的测试。我们调查了问题并修复了函数,第二个测试通过了。我们可以继续编写越来越复杂的单元测试,直到我们确信该函数在大多数情况下按预期工作。

提示

运行nosetests --pdb以在失败时进入 Python 调试器。这对于快速找出单元测试失败的原因非常方便。

这就是测试驱动开发,它要求在编写实际代码之前编写单元测试。这个工作流迫使我们思考代码的功能和使用方式,而不是它是如何实现的。

单元测试与持续集成

养成每次提交时运行完整测试套件的好习惯。实际上,我们甚至可以通过 持续集成 完全透明且自动地做到这一点。我们可以设置一个服务器,在每次提交时自动在云端运行我们的测试套件。如果某个测试失败,我们会收到一封自动邮件,告诉我们问题所在,以便我们修复。

有很多持续集成系统和服务:Jenkins/Hudson、drone.iostridercd.comtravis-ci.org 等等。其中一些与 GitHub 项目兼容。例如,要在 GitHub 项目中使用 Travis CI,可以在 Travis CI 上创建账户,将 GitHub 项目与此账户关联,然后在仓库中添加一个 .travis.yml 文件,其中包含各种设置(有关更多详情,请参见以下参考资料)。

总结来说,单元测试、代码覆盖率和持续集成是所有重大项目应遵循的标准实践。

这里有一些参考资料:

使用 IPython 调试代码

调试是软件开发和交互式计算的一个不可或缺的部分。一种常见的调试技术是在代码中的各个地方放置 print 语句。谁没做过这个呢?它可能是最简单的解决方案,但肯定不是最有效的(它是穷人版的调试器)。

IPython 完美适配调试,集成的调试器非常易于使用(实际上,IPython 只是提供了一个友好的界面来访问原生的 Python 调试器 pdb)。特别是,IPython 调试器中支持 Tab 补全。本节内容描述了如何使用 IPython 调试代码。

注意

早期版本的 IPython 笔记本不支持调试器,也就是说,调试器可以在 IPython 终端和 Qt 控制台中使用,但在笔记本中无法使用。这个问题在 IPython 1.0 中得到了解决。

如何做到这一点...

在 Python 中有两种非互斥的调试方式。在事后调试模式下,一旦抛出异常,调试器会立即进入代码,这样我们就可以调查导致异常的原因。在逐步调试模式下,我们可以在断点处停止解释器,并逐步恢复执行。这个过程使我们能够在代码执行时仔细检查变量的状态。

两种方法其实可以同时使用;我们可以在事后调试模式下进行逐步调试。

事后调试模式

当在 IPython 中抛出异常时,执行 %debug 魔法命令启动调试器并逐步进入代码。并且,%pdb on 命令告诉 IPython 一旦抛出异常,就自动启动调试器。

一旦进入调试器,你可以访问几个特殊命令,下面列出的是最重要的一些:

  • p varname 打印一个变量的值

  • w 显示你在堆栈中的当前位置

  • u 在堆栈中向

  • d 在堆栈中向

  • l 显示你当前位置周围的代码行

  • a 显示当前函数的参数

调用堆栈包含代码执行当前位置的所有活动函数的列表。你可以轻松地在堆栈中上下导航,检查函数参数的值。虽然这个模式使用起来相当简单,但它应该能帮助你解决大部分问题。对于更复杂的问题,可能需要进行逐步调试。

步骤调试

你有几种方法来启动逐步调试模式。首先,为了在代码中某处设置断点,插入以下命令:

import pdb; pdb.set_trace()

其次,你可以使用以下命令从 IPython 运行一个脚本:

%run -d -b extscript.py:20 script

这个命令在调试器控制下运行 script.py 文件,并在 extscript.py 的第 20 行设置一个断点(该文件在某个时刻由 script.py 导入)。最后,一旦进入调试器,你就可以开始逐步调试。

步骤调试就是精确控制解释器的执行过程。从脚本开始或者从断点处,你可以使用以下命令恢复解释器的执行:

  • s 执行当前行并尽快停下来(逐步调试,也就是最细粒度的执行模式)

  • n 继续执行直到到达当前函数中的下一

  • r 继续执行直到当前函数返回

  • c 继续执行直到到达下一个断点

  • j 30 将你带到当前文件的第 30 行

你可以通过 b 命令或 tbreak(临时断点)动态添加断点。你还可以清除所有或部分断点,启用或禁用它们,等等。你可以在 docs.python.org/3/library/pdb.html 找到调试器的完整细节。

还有更多...

要使用 IPython 调试代码,你通常需要先通过 IPython 执行它,例如使用 %run。然而,你可能并不总是有一个简单的方法来做到这一点。例如,你的程序可能通过一个自定义的命令行 Python 脚本运行,可能是由一个复杂的 bash 脚本启动,或者集成在一个 GUI 中。在这些情况下,你可以在代码的任何位置嵌入一个 IPython 解释器(由 Python 启动),而不是用 IPython 运行整个程序(如果你只需要调试代码的一小部分,使用整个程序可能会显得过于复杂)。

要将 IPython 嵌入到你的程序中,只需在代码中的某个地方插入以下命令:

from IPython import embed
embed()

当你的 Python 程序执行到这段代码时,它会暂停并在该特定位置启动一个交互式的 IPython 终端。你将能够检查所有局部变量,执行你想要的任何代码,并且在恢复正常执行之前,可能会调试你的代码。

提示

rfoo,访问链接 code.google.com/p/rfoo/,让你可以远程检查和修改正在运行的 Python 脚本的命名空间。

GUI 调试器

大多数 Python 集成开发环境(IDE)都提供图形化调试功能(参见 使用 IPython 的高效交互式计算工作流)。有时候,GUI 比命令行调试器更为方便。我们还可以提到 Winpdb(winpdb.org),一个图形化、平台无关的 Python 调试器。