Python3-科学计算教程-全-

Python3 科学计算教程(全)

原文:Scientific Computing with Python 3

协议:CC BY-NC-SA 4.0

零、前言

Python 不仅仅可以用于通用编程。这是一种免费的开源语言和环境,在科学计算领域具有巨大的应用潜力。这本书将 Python 与数学应用紧密联系在一起,并演示了如何将 Python 中的各种概念用于计算目的,包括 Python 3 最新版本的示例。Python 是将科学计算和数学结合起来使用的有效工具,这本书将教你如何将它用于线性代数、数组、绘图、迭代、函数、多项式等等。

这本书涵盖了什么

第 1 章入门、在不详细介绍的情况下,讲述了 Python 的主要语言元素。在这里,我们做一个简短的参观。对于那些想直接开始的人来说,这是一个很好的起点。对于那些希望在下一章中理解一个例子的读者来说,这是一个快速的参考,这个例子在函数被深入解释之前,可能会使用像函数这样的构造。

第二章变量和基本类型,介绍了 Python 中最重要和最基本的类型。浮点数是科学计算中与特殊数字 nan 和 inf 一起使用的更重要的数据类型。布尔值、整数、复数和字符串是其他基本数据类型,将在本书中使用。

第 3 章容器类型、说明如何使用容器类型,主要是列表。字典和元组将通过容器对象进行解释,以及索引和循环。有时,人们甚至使用集合作为一种特殊的容器类型。

第 4 章线性代数,处理线性代数中最重要的对象——向量和矩阵。本书选择 NumPy 数组作为描述矩阵甚至高阶张量的中心工具。数组有许多高级特性,也允许通用函数在元素上作用于矩阵或向量。这本书强调数组索引、切片和点积是大多数计算任务中的基本操作。通过一些线性代数的例子来说明 SciPy 子模线性代数的使用。

第五章高级阵概念、解释了阵的一些更高级的方面。数组副本和视图之间的区别得到了广泛的解释,因为视图使使用数组的程序非常快,但经常是错误的来源,很难调试。展示和演示了使用布尔数组编写有效、紧凑和可读的代码。最后,数组广播技术 NumPy 数组的一个独特特性——是通过它对函数执行的操作的类比来解释的。

第 6 章标绘、展示了如何制作图,主要是经典的 x/yplots,也有 3D 图和直方图。科学计算需要好的工具来可视化结果。Python 的 matplotlib 模块是从其子模块 pyplot 中方便的绘图命令开始介绍的。通过创建图形对象(如轴),可以微调和修改绘图。我们展示了如何更改这些对象的属性以及如何进行注释。

第 7 章函数、构成了编程中最基本的构建模块,它可能最接近底层的数学概念。函数定义和函数调用被解释为设置函数参数的不同方式。匿名 lambda 函数在整本书的各种例子中都有介绍和使用。

第 8 章类、将对象定义为类的实例,我们为其提供方法和属性。在数学中,类属性通常是相互依赖的,这就要求 setter 和 getter 函数采用特殊的编程技术。可以为特殊的数学数据类型定义诸如+之类的基本数学运算。继承和抽象是面向对象编程所反映的数学概念。我们通过一个简单的常微分方程求解类来演示继承的使用。

第 9 章迭代、使用循环和迭代器呈现迭代。这本书现在有一章没有循环和迭代,但是在这里我们来到迭代器的原理,并创建自己的生成器对象。在本章中,您将了解为什么发电机会耗尽,以及如何对无限循环进行编程。Python 的 itertools 模块是本章的有用助手。

第 10 章错误处理、涵盖错误和异常以及如何查找和修复它们。错误或异常是中断程序单元执行的事件。本章说明了接下来该做什么,也就是如何处理异常。您将学习定义自己的异常类以及如何提供有价值的信息,这些信息可用于捕获这些异常。错误处理不仅仅是打印错误信息。

第 11 章名称空间、范围和模块、涵盖了 Python 模块。什么是局部变量和全局变量?什么时候一个变量是已知的,什么时候一个程序单元是未知的?这将在本章中讨论。变量可以通过参数列表传递给函数,也可以通过使用其作用域来默认注入。这项技术应该什么时候应用,什么时候不应用?这一章试图回答这个中心问题。

第 12 章输入输出、介绍了一些处理数据文件的选项。数据文件用于存储和提供给定问题的数据,通常是大规模测量。本章描述了如何使用不同的格式访问和修改这些数据。

第 13 章测试、重点关注科学编程的测试。关键工具是 unittest,它允许自动测试和参数化测试。通过考虑数值数学中的经典二分算法,我们举例说明了设计有意义测试的不同步骤,作为副作用,这也提供了一段代码使用的文档。仔细的测试提供了测试协议,这些协议在以后调试复杂的代码时会很有帮助,这些代码通常是由许多不同的程序员编写的。

第 14 章综合示例、给出了一些综合且较长的示例,并简要介绍了理论背景及其完整实现。这些例子利用了到目前为止书中显示的所有结构,并将它们放在一个更大、更复杂的上下文中。它们开放给读者扩展。

第 15 章符号计算- SymPy、讲述符号计算。科学计算主要是具有不精确数据和近似结果的数值计算。这与符号计算(通常是形式操作)形成对比,后者旨在以封闭形式的表达式获得精确解。在本书的最后一章中,我们用 Python 介绍了这种技术,它通常用于推导和验证理论上的数学模型和数值结果。我们强调符号表达式的高精度浮点计算。

这本书你需要什么

您将需要 Pyhon3.5 或更高版本、SciPy、NumPy、Matplotlib、IPython shell(我们强烈建议通过 Anaconda 安装 Python 及其软件包)。书中的示例对内存和图形没有任何特殊的硬件要求。

这本书是给谁的

这本书是自 2008 年以来在隆德大学教授的科学计算 Python 课程的成果。多年来,该课程不断扩展,科隆、特隆赫姆、斯塔万格、索兰、拉普兰塔等大学以及以计算为导向的公司都教授该材料的浓缩版本。

我们相信 Python 及其周围的科学计算生态系统——SciPy、NumPY 和 matplotlib——代表了科学计算环境的巨大进步。Python 和前面提到的库都是免费和开源的。更重要的是,它是一种现代语言,具有这个形容词所包含的所有功能:面向对象编程、测试、带有 IPython 的高级 shell 等。写这本书的时候,我们心里有两组读者:

  • 选择 Python 作为他或她的第一种编程语言的读者将在教师指导的课程中使用这本书。这本书介绍了不同的主题,并提供了背景阅读和实验。老师通常从这本书里挑选和整理材料,使其适合入门课程的特定学习结果。
  • 已经对编程有一定经验,对科学计算或数学有一定品味的读者,在潜入 Scipy 和 Numpy 的世界时,会把这本书作为伴侣。比如说,用 Python 编程和用 MATLAB 编程可能会有很大的不同。这本书想指出编程的“pythonic”方式,这使得编程成为一种乐趣。

我们的目标是解释在科学计算的背景下开始使用 Python 的步骤。这本书可以从头到尾读一遍,也可以挑选看起来最有趣的部分。不用说,因为提高一个人的编程技能需要大量的练习,所以实验和玩书中的例子和练习是非常明智的。

我们希望读者和我们一样喜欢用 Python、SciPy、NumPY 和 matplotlib 编程。

Python 对其他语言

当决定一本关于科学计算的书使用什么语言时,许多因素都起了作用。语言本身的学习门槛对新人来说很重要,这里脚本语言通常提供了最好的选择。大量的数值计算模块是必要的,最好有强大的开发者社区。如果这些核心模块建立在经过良好测试和优化的快速库基础上,比如 LAPACK,那就更好了。最后,如果这种语言也适用于更广泛的环境和更广泛的应用,读者在学术环境之外使用从这本书中学到的技能的机会就更大。因此,选择 Python 是很自然的。

简而言之,Python 是

  • 免费开放源码
  • 一种脚本语言,意味着它被解释
  • 现代语言(面向对象、异常处理、动态类型等。)
  • 简洁、易读、易学
  • 充满了免费提供的库,特别是科学库(线性代数、可视化工具、绘图、图像分析、微分方程求解、符号计算、统计等)。)
  • 适用于更广泛的环境:科学计算、脚本、网站、文本解析等。
  • 广泛用于工业应用

Python 还有其他替代方法。这里列出了其中的一些以及与 Python 的区别。

Java,C++:面向对象的编译语言。与 Python 相比,更加冗长和低级。很少有科学图书馆。

FORTRAN:低级编译语言。这两种语言在科学计算中被广泛使用,在科学计算中,计算时间很重要。现在这些语言经常和 Python 包装器结合在一起。

PHP,Ruby,其他解释语言。PHP 是面向网络的。Ruby 和 Python 一样灵活,但是几乎没有科学库。

MATLAB,Scilab,Octave : MATLAB 是为科学计算而进化的矩阵计算工具。科学图书馆是巨大的。语言特性没有 Python 开发的好。既不是免费的,也不是开源的。SciLab 和 Octave 是开源工具,在语法上类似于 MATLAB。

Haskell : Haskell 是一种现代函数式语言,遵循与 Python 不同的编程范式。有一些常见的结构,如列表理解。Haskell 很少用于科学计算。另请参见【12】

其他蟒蛇文献

在这里,我们给出了一些 Python 文献的提示,这些文献可以作为补充资料或者作为平行阅读的文本。大多数关于 Python 的入门书籍都致力于将这种语言作为通用工具来教授。我们想在这里明确提到的一个很好的例子是【19】。它通过简单的例子来解释语言,例如,面向对象编程是通过组织一个比萨饼店来解释的。

很少有专门针对科学计算和工程的 Python 书籍。在这几本书中,我们想提到朗廷根的两本书,这两本书将科学计算与现代编程的“pythonic”观点相结合,【16,17】

这种“pythonic”观点也是我们教授数值算法编程的指导方针。我们试图展示计算机科学中有多少成熟的概念和结构可以应用于科学计算中的问题。披萨饼店的例子被拉格朗日多项式代替,生成器成为微分方程的时间步进方法,等等。

最后,我们不得不提到网络上几乎无限量的文献。在准备这本书时,网络也是一个很大的知识来源。来自网络的文献通常涵盖新的东西,但也可能完全过时。网络还提供了可能相互矛盾的解决方案和解释。我们强烈建议使用网络作为额外的来源,但我们认为一个“传统的”教科书,网络资源“编辑”作为一个丰富的新世界的更好的切入点。

惯例

在这本书里,你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。

文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名和用户输入如下所示:“在您的虚拟环境中安装带有conda install的附加包”

代码块设置如下:

from scipy import *
from matplotlib.pyplot import *

任何命令行输入或输出都编写如下:

jupyter notebook

新名词重要词语以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中看到的单词,会以这样的方式出现在文本中: Jupyter 笔记本是展示你的作品的绝佳工具

警告或重要提示会出现在这样的框中。

型式

提示和技巧是这样出现的。

读者反馈

我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它有助于我们开发出你真正能从中获益的标题。要向我们发送一般反馈,只需给 feedback@packtpub.com 发电子邮件,并在邮件主题中提及书名。如果你对某个主题有专业知识,并且对写作或投稿感兴趣,请参见我们位于www.packtpub.com/authors的作者指南。

客户支持

现在,您已经自豪地拥有了一本书,我们有许多东西可以帮助您从购买中获得最大收益。

下载示例代码

你可以从你在http://www.packtpub.com的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问http://www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。
  2. 将鼠标指针悬停在顶部的 SUPPORT 选项卡上。
  3. 点击代码下载&勘误表
  4. 搜索框中输入图书名称。
  5. 选择要下载代码文件的书籍。
  6. 从您购买这本书的下拉菜单中选择。
  7. 点击代码下载

下载文件后,请确保使用最新版本的解压缩文件夹:

  • 视窗系统的 WinRAR / 7-Zip
  • zipeg/izp/un ARX for MAC
  • 适用于 Linux 的 7-Zip / PeaZip

这本书的代码包也托管在 GitHub 上,网址为https://GitHub . com/packt publishing/Scientific-Computing-with-Python-3。我们还有来自丰富的图书和视频目录的其他代码包,可在https://github.com/PacktPublishing/获得。看看他们!

下载本书的彩色图片

我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。彩色图像将帮助您更好地理解输出中的变化。您可以从https://www . packtpub . com/sites/default/files/downloads/Scientificating with python 3 _ color images . pdf下载此文件。

勘误表

尽管我们尽了最大努力来确保我们内容的准确性,但错误还是会发生。如果你在我们的某本书里发现一个错误,也许是文本或代码中的错误,如果你能向我们报告,我们将不胜感激。通过这样做,你可以让其他读者免受挫折,并帮助我们改进这本书的后续版本。如果您发现任何勘误表,请访问http://www.packtpub.com/submit-errata,选择您的书籍,点击勘误表提交表链接,并输入您的勘误表的详细信息。一旦您的勘误表得到验证,您的提交将被接受,勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。

要查看之前提交的勘误表,请前往https://www.packtpub.com/books/content/support并在搜索栏中输入图书名称。所需信息将出现在勘误表部分。

盗版

互联网上版权材料的盗版是所有媒体的一个持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝,请立即向我们提供位置地址或网站名称,以便我们寻求补救。

请联系我们在 copyright@packtpub.com 的链接到可疑的盗版材料。

我们感谢您在保护我们的作者方面的帮助,以及我们为您带来有价值内容的能力。

问题

如果你对这本书的任何方面有问题,你可以联系我们在 questions@packtpub.com,我们将尽最大努力解决这个问题。

一、入门指南

在本章中,我们将简要概述 Python 的主要语法元素。刚开始学习编程的读者可以在本章的书中得到指导。每个主题都以操作方法的方式呈现在这里,并将在本书后面以更深入的概念方式进行解释,并且还将通过许多应用和扩展得到丰富。

已经熟悉另一种编程语言的读者会在本章中遇到 Python 处理经典语言构造的方式。它为他们提供了 Python 编程的快速入门。

两种类型的读者都被鼓励在曲折地阅读这本书时,把这一章作为一个简短的指南。然而,在我们开始之前,我们必须确保一切就绪,并且您已经安装了正确版本的 Python 以及科学计算的主要模块和工具,例如一个好的编辑器和一个外壳,这有助于代码开发和测试。

阅读以下部分,即使您已经可以访问安装了 Python 的计算机。你可能想调整一下,让工作环境符合这本书的介绍。

安装和配置说明

在深入研究这本书的主题之前,你应该在电脑上安装所有相关的工具。我们将为您提供一些建议,并推荐您可能想要使用的工具。我们只描述公共领域和免费工具。

安装

Python 目前有两个主要版本; 2.x 分公司和新的 3.x 分公司。这些分支之间存在语言不兼容,你必须知道使用哪一个。本书基于 3.x 分支,考虑到语言直到发布 3.5

对于本书,您需要安装以下内容:

  • 解释器:Python 3.5 (或更高版本)
  • 科学计算的模块:科学与数字
  • 数学结果的图形表示模块:matplotlib
  • 外壳:IPython
  • 一个 Python 相关的编辑器:Spyder(参考下面的图 1.1Spyder ,Geany

所谓的分发包方便了这些设备的安装。我们建议您使用 Anaconda。Spyder 的默认屏幕由左侧的编辑器窗口、右下角的控制台窗口(可访问 IPython shell)和右上角的帮助窗口组成,如下图所示:

Installation

图 1.1:Spyder 的默认屏幕由左侧的编辑器窗口、右下角的控制台窗口(可以访问 IPython shell)和右上角的帮助窗口组成。

蟒蛇

即使您的计算机上预装了 Python,我们也建议您创建一个个人 Python 环境,让您在工作时不会有意外影响计算机功能可能依赖的软件的风险。有了虚拟环境,比如 Anaconda,您可以自由地更改语言版本和安装软件包,而不会产生意想不到的副作用。

如果最坏的情况发生了,你把事情完全搞砸了,只需要删除 Anaconda 目录,然后重新开始。运行 Anaconda 安装程序将安装 Python、Python 开发环境和编辑器(Spyder)、shell IPython 以及最重要的数值计算软件包,例如 SciPy、NumPy 和 matplotlib。

您可以在 Anaconda 创建的虚拟环境中安装带有conda install的附加软件包(请参考【2】的官方文档)。

配置

大多数 Python 代码将收集在文件中。我们建议您在所有 Python 文件中使用以下标题:

from scipy import *
from matplotlib.pyplot import *

这样,您就可以确保本书中使用的所有标准模块和函数(如 SciPy)都是导入的。没有这一步,书中的大多数例子都会出现错误。许多编辑器,如 Spyder,提供了为您的文件创建模板的可能性。寻找这个特性,并将前面的标题放入模板中。

蟒蛇壳

Python 外壳很好,但对于交互式脚本来说不是最佳的。因此,我们建议改用 IPython(官方文档请参考【26】)。IPython 可以通过不同的方式启动:

  • 在终端外壳中运行以下命令:ipython
  • 通过直接点击一个名为 Jupyter QT 控制台的图标

Python Shell

  • 使用 Spyder 时,您应该使用 IPython 控制台(参见图 1.1Spyder )。

执行脚本

您通常希望执行文件的内容。根据文件在计算机上的位置,在执行文件内容之前,有必要导航到正确的位置。

  • 使用 IPython 中的命令cd移动到文件所在的目录。
  • 要执行名为myfile.py的文件的内容,只需在 IPython shell 中运行以下命令
 run myfile

获得帮助

以下是一些关于如何使用 IPython 的提示:

  • 要获得对象的帮助,只需在对象名称后键入?,然后键入return
  • 使用箭头键重复使用最后执行的命令。
  • 您可以使用选项卡键完成(也就是说,您编写一个变量或方法的第一个字母,IPython 向您显示一个包含所有可能完成的菜单)。
  • 使用 Ctrl+D 退出。
  • 使用 IPython 的神奇功能。在命令提示符下应用%magic可以找到列表和解释。

型式

你可以在 IPython 的在线文档中找到更多关于 IPython 的信息,【15】

jupyter–python 笔记本电脑

Jupyter 笔记本是展示你作品的绝佳工具。学生可能想用它来制作和记录作业和练习,老师可以用它来准备讲座,甚至幻灯片和网页。

如果您已经通过 Anaconda 安装了 Python,那么您已经为 Jupyter 准备好了一切。您可以通过在终端窗口中运行以下命令来调用笔记本:

jupyter notebook

浏览器窗口将会打开,您可以通过网络浏览器与 Python 进行交互。

程序和程序流程

程序是以自上而下的顺序执行的一系列语句。这种线性执行顺序有一些重要的例外:

  • 可能会有替代语句组(块)的条件执行,我们称之为分支。
  • 有重复执行的块,称为循环(参见下面的图 1.2程序流程)。
  • 有些函数调用是对另一段代码的引用,它在主程序流恢复之前执行。函数调用中断线性执行并暂停程序单元的执行,同时将控制传递给另一个单元-函数。当这完成时,它的控制被返回到调用单元。

Program and program flow

图 1.2:程序流程

Python 使用一种特殊的语法来标记语句块:一个关键字、一个冒号和一个缩进的语句序列,它们都属于语句块(参见下面的图 1.3Block 命令)。

Program and program flow

图 1.3:阻止命令

评论

如果程序中的一行包含符号#,同一行中的所有内容都被视为注释:

# This is a comment of the following statement
a = 3  # ... which might get a further comment here  

线连接

行尾的反斜杠\将下一行标记为延续行,即显式行连接。如果该行在所有圆括号都关闭之前结束,下面的行将自动被识别为延续行,即隐式行连接。

基本类型

让我们回顾一下您将在 Python 中遇到的基本数据类型。

数字

数字可以是整数、实数或复数。通常的操作是:

  • 加减,+-
  • 乘法和除法,*/
  • 动力,**

这里有一个例子:

2 ** (2 + 2) # 16
1j ** 2 # -1
1\. + 3.0j

复数符号

j是表示复数虚部的符号。它是一个语法元素,不应该与变量相乘相混淆。关于复数的更多信息可以在第二章变量和基本类型数值类型一节中找到。

字符串

字符串是字符序列,用单引号或双引号括起来:

'valid string'
"string with double quotes"
"you shouldn't forget comments"
'these are double quotes: ".." '

您也可以对多行字符串使用三重引号:

"""This is
 a long,
 long string"""

变量

变量是对对象的引用。一个对象可能有多个引用。使用赋值运算符=为变量赋值:

x = [3, 4] # a list object is created
y = x # this object now has two labels: x and y
del x # we delete one of the labels
del y # both labels are removed: the object is deleted

变量的值可以通过print功能显示:

x = [3, 4] # a list object is created
print(x)

列表

列表是非常有用的构造,也是 Python 中的基本类型之一。Python 列表是用方括号括起来的有序对象列表。可以使用方括号内从零开始的索引来访问列表的元素:

L1 = [5, 6]
L1[0] # 5
L1[1] # 6
L1[2] # raises IndexError
L2 = ['a', 1, [3, 4]]
L2[0] # 'a'
L2[2][0] # 3
L2[-1] # last element: [3,4]
L2[-2] # second to last: 1

元素的索引从零开始。可以将任何类型的对象放入列表,甚至其他列表。一些基本列表功能如下:

  • list(range(n))}n元素创建一个列表,从零开始:
      print(list(range(5))) # returns [0, 1, 2, 3, 4]
  • len给出列表的长度:
      len(['a', 1, 2, 34]) # returns 4
  • append用于将元素添加到列表中:
      L = ['a', 'b', 'c']
      L[-1] # 'c'
      L.append('d')
      L # L is now ['a', 'b', 'c', 'd']
      L[-1] # 'd'

列表操作

  • 运算符+连接两个列表:

          L1 = [1, 2]
          L2 = [3, 4]
          L = L1 + L2 # [1, 2, 3, 4]
    
  • As one might expect, multiplying a list with an integer concatenates the list with itself several times:

    n*L相当于做 n 的加法。

          L = [1, 2]
          3 * L # [1, 2, 1, 2, 1, 2]
    

布尔表达式

布尔表达式是可能具有值TrueFalse的表达式。产生条件表达式的一些常见运算符如下:

  • 相等,==
  • 不相等,!=
  • 小于、小于或等于、<<=
  • 大于、大于或等于、>>=

一种是将不同的布尔值与orand组合在一起。关键字not,给出了下面表达式的逻辑否定。比较可以连锁,例如x < y < z相当于x < y and y < z。不同的是y在第一个例子中只评估一次。在这两种情况下,当第一个条件x < y评估为False时,根本不评估z:

2 >= 4  # False
2 < 3 < 4 # True
2 < 3 and 3 < 2 # False
2 != 3 < 4 or False # True
2 <= 2 and 2 >= 2 # True
not 2 == 3 # True
not False or True and False # True!

优先规则

<><=>=!===运算符的优先级高于not.运算符andor运算符的优先级最低。优先级规则较高的运算符在优先级规则较低的运算符之前计算。

循环重复语句

循环用于重复执行一系列语句,同时在每次迭代之间更改变量。这个变量被称为索引变量。它被依次分配给列表的元素(参见第 9 章迭代):

L = [1, 2, 10]
for s in L:
    print(s * 2) # output: 2 4 20

for循环中重复的部分必须适当缩进:

for elt in my_list:
    do_something
    something_else
print("loop finished") # outside the for block

重复任务

for循环的一个典型用途是将某项任务重复固定次数:

n = 30
for iteration in range(n):
    do_something # this gets executed n times

休息,否则

for语句有两个重要的关键词:breakelsebreak退出for循环,即使我们正在迭代的列表没有用完:

for x in x_values:
   if x > threshold:
     break
   print(x)

定案else检查for循环是否是带有break关键字的broken。如果没有被破坏,则执行else关键字后面的块:

for x in x_values:
    if x > threshold:
       break
else:
    print("all the x are below the threshold")

条件语句

本节介绍如何使用条件来分支、中断或控制代码。条件语句限定了一个块,如果条件为真,将执行该块。如果不满足条件,将执行以关键字else开始的可选块(参见图 1.3块命令图)。我们通过打印|x|x 的绝对值来演示这一点:

Conditional statements

Python 的等价物如下:

x = ...
if x >= 0:
    print(x)
else:
    print(-x)

可以测试任何对象的真值,用于ifwhile语句。如何获得真值的规则在第二章变量和基本类型的布尔部分进行了解释。

用函数封装代码

函数对于在一个地方收集相似的代码片段很有用。考虑以下数学函数:

Encapsulating code with functions

Python 的等价物如下:

def f(x):
    return 2*x + 1

在图 1.4 中,解释了功能块的元素。

  • 关键字def告诉 Python 我们正在定义一个函数。
  • f是函数的名称。
  • x是函数的参数或输入。
  • return之后的部分称为函数的输出。

Encapsulating code with functions

图 1.4:功能解剖

定义函数后,可以使用以下代码调用它:

f(2) # 5
f(1) # 3

脚本和模块

文件中的语句集合(通常扩展名为py)称为脚本。假设我们将以下代码的内容放入名为smartscript.py的文件中:

def f(x):
    return 2*x + 1
z = []
for x in range(10):
    if f(x) > pi:
        z.append(x)
    else:
        z.append(-1)
print(z)

在 Python 或 IPython shell 中,这样的脚本可以在打开和读取文件后用exec命令执行。写成一行字,上面写着:

exec(open('smartscript.py').read())

IPython shell 提供了神奇的命令%run作为执行脚本的便捷替代方式:

%run smartscript

简单模块-采集功能

人们经常在脚本中收集函数。这将创建一个具有附加 Python 功能的模块。为了演示这一点,我们通过收集单个文件中的函数来创建一个模块,例如smartfunctions.py:

def f(x):
    return 2*x + 1
def g(x):
    return x**2 + 4*x - 5
def h(x):
    return 1/f(x)
  • 这些函数现在可以由任何外部脚本使用,或者直接在 IPython 环境中使用。
  • 模块内的功能可以相互依赖。
  • 将具有共同主题或目的的功能分组,给出了可以被其他人共享和使用的模块。

同样,命令exec(open('smartfunctions.py').read())使这些功能对你的 IPython 外壳可用(注意还有 IPython 魔法功能run)。在 Python 术语中,有人说它们被放入实际的命名空间中。

使用模块和名称空间

或者,可以通过命令import导入模块。它创建了一个命名空间命令from将函数放入通用名称空间:

import smartfunctions
print(smartfunctions.f(2))      # 5

from smartfunctions import g    #import just this function
print(g(1)) # 0

from smartfunctions import *    #import all
print(h(2)*f(2))                # 1.0

型式

导入

命令importfrom只将函数导入各自的名称空间一次。导入后更改函数对当前 Python 会话没有影响。关于模块的更多信息可以在第 11 章名称空间、范围和模块的模块部分找到。

翻译

Python 解释器执行以下步骤:

  • 首先,运行语法。
  • 然后一行一行地执行代码。
  • 函数或类声明中的代码是而不是执行的(但检查语法)。
      def f(x):
          return y**2  
      a = 3   # here both a and f are defined

您可以运行前面的程序,因为没有语法错误。只有在调用函数f时才会出现错误。

f(2) # error, y is not defined

总结

在本章中,我们简要介绍了 Python 的主要语言元素,但没有详细介绍。现在,您应该能够开始处理小部分代码,并测试不同的程序结构。所有这些都是为了在接下来的章节中作为开胃菜,在这些章节中,我们将为您提供细节、示例、练习和更多的背景信息。

二、变量和基本类型

在本章中,我们将介绍 Python 中最重要和最基本的类型。什么是类型?它是由数据内容、其表示和所有可能的操作组成的集合。在本书的后面,当我们在第 8 章中介绍一个类的概念时,我们将使这个定义更加精确。

变量

变量是对 Python 对象的引用。它们由分配创建,例如:

a = 1 
diameter = 3.
height = 5.
cylinder = [diameter, height] # reference to a list

变量的名称由大写字母和小写字母的任意组合、下划线_ 和数字组成。变量名不能以数字开头。请注意,变量名区分大小写。变量的良好命名是记录工作的重要部分,因此我们建议您使用描述性变量名。

Python 有一些保留关键字,不能作为变量名(参考下表,表 2.1 )。试图使用这样的关键字作为变量名会引发语法错误。

Variables

表 2.1:保留的 Python 关键字。

与其他编程语言相反,变量不需要类型声明。您可以使用多重赋值语句创建几个变量:

a = b = c = 1   # a, b and c get the same value 1

变量也可以在定义后更改:

a = 1 
a = a + 1 # a gets the value 2 
a = 3 * a   # a gets the value 6

最后两个语句可以通过使用增量运算符将这两个操作与赋值直接结合起来来编写:

a += 1  # same as a = a + 1 
a *= 3  # same as a = 3 * a

数字类型

在某些时候,您将不得不处理数字,所以我们从考虑 Python 中不同形式的数字类型开始。在数学中,我们区分自然数(ℕ)、整数(ℤ)、有理数(ℚ)、实数(ℝ)和复数(ℂ).这些是无限组数字。这些集合之间的操作不同,甚至可能没有定义。例如,在ℤ通常将两个数字相除可能不会得到一个整数——它在ℤ.没有定义

在 Python 中,像许多其他计算机语言一样,我们有数字类型:

  • 数字类型int,至少理论上是整个ℤ
  • 数字类型float,它是ℝ和的有限子集
  • 数字类型complex,它是ℂ的有限子集

有限集合有一个最小数和一个最大数,两个数之间有一个最小间距;更多细节请参考浮点表示部分。

整数

最简单的数字类型是整数类型。

普通整数

语句k = 3将变量k赋给一个整数。

将类型为+-*的运算应用于整数将返回一个整数。除法运算符//返回整数,/可能返回float:

6 // 2  # 3
7 // 2  # 3
7 / 2   # 3.5

Python 中的整数集合是无界的;没有最大的整数。这里的限制是计算机的内存,而不是语言给出的任何固定值。

型式

如果示例中的除法运算符(/)返回 3,则您可能没有安装正确的 Python 版本。

浮点数

如果在 Python 中执行语句a = 3.0,则创建一个浮点数(Python 类型:float)。这些数字构成有理数的子集,ℚ.

或者,常数可以用指数表示法给出a = 30.0e-1或简单地给出a = 30.e-1。符号e将指数和尾数分开,表达式为数学符号a = 30.0×101浮点数这个名称是指这些数字的内部表示,反映了在大范围考虑数字时小数点的浮动位置。

将初等数学运算+-*/应用于两个浮点数或一个整数和一个浮点数会返回一个浮点数。浮点数之间的运算很少返回有理数运算的精确结果:

0.4 - 0.3 # returns 0.10000000000000003

当比较浮点数时,这些事实很重要:

0.4 - 0.3 == 0.1 # returns False

浮点表示

在内部,浮点数由四个量表示:符号、尾数、指数符号和指数:

Floating point representation

β ϵ 和*x0T7】0,0 ≤ x i ≤ β*

x 0 ...x t-1 称为尾数, β 为基数, e 为指数 |e| ≤ Ut 称为尾数长度。条件 x 0 ≠ 0 使表示唯一,并在二进制情况下( β = 2)保存一位。

存在两个浮点零+0 和-0,两者都由尾数 0 表示。

在典型的英特尔处理器上, β = 2。为了在float类型中表示一个数字,使用了 64 位,即 2 位用于符号, t = 52 位用于尾数,10 位用于指数|e|。指数的上限 U 因此为 210-1=1023

有了这些数据,最小的正可表示数是

flmin= 1.0×2-1023≈10-308,最大 fl max = 1.111...1×21023≈10308

请注意,浮点数在[0,fl max 中的间距并不相等。尤其是在零点有一个间隙(参见【29】)。0 与第一个正数之间的距离为 2 -1023 ,而第一个与第二个之间的距离小一个因子2-52≈2.2**×10-16。此效果由归一化x0≠0引起,在图 2.1 中可视化。

这个间隙用次正规浮点数等距填充,这样的结果被四舍五入。次正规浮点数具有最小的可能指数,并且不遵循前导数字 x 0 必须不同于零的惯例;参见【13】

无限而不是一个数

总共有Infinite and not a number个浮点数。有时数字算法会计算这个范围之外的浮点数。

这会产生数字上溢或下溢。在 SciPy 中,特殊的浮点数inf被分配给溢出结果:

exp(1000.) # inf 
a = inf
3 - a   # -inf
3 + a   # inf

使用inf可能会导致数学上未定义的结果。这在 Python 中通过给结果分配另一个特殊的浮点数nan来表示。这代表非数字,即数学运算的未定义结果:

a + a # inf
a - a # nan 
a / a # nan

对于naninf的操作有特殊规定。例如,nan相比任何东西(甚至是它自己)总是返回False:

x = nan 
x < 0 # False
x > 0 # False
x == x # False

参见练习 4 了解nan永远不等于它自己这一事实的一些令人惊讶的后果。

浮子inf的表现比预期的要好得多:

0 < inf     # True 
inf <= inf  # True 
inf == inf  # True 
-inf < inf  # True 
inf - inf   # nan 
exp(-inf)   # 0 
exp(1 / inf)  # 1

检查naninf的一种方法是使用isnanisinf功能。通常,当一个变量得到值naninf时,人们希望直接做出反应。这可以通过使用 NumPy 命令seterr来实现。以下命令

seterr(all = 'raise')

如果计算返回其中一个值,将会引发错误。

底流-机器ε

当一个操作导致一个有理数落入零处的间隙时,就会发生下溢;参见图 2.1

Underflow - Machine Epsilon

图 2.1:零点的浮点间隙,这里 t = 3,U = 1

机器ε或舍入单位是最大的数字 ε ,这样浮动 ( 1.0 + ε) = 1.0。

注意εβ1-t/2 = 1.1102×10-16在今天的大部分电脑上。使用以下命令可以访问在运行代码的实际计算机上有效的值:

import sys 
sys.float_info.epsilon # 2.220446049250313e-16 (something like that)

变量sys.float_info包含更多关于机器上浮点类型内部表示的信息。

函数float将其他类型转换成浮点数——如果可能的话。当将适当的字符串转换为数字时,此函数特别有用:

a = float('1.356')

NumPy 中的其他浮点类型

NumPy 还提供其他浮点类型,从其他编程语言中称为双精度和单精度数字,即float64float32:

a = pi            # returns 3.141592653589793 
a1 = float64(a)   # returns 3.1415926535897931 
a2 = float32(a)   # returns 3.1415927 
a - a1            # returns 0.0 
a - a2            # returns -8.7422780126189537e-08

第二个最后一行证明aa1在精度上没有区别。在前两行中,它们仅在显示方式上有所不同。精度上的真正差异是在a和它的单精度对应物a2之间。

NumPy 函数finfo可用于显示这些浮点类型的信息:

f32 = finfo(float32) 
f32.precision   # 6 (decimal digits) 
f64 = finfo(float64) 
f64.precision   # 15 (decimal digits) 
f = finfo(float) 
f.precision     # 15 (decimal digits) 
f64.max         # 1.7976931348623157e+308 (largest number) 
f32.max         # 3.4028235e+38 (largest number) 
help(finfo)     # Check for more options

复数

复数是在许多科学和工程领域中经常使用的实数的扩展。

数学中的复数

复数由两个浮点数组成,实数部分 a 和虚数部分 b 。在数学中,复数写成 z=a+b i,其中 I 由 i 2 = 定义-1 是虚单位。 z 的共轭复数对应物是Complex Numbers in Mathematics

如果实部 a 为零,则该数称为虚数。

j 符号

在 Python 中,虚数的特征是在浮点数后面加上字母j,例如z = 5.2j。一个复数是由一个浮点数和一个虚数之和构成的,例如z = 3.5 + 5.2j

而在数学中,虚数部分表示为实数 b 与虚数单位 I 的乘积,Python 表示虚数的方式不是乘积:j只是一个后缀,表示该数是虚数。

下面的小实验证明了这一点:

b = 5.2 
z = bj   # returns a NameError 
z = b*j  # returns a NameError
z = b*1j # is correct

方法conjugate返回z的共轭:

z = 3.2 + 5.2j 
z.conjugate() # returns (3.2-5.2j)

实部和虚部

可以使用realimag属性访问复数的实数和虚数部分 z 。这些属性是只读的:

z = 1j 
z.real       # 0.0 
z.imag       # 1.0 
z.imag = 2   # AttributeError: readonly attribute

无法将复数转换为实数:

z = 1 + 0j 
z == 1     # True 
float(z)   # TypeError

有趣的是,realimag属性以及共轭方法对于复杂数组 ( 第 4 章线性代数–数组)同样有效。我们通过计算Real and imaginary partsN 个单位根来证明这一点,即方程Real and imaginary partsN 个解:

N = 10
# the following vector contains the Nth roots of unity: 
unity_roots = array([exp(1j*2*pi*k/N) for k in range(N)])
# access all the real or imaginary parts with real or imag:
axes(aspect='equal')
plot(unity_roots.real, unity_roots.imag, 'o')
allclose(unity_roots**N, 1) # True

结果图(图 2.2 )显示了单位圆的根。(关于如何制作剧情的更多细节,请参考第六章剧情。)

Real and imaginary parts

图 2.2:单位圆的单位根

当然可以混合使用前面的方法,如以下示例所示:

z = 3.2+5.2j 
(z + z.conjugate()) / 2\.   # returns (3.2+0j) 
((z + z.conjugate()) / 2.).real   # returns 3.2 
(z - z.conjugate()) / 2\.   # returns 5.2j 
((z - z.conjugate()) / 2.).imag   # returns 5.2 
sqrt(z * z.conjugate())   # returns (6.1057350089894991+0j)

布尔人

布尔是以乔治·布尔 (1815-1864)命名的数据类型。一个布尔变量只能取两个值,TrueFalse。这种类型主要用于逻辑表达式。以下是一些例子:

a = True 
b = 30 > 45   # b gets the value False

布尔表达式经常与if语句结合使用:

if x > 0:
   print("positive")
else:
   print("nonpositive)

布尔运算符

使用 Python 中的andornot关键字执行布尔运算:

True and False # False
False or True # True
(30 > 45) or (27 < 30) # True
not True # False
not (3 > 4) # True

操作符遵循一些优先规则(参见第 1 章入门中的执行脚本一节),这将使第三行和最后一行中的括号过时(无论如何使用它们来增加代码的可读性是一个很好的做法)。请注意,and运算符隐式链接在以下布尔表达式中:

a < b < c     # same as: a < b and b < c 
a == b == c   # same as: a == b and b == c

转换为布尔值的规则:

Boolean operators

表 2.2:转换为布尔值的规则

布尔铸造

大多数 Python 对象可能会转换为布尔值;这叫布尔铸造。内置函数bool执行该转换。请注意,除了0、空元组、空列表、空字符串或空数组之外,大多数对象都被强制转换为True。这些都是投给False的。

除非数组不包含或只包含一个元素,否则不可能将数组转换为布尔值。这将在第 5 章高级阵概念中进一步解释。上表包含布尔转换的总结规则。一些用法示例:

bool([])   # False 
bool(0)   # False 
bool(' ')   # True 
bool('')   # False 
bool('hello')   # True 
bool(1.2)   # True 
bool(array([1]))   # True 
bool(array([1,2]))   # Exception raised!

自动布尔铸造

使用非布尔类型的if语句会将其转换为布尔类型。换句话说,以下两种说法总是等价的:

if a:
   ...
if bool(a): # exactly the same as above
   ...

一个典型的例子是测试列表是否为空:

# L is a list
if L:
    print("list not empty")
else:
    print("list is empty")

空数组、列表或元组将返回False。您也可以在if语句中使用变量,例如整数:

# n is an integer
if n % 2:
    print("n is odd")
else:
    print("n is even")

注意,我们使用%进行模运算,它返回整数除法的余数。在这种情况下,它返回01作为模2后的余数。

在最后这个例子中,值01被转换为bool。布尔运算符orandnot也将隐式地将它们的一些参数转换为布尔值。

和与或的返回值

请注意,运算符andor不一定会产生布尔值。 *x* and *y* 这个表达相当于:

def and_as_function(x,y):
    if not x:
        return x
    else:
        return y

而表达式x or y相当于:

def or_as_function(x,y):
    if x:
        return x
    else:
        return y

有趣的是,这意味着在执行语句True or x时,甚至不需要定义变量xFalse and x也是如此。

请注意,与数学逻辑中的对应运算符不同,这些运算符在 Python 中不再是可交换的。事实上,以下表达式并不等价:

[1] or 'a' # produces [1] 
'a' or [1] # produces 'a'

布尔和整数

事实上,布尔值和整数是一样的。唯一的区别是 0 和 1 的字符串表示,布尔人FalseTrue的情况分别如此。这允许这样的构造(格式方法参见字符串格式一节):

def print_ispositive(x):
    possibilities = ['nonpositive', 'positive']
    return "x is {}".format(possibilities[x>0])

对于已经熟悉子类概念的读者,我们注意到类型bool是类型int的子类(参考第 8 章)。事实上,所有四个查询isinstance(True, bool)isinstance(False, bool)isinstance(True, int)isinstance(False, int)都返回值True(参考第 3 章集装箱类型中的类型检查部分)。

甚至像True+13这样很少使用的语句在句法上也是正确的。

字符串

类型string是用于文本的类型:

name = 'Johan Carlsson'
child = "Åsa is Johan Carlsson's daughter"
book = """Aunt Julia 
       and the Scriptwriter"""

字符串用单引号或双引号括起来。如果一个字符串包含几行,必须用三个双引号"""或三个单引号'''括起来。

字符串可以用简单的索引或切片进行索引(关于切片的全面说明,请参考第 3 章容器类型):

book[-1] # returns 'r' 
book[-12:] # returns 'Scriptwriter'

字符串是不可变的;也就是说,项目不能被更改。他们与元组共享这个属性。 book[1] = 'a' 命令返回:

TypeError: 'str' object does not support item assignment

字符串'\n'用于插入一个换行符,'t'在字符串中插入一个水平制表器(TAB)来对齐几行:

print('Temperature:\t20\tC\nPressure:\t5\tPa')

这些字符串是转义序列的例子。转义序列总是以反斜杠\开头。多行字符串自动包含转义序列:

a=""" 
A multiline 
example""" 
a #  returns '\nA multiline \nexample'

一个特殊的转义序列是"",表示文本中的反斜杠符号:

latexfontsize="\\tiny"

通过使用原始字符串可以实现同样的效果:

latexfs=r"\tiny"   # returns "\tiny"
latexfontsize == latexfs  # returns True

请注意,在原始字符串中,反斜杠保留在字符串中,用于转义一些特殊字符:

r"\"\"   # returns  '\\"'
r"\\"   # returns  '\\\\'
r"\"    # returns an error

对字符串和字符串方法的操作

字符串的添加意味着串联:

last_name = 'Carlsson'
first_name = 'Johanna'
full_name = first_name + ' ' + last_name
                              # returns 'Johanna Carlsson'

乘法只是重复加法:

game = 2 * 'Yo' # returns 'YoYo'

当比较字符串时,将应用字典顺序,并且大写形式在小写形式之前的相同字母:

'Anna' > 'Arvi' # returns false 
'ANNA' < 'anna'  # returns true 
'10B' < '11A'    # returns true

在各种各样的字符串方法中,我们这里只提到最重要的:

  • 拆分字符串:该方法通过使用一个或多个空格作为分隔符,从字符串中生成一个列表。或者,可以通过指定特定字符串作为分隔符来给出参数:

          text = 'quod erat    demonstrandum'
          text.split() # returns ['quod', 'erat', 'demonstrandum']
          table = 'Johan;Carlsson;19890327'
          table.split(';') # returns ['Johan','Carlsson','19890327']
          king = 'CarlXVIGustaf'
          king.split('XVI')  # returns ['Carl','Gustaf']
    
    
  • 将列表加入字符串:这是拆分的反向操作:

          sep = ';'
          sep.join(['Johan','Carlsson','19890327']) 
          # returns 'Johan;Carlsson;19890327'
    
    
  • Searching in a string: This method returns the first index in the string, where a given search substring starts:

          birthday = '20101210'
          birthday.find('10') # returns 2 
    

    如果没有找到搜索字符串,则该方法的返回值为-1。

字符串格式

字符串格式化使用format方法完成:

course_code = "NUMA21"
print("This course's name is {}".format(course_code)) 
# This course's name is NUMA21

函数format是一个字符串方法;它扫描字符串以查找占位符,占位符用花括号括起来。这些占位符以 format 方法的参数指定的方式被替换。如何替换取决于每个{}对中定义的格式规范。格式规范以冒号(":")作为前缀。

format 方法提供了一系列可能性来根据对象的类型自定义对象的格式。科学计算中特别有用的是float类型的格式说明符。可以选择带有{:f}的标准或带有{:e}的指数符号:

quantity = 33.45
print("{:f}".format(quantity)) # 33.450000
print("{:1.1f}".format(quantity)) # 33.5
print("{:.2e}".format(quantity)) # 3.35e+01

格式说明符允许指定舍入精度(表示形式中小数点后的数字)。还可以设置包括前导空格在内的符号总数来表示数字。

在本例中,插入其值的对象的名称作为 format 方法的参数给出。第一对{}由第一个参数代替,后面的对由后面的参数代替。或者,使用键值语法可能也很方便:

print("{name} {value:.1f}".format(name="quantity",value=quantity))
# prints "quantity 33.5"

这里,处理两个值,一个没有格式说明符的字符串name和一个以小数点后一位数字的定点记数法打印的浮点数value。(有关字符串格式【34】的更多详细信息,请参考完整的参考文档)。

型式

绳子上的大括号

有时,一个字符串可能包含一对大括号,这不应该被认为是format方法的占位符。在这种情况下,使用双括号:

r"we {} in LaTeX \begin{{equation}}".format('like')

这将返回以下字符串:'we like in LaTeX \\begin{equation}'

总结

在本章中,您遇到了 Python 中的基本数据类型,并看到了相应的语法元素。我们将主要处理数字类型,如整数、浮点数和复数。

布尔函数是设置条件所必需的,通过使用字符串,我们经常会传递结果和消息。

练习

Ex。1 →检查 x = 2.3 是否为零的功能:

Exercises

Ex。2 → 根据德·莫伊弗公式,以下成立:

Exercises

选择数字 nx ,用 Python 验证该公式。

Ex。3 → 复数。用同样的方法验证欧拉公式:

Exercises

Ex。4 → 假设我们试图检查一个发散序列的收敛性(此处序列由递归关系unT5+1= 2unT11】和 u 0 = 1.0 定义)😗*

u = 1.0 # you have to use a float here!
uold = 10\. 
for iteration in range(2000):
    if not abs(u-uold) > 1.e-8:
         print('Convergence')
         break # sequence has converged
    uold = u
    u = 2*u
else:
    print('No convergence')
  1. 由于序列不收敛,代码应打印No convergence信息。执行它,看看会发生什么。

  2. What happens if you replace the line:

          if not abs(u-uold) > 1.e-8
    

    与:

          if abs(u-uold) < 1.e-8
    

    它应该给出完全相同的结果,不是吗?再次运行代码,看看会发生什么。

  3. 如果把 u=1.0 换成 u=1 (没有小数点)会怎么样。运行代码来检查您的预测。

  4. 解释这段代码的意外行为。理解发生了什么的关键是inf的求值结果为nan,而nan与其他任何东西的比较结果是始终返回值 False

Ex。5 → 一个含义C =(A B)是一个布尔表达式,定义为

  • CTrue如果 AFalse或者 AB 都是True

  • C is False otherwise

    写一个 Python 函数implication(A, B)

Ex。6 → 本练习是训练布尔运算。两个二进制数字(位)通过一种叫做半加法器的逻辑装置相加。它产生一个进位位(下一个较高值的数字)和下表定义的和,以及半加法器电路。

| **p** | **q** | **总和** | **携带** | | one | one | Zero | one | | one | Zero | one | Zero | | Zero | one | one | Zero | | Zero | Zero | Zero | Zero |

半加法器操作的定义

Exercises

图 2.3:半加法器电路

全加器由两个半加法器组成,对输入端的两位和一个附加进位进行求和(参见下图):

Exercises

图 2.4:全加器电路

写一个实现半加法器的函数和另一个实现全加器的函数。测试这些功能。

三、容器类型

容器类型用于将对象分组在一起。不同容器类型之间的主要区别在于访问单个元素的方式以及操作的定义方式。

列表

顾名思义,列表是任何类型的对象的列表:

L = ['a' 20.0, 5]
M = [3,['a', -3.0, 5]]

通过为每个元素分配一个索引来枚举各个对象。列表中的第一个元素获取索引 0。这种从零开始的索引常用于数学符号。考虑多项式系数的通常索引。

该索引允许我们访问以下对象:

L[1] # returns 20.0
L[0] # returns 'a'
M[1] # returns ['a',-3.0,5]
M[1][2] # returns 5

这里的括号符号对应于数学公式中下标的使用。L是一个简单的列表,而M本身包含一个列表,因此需要两个索引来访问内部列表的一个元素。

通过range命令可以很容易地生成包含后续整数的列表:

L=list(range(4)) # generates a list with four elements: [0, 1, 2 ,3]

更一般的用法是为该命令提供启动、停止和步进参数:

L=list(range(17,29,4)) # generates [17, 21, 25]

命令len返回列表的长度:

len(L) # returns 3

切片

ij之间分割一个列表会创建一个包含从index i开始到j之前结束的元素的新列表。

对于切片,必须给出一个索引范围。L[i:j]是指从L[i]开始到L[j-1]的所有元素创建一个列表。换句话说,新列表是通过从L中移除第一个i元素并获取下一个j-i元素(对于 j > i ≥ 0)而获得的。更多示例见下图(图 3.1 ):

Slicing

图 3.1:一些典型的切片情况

这里,L[i:]表示去掉第一个 i 元素,L[:i]表示只取第一个 i 元素,同样,L[:-i]表示去掉最后一个 i 元素,L[-i:]表示只取最后一个 i 元素。这可以在L[i:-j]中合并,删除第一个 i 和最后一个 j 元素:

L = ['C', 'l', 'o', 'u', 'd', 's']
L[1:5] # remove one element and take four from there:
# returns ['l', 'o', 'u', 'd']

可以省略切片的第一个或最后一个界限:

L = ['C', 'l', 'o', 'u','d', 's']
L[1:] # ['l', 'o', 'u', 'd','s']
L[:5] # ['C', 'l', 'o','u','d']
L[:] # the entire list

Python 允许使用负索引从右边开始计数。特别是元素L[-1]是列表L中的最后一个元素。

一些列表索引描述:

  • L[i:]相当于除了第一个 i 之外的所有元素
  • L[:i]等于取第一 i 元素
  • L[-i:]等于取最后的 i 元素
  • L[:-i]相当于除了最后一个 i 之外的所有元素

这里有一个例子:

L = ['C', 'l', 'o', 'u', 'd', 's']
L[-2:] # ['d', 's']
L[:-2] # ['C', 'l', 'o','u']

在这个范围内省略一个指数相当于ℝ.的半开区间半开区间(∞, a 表示,取所有严格低于 a 的数字;这类似于语法L[:j]

出界切片

请注意,对于超出界限的切片,您永远不会得到索引错误。你可能会得到空的列表。

这里有一个例子:

L = list(range(4)) # [0, 1, 2, 3]
L[4] # IndexError: list index out of range
L[1:100] # same as L[1:]
L[-100:-1] # same as L[:-1]
L[-100:100] # same as L[:]
L[5:0] # empty list []
L[-2:2] # empty list []

在索引中使用可能变成负数的变量时要小心,因为这会完全改变切片。这可能会导致意想不到的结果:

a = [1,2,3]
 for iteration in range(4): 
     print(sum(a[0:iteration-1]))

结果是3013,而人们期望的是0013

大步

在计算切片时,还可以指定步幅,即从一个索引到另一个索引的步长。默认步幅为 1。这里有一个例子:

L = list(range(100))
L[:10:2] # [0, 2, 4, 6, 8]
L[::20] # [0, 20, 40, 60, 80]
L[10:20:3] # [10, 13, 16, 19]

请注意,步幅也可能是负的:

L[20:10:-3] # [20, 17, 14, 11]

也可以使用负步幅创建一个反向的新列表(在就地操作部分找到关于反向的方法):

L = [1, 2, 3]
R = L[::-1] # L is not modified
R # [3, 2, 1]

更改列表

对列表的典型操作是插入和删除元素以及列表连接。有了切片符号,列表的插入和删除变得显而易见;删除只是将列表的一部分替换为一个空列表[]:

L = ['a', 1, 2, 3, 4]
L[2:3] = [] # ['a', 1, 3, 4]
L[3:] = [] # ['a', 1, 3]

插入意味着用要插入的列表替换空切片:

L[1:1] = [1000, 2000] # ['a', 1000, 2000, 1, 3]

两个列表通过加号运算符+连接在一起:

L = [1, -17]
M = [-23.5, 18.3, 5.0]
L + M # gives [1, -17, 23.5, 18.3, 5.0]

将一个列表n与其自身连接起来会激发乘法运算符*的使用:

n = 3
n * [1.,17,3] # gives [1., 17, 3, 1., 17, 3, 1., 17, 3]
[0] * 5 # gives [0,0,0,0,0]

列表上没有算术运算,如元素求和或除法。对于这样的操作,我们使用数组(参见数组一节)。

属于一个列表

可以使用关键字innot in来确定一个元素是否属于列表,这类似于数学中的Belonging to a listBelonging to a list:

L = ['a', 1, 'b', 2]
'a' in L # True
3 in L # False
4 not in L # True

列出方法

一些有用的list类型的方法收集在下面的 T 表 3.1 中:

| **命令** | **动作** | | `list.append(x)` | 在列表末尾添加`x`。 | | `list.expand(L)` | 通过列表`L`的元素展开列表。 | | `list.insert(i,x)` | 在`i`位置插入`x`。 | | `list.remove(x)` | 从列表中删除值为`x`的第一项。 | | `list.count(x)` | `x`在列表中出现的次数。 | | `list.sort()` | 对列表中的项目进行适当的排序。 | | `list.reverse()` | 将列表中的元素颠倒过来。 | | `list.pop()` | 就地删除列表的最后一个元素。 |

表 3.1:数据类型列表的方法

列表方法有两种作用方式:

  • 他们可以直接修改列表,也就是就地操作。
  • 他们产生了一个新的物体。

就地操作

产生列表的所有方法都是原位操作方法,例如reverse:

L = [1, 2, 3]
L.reverse() # the list
L is now reversed
L # [3, 2, 1]

注意就地操作。人们可能会想写道:

L=[3, 4, 4, 5]
newL = L.sort()

这是正确的 Python。但这可能会导致值为“T2”的变量“T1”中的“T0”发生意外变化。原因是sort运行到位。

在这里,我们演示就地操作方法:

L = [0, 1, 2, 3, 4]
L.append(5) # [0, 1, 2, 3, 4, 5]
L.reverse() # [5, 4, 3, 2, 1, 0]
L.sort() # [0, 1, 2, 3, 4, 5]
L.remove(0) # [1, 2, 3, 4, 5]
L.pop() # [1, 2, 3, 4]
L.pop() # [1, 2, 3]
L.extend(['a','b','c']) # [1, 2, 3, 'a', 'b', 'c']

L被改动。count方法是生成新对象的方法示例:

L.count(2) # returns 1

合并列表–压缩

列表的一个特别有用的功能是zip。它可以通过配对原始列表的元素来将两个给定列表合并成一个新列表。结果是元组列表(有关更多信息,请参考章节元组:

ind = [0,1,2,3,4]
color = ["red", "green", "blue", "alpha"]
list(zip(color,ind)) # gives [('red', 0), ('green', 1), 
                                          ('blue', 2), ('alpha', 3)]

此示例还演示了如果列表长度不同会发生什么。压缩列表的长度是两个输入列表中较短的一个。zip创建一个特殊的可迭代对象,该对象可以通过应用list函数变成一个列表,就像前面的例子一样。有关可迭代对象的更多详细信息,请参考第 9 章、迭代中的迭代器一节。

列表理解

建立列表的一个方便的方法是使用列表理解结构,里面可能有一个条件。列表理解的语法是:

[<expr> for <variable> in <list>]

或者更一般地说:

[<expr> for <variable> in <list> if <condition>]

这里有一个例子:

L = [2, 3, 10, 1, 5]
L2 = [x*2 for x in L] # [4, 6, 20, 2, 10]
L3 = [x*2 for x in L if 4 < x <= 10] # [20, 10]

列表理解中可能有几个for循环:

M = [[1,2,3],[4,5,6]]
flat = [M[i][j] for i in range(2) for j in range(3)] 
# returns [1, 2, 3, 4, 5, 6]

这在处理数组时特别有意义。

型式

设置符号

列表理解与集合的数学符号密切相关。比较:List comprehensionL2 = [2*x for x in L]

一个很大的区别是列表是有序的,而集合不是有序的(更多信息请参考集合一节)。

数组

NumPy 包提供数组,数组是用于操作向量、矩阵甚至数学中的高阶张量的容器结构。在本节中,我们指出了数组和列表之间的相似之处。但是数组值得更广泛的介绍,将在第 4 章线性代数-数组第 5 章高级数组概念中给出。

数组通过函数array从列表中构建:

v = array([1.,2.,3.])
A = array([[1.,2.,3.],[4.,5.,6.]])

要访问向量的一个元素,我们需要一个索引,而矩阵的一个元素由两个索引寻址:

v[2]     # returns 3.0
A[1,2]   # returns 6.0

乍一看,数组类似于列表,但请注意它们在根本上是不同的,这可以通过以下几点来解释:

  • 使用方括号和切片对数组数据的访问对应于对列表的访问。它们也可以用来改变数组:

            M = array([[1.,2.],[3.,4.]])
            v = array([1., 2., 3.])
            v[0] # 1
            v[:2] # array([1.,2.])
            M[0,1] # 2
            v[:2] = [10, 20] # v is now array([10., 20., 3.])
    
  • 向量中元素的数量,或者矩阵的行数,是通过函数len :

            len(v) # 3
    

    得到的

  • 数组只存储相同数字类型的元素(通常是floatcomplex,但也有int)。有关更多信息,请参考第 4 章、线性代数–数组中的数组属性一节。

  • 操作+*/-都是元素级的。dot函数以及在 Python 版本≥ 3.5 中,中缀运算符@用于标量积和相应的矩阵运算。

  • 与列表不同,数组没有append方法。然而,通过堆叠更小尺寸的数组来构造数组有特殊的方法(更多信息,请参考第 4 章线性代数-数组中的堆叠一节)。).一个相关的观点是数组不像列表那样有弹性;人们不能用切片来改变它们的长度。

  • 矢量切片是视图;也就是说,它们可以用来修改原始数组。更多信息请参考第五章高级数组概念中的数组视图和副本部分。

元组

元组是不可变的列表。不可变意味着不能修改。元组只是一个逗号分隔的对象序列(没有括号的列表)。为了增加可读性,人们通常在一对括号中包含一个元组:

my_tuple = 1, 2, 3     # our first tuple
my_tuple = (1, 2, 3)   # the same
my_tuple = 1, 2, 3,    # again the same
len(my_tuple) # 3, same as for lists
my_tuple[0] = 'a'   # error! tuples are immutable

逗号表示对象是一个元组:

singleton = 1,   # note the comma
len(singleton)   # 1

当一组值放在一起时,元组是有用的;例如,它们用于从函数中返回多个值(参见第 7 章函数中的返回值一节)。通过解包一个列表或元组,可以一次分配几个变量:

a, b = 0, 1 # a gets 0 and b gets 1
a, b = [0, 1] # exactly the same effect
(a, b) = 0, 1 # same
[a,b] = [0,1] # same thing

型式

交换技巧

使用打包和解包来交换两个变量的内容:a, b = b, a

总结一下:

  • 元组只不过是不可变的列表,带有不带括号的符号。
  • 在大多数情况下,可以使用列表代替元组。
  • 不带括号的符号很方便,但很危险。当您不确定时,应该使用括号:
      a, b = b, a # the swap trick; equivalent to:
      (a, b) = (b, a)
      # but
      1, 2 == 3, 4 # returns (1, False, 4) 
      (1, 2) == (3, 4) # returns False

词典

列表、元组和数组是有序的对象集。根据单个对象在列表中的位置来插入、访问和处理它们。另一方面,字典是无序的成对集合。人们通过键来访问字典数据。

创建和修改词典

例如,我们可以创建一个包含力学中刚体数据的字典,如下所示:

truck_wheel = {'name':'wheel','mass':5.7,
               'Ix':20.0,'Iy':1.,'Iz':17.,
               'center of mass':[0.,0.,0.]}

键/数据对由冒号“:”表示。这些对以逗号分隔,列在一对花括号{}中。

单个元素通过它们的键来访问:

truck_wheel['name']   # returns 'wheel'
truck_wheel['mass']   # returns 5.7

通过创建新关键字,新对象被添加到字典中:

truck_wheel['Ixy'] = 0.0

字典也用于为函数提供参数(更多信息,请参考第 7 章函数中的参数和参数部分)。字典中的键可以是字符串、函数、具有不可变元素的元组和类。键不能是列表或数组。命令dict从具有键/值对的列表中生成字典:

truck_wheel = dict([('name','wheel'),('mass',5.7),('Ix',20.0), 
                    ('Iy',1.), ('Iz',17.), 
                    ('center of mass',[0.,0.,0.])])

在这种情况下zip功能可能会派上用场(参考合并列表部分)。

遍历字典

主要有三种方式来循环字典:

  • 按键:
        for key in truck_wheel.keys():
            print(key) # prints (in any order) 'Ix', 'Iy', 'name',...

或者等效地:

        for key in truck_wheel:
            print(key) # prints (in any order) 'Ix', 'Iy', 'name',...
  • 按值:
        for value in truck_wheel.value():
            print(value) 
               # prints (in any order) 1.0, 20.0, 17.0, 'wheel', ...
  • 按项目,即键/值对:
        for item in truck_wheel.items():
            print(item) 
               # prints (in any order) ('Iy', 1.0), ('Ix, 20.0),...

文件访问专用字典对象请参考第十二章输入输出章节货架

集合是与数学中的集合共享属性和运算的容器。数学集合是不同对象的集合。以下是一些数学集合表达式:

Sets

以及它们的 Python 对应物:

A = {1,2,3,4}
B = {5}
C = A.union(B)   # returns set([1,2,3,4,5])
D = A.intersection(C)   # returns set([1,2,3,4])
E = C.difference(A)   # returns set([5])
5 in C   # returns True

集合只包含一个元素一次,对应于前面提到的定义:

A = {1,2,3,3,3}
B = {1,2,3}
A == B # returns True

一个集合是无序的;也就是说,没有定义集合中元素的顺序:

A = {1,2,3}
B = {1,3,2}
A == B # returns True

Python 中的集合可以包含所有类型的 hashable 对象,即数字对象、字符串和布尔值。

unionintersection两种方法:

A={1,2,3,4}
A.union({5})
A.intersection({2,4,6}) # returns set([2, 4])

另外,可以使用方法issubsetissuperset来比较集合:

{2,4}.issubset({1,2,3,4,5}) # returns True
{1,2,3,4,5}.issuperset({2,4}) # returns True

型式

空套

空集合在 Python 中是由empty_set=set([])定义的,而不是由{}定义的,后者会定义一个空字典!

容器转换

我们在下面的表 3.2 中总结了到目前为止呈现的容器类型的最重要的属性。数组将在第四章线性代数-数组中讨论。

Container conversions

表 3.2:容器类型

从上表中可以看出,访问容器元素是有区别的,集合和字典是没有顺序的。

由于各种容器类型的不同属性,我们经常将一种类型转换为另一种类型:

Container conversions

类型检查

查看变量类型的直接方法是使用type命令:

label = 'local error'
type(label) # returns str
x = [1, 2] # list
type(x) # returns list

但是,如果您想测试某个变量是否是某个类型,您应该使用isinstance(而不是将类型与type进行比较):

isinstance(x, list) # True

在阅读了第 8 章类、后,使用isinstance的原因变得显而易见,尤其是第 8 章子类化和继承一节中的子类化和继承的概念。简而言之,通常不同的类型与一些基本类型共享一些共同的属性。经典的例子是类型bool,它是从更一般的类型int子类化而来的。在这种情况下,我们看到命令isinstance如何以更通用的方式使用:

test = True
isinstance(test, bool) # True
isinstance(test, int) # True
type(test) == int # False
type(test) == bool # True

因此,为了确保变量test和整数一样好(具体类型可能无关),您应该检查它是否是integer的实例:

if isinstance(test, int):
    print("The variable is an integer")

类型检查

Python 不是类型化语言。这意味着对象是由它们能做什么而不是它们是什么来识别的。例如,如果您有一个通过使用len方法作用于对象的字符串操作函数,那么您的函数可能对实现该方法的任何对象都有用。

到目前为止,我们已经遇到了不同的类型:floatintboolcomplexlisttuplemodulefunctionstrdictarray

总结

在本章中,您学习了如何处理容器类型,主要是列表。了解如何填充这些容器以及如何访问它们的内容非常重要。我们看到,可以通过位置或关键字进行访问。

我们将在下一章的数组中再次遇到切片的重要概念。这些是专门为数学运算设计的容器。

练习

Ex。1 →执行以下语句:

    L = [1, 2]
    L3 = 3*L
  1. L3的内容是什么?

  2. 尝试预测以下命令的结果:

          L3[0]
          L3[-1]
          L3[10]
    
  3. 下面的命令是做什么的?

           L4 = [k**2 for k in L3]
    
  4. L3L4连接到新列表L5

Ex。2 →使用range命令和列表理解生成一个列表,其中 100 个等距值在 0 和 1 之间。

Ex。3 →假设以下信号存储在列表中:

    L = [0,1,2,1,0,-1,-2,-1,0]

结果是什么:

L[0]
L[-1]
L[:-1]
L + L[1:-1] + L
L[2:2] = [-3]
L[3:4] = []
L[2:5] = [-5]

只通过检查来做这个练习,也就是说,不使用您的 Python Shell。

Ex。4 →考虑 Python 语句:

L = [n-m/2 for n in range(m)]
ans = 1 + L[0] + L[-1]

并且假设变量m先前已经被分配了整数值。ans有什么价值?回答这个问题,不要执行 Python 中的语句。

Ex。5 →考虑递归公式:

Exercises

随着 n = 0,...,1000, h = 1/1000, a = -0.5。

  1. 创建列表u。存储在其前三个元素e0**ehaT8】和e2haT12】中。这些代表给定公式中的起始值 u 0u 1u 2 。根据递归公式建立完整的列表。**
  2. 构建第二个列表td,在其中存储数值 nh ,其中 n = 0,..., 1000.绘图tdu(更多信息请参考第 6 章绘图中的基本绘图一节)。制作第二个标绘差的标绘,即 |e n-un|,其中 t n 代表向量td内的值。设置轴标签和标题。

递归是求解微分方程u’= au的多步公式,初始值为 u(0) = u 0 = 1u n 近似于u(NH)= eAnhu0

Ex。6 →设置 AB 。集合(A \ B)∞(B \ A)称为两个集合的对称差。编写一个执行此操作的函数。将您的结果与命令的结果进行比较:

A.symmetric_difference(B).

Ex。7 →在 Python 中验证空集合是任意集合的子集的说法。

Ex。8 →学习器械包上的其他操作。您可以使用IPython的命令完成功能找到这些命令的完整列表。特别要学习updateintersection_update的方法。intersectionintersection_update有什么区别?

四、线性代数——数组

线性代数是计算数学的基本构件之一。线性代数的对象是向量和矩阵。NumPy 包包含了操作这些对象的所有必要工具。

第一个任务是建立矩阵和向量,或者通过切片来改变现有的矩阵和向量。另一个主要任务是dot运算,它体现了大部分线性代数运算(标量积、矩阵向量积、矩阵矩阵积)。最后,有各种方法可以用来解决线性问题。

数组类型概述

对于不耐烦的人来说,这里简单介绍一下如何使用数组。请注意,数组的行为起初可能令人惊讶,因此我们鼓励您在这一介绍部分之后继续阅读。

向量和矩阵

创建向量就像使用函数array将列表转换为数组一样简单:

v = array([1.,2.,3.])

物体v现在是一个向量,它的行为很像线性代数中的向量。我们已经强调了与 Python 中列表对象的区别(参见第 3 章容器类型中的数组一节)。下面是一些关于向量的基本线性代数运算的例子:

# two vectors with three components
v1 = array([1., 2., 3.])
v2 = array([2, 0, 1.])

# scalar multiplications/divisions
2*v1 # array([2., 4., 6.])
v1/2 # array([0.5, 1., 1.5])

# linear combinations
3*v1 # array([ 3., 6., 9.])
3*v1 + 2*v2 # array([ 7., 6., 11.])

# norm
from scipy.linalg import norm
norm(v1) # 3.7416573867739413
# scalar product
dot(v1, v2) # 5.
v1 @ v2 # 5 ; alternative formulation

请注意,所有基本算术运算都是按元素执行的:

# elementwise operations:
v1 * v2 # array([2., 0., 3.])
v2 / v1 # array([2.,0.,.333333])
v1 - v2 # array([-1., 2., 2.])
v1 + v2 # array([ 3., 2., 4.])

一些函数在元素上也作用于数组:

cos(v1) # cosine, elementwise: array([ 0.5403,
                                 -0.4161, -0.9899])

这个主题将在作用于数组的函数一节中讨论。

矩阵的创建方式类似于向量,但却是从列表中创建的:

M = array([[1.,2],[0.,1]])

向量是无列无行矩阵

n 向量、 n × 1 和 1 × n 矩阵是三个不同的对象,即使它们包含相同的数据。

为了创建包含与向量v = array([1., 2., 1.])相同数据的行矩阵,我们这样做:

R = array([[1.,2.,1.]]) # notice the double brackets: 
                        # this is a matrix
shape(R)                # (1,3): this is a row matrix

相应的列矩阵通过reshape方法得到:

C = array([1., 2., 1.]).reshape(3, 1)
shape(C) # (3,1): this is a column matrix

索引和切片

索引和切片类似于列表。主要区别在于,当数组是矩阵时,可能有几个索引或切片。主题将在数组索引一节中深入讨论;这里,我们只给出一些索引和切片的示例:

v = array([1., 2., 3])
M = array([[1., 2],[3., 4]])

v[0] # works as for lists
v[1:] # array([2., 3.])

M[0, 0] # 1.
M[1:] # returns the matrix array([[3., 4]])
M[1] # returns the vector array([3., 4.])

# access
v[0] # 1.
v[0] = 10

# slices
v[:2] # array([10., 2.])
v[:2] = [0, 1] # now v == array([0., 1., 3.])
v[:2] = [1, 2, 3] # error!

线性代数运算

执行大多数线性代数常规操作的基本运算符是 Python 函数dot。它用于矩阵向量乘法:

dot(M, v) # matrix vector multiplication; returns a vector
M @ v # alternative formulation

它可以用来计算两个向量之间的标量积:

dot(v, w) # scalar product; the result is a scalar
v @ w # alternative formulation

最后,它用于计算矩阵-矩阵乘积:

dot(M, N) # results in a matrix
M @ N # alternative formulation

求解线性系统

如果 A 是矩阵, b 是向量,可以解出线性方程:

Solving a linear system

使用solve方法,其语法如下:

from scipy.linalg import solve
x = solve(A, b)

例如,我们想解决:

Solving a linear system

以下是上述方程的解:

from scipy.linalg import solve
A = array([[1., 2.], [3., 4.]])
b = array([1., 4.])
x = solve(A, b)
allclose(dot(A, x), b) # True
allclose(A @ x, b) # alternative formulation

这里使用命令allclose来比较两个向量。如果它们彼此足够靠近,该命令返回True。可选地,可以设置公差值。有关线性方程组的更多方法,请参考 SciPy 中的线性代数方法一节。

数学预赛

为了理解数组在 NumPy 中是如何工作的,理解通过索引访问张量(矩阵和向量)元素和通过提供参数评估数学函数之间的数学并行是有用的。在这一节中,我们还讨论了点积作为约简算子的推广。

数组作为函数

可以从几个不同的角度来考虑数组。我们认为,为了理解数组,最有成果的是几个变量的函数。

例如,在 n 中选择给定向量的一个分量可能只是被认为是从ℕ n 集合到ℝ的一个函数,在这里我们定义集合:

Arrays as functions

这里设定ℕ nn 元素。Python 函数range生成ℕ n

另一方面,选择给定矩阵的元素是两个参数的函数,取其在ℝ.中的值因此,挑选一个m×nn矩阵的特定元素可以被认为是从ℕt5】mT7】×ℕT9】nt11】到ℝ.的函数

操作是元素级的

NumPy 数组本质上被视为数学函数。对于操作来说尤其如此。考虑两个函数, fg ,定义在同一个域上,取实值。这两个函数的乘积 f g 定义为逐点乘积,即:

Operations are elementwise

请注意,这种结构对于两种功能之间的任何操作都是可能的。对于在两个标量上定义的任意操作,我们在这里用Operations are elementwise表示,我们可以定义Operations are elementwise如下:

Operations are elementwise

这句无关痛痒的话让我们理解了 NumPy 在运营上的立场;所有操作在数组中都是元素式的。例如,两个矩阵 mn 之间的乘积定义为函数,如下所示:

Operations are elementwise

形状和尺寸数量

有一个明显的区别:

  • 标量:无参数函数
  • 向量:带一个参数的函数
  • 矩阵:双参数函数
  • 高阶张量:具有两个以上参数的函数

在接下来的内容中,维数是函数的参数数量。形状本质上对应于函数的定义范围。

例如,大小为 n 的向量是从集合ℕt5】nT7】到ℝ.的函数因此,其域的定义是ℕ n 。它的形状被定义为单例( n,)。类似地,大小为 m × n 的矩阵是在ℕ×t19】m×ℕ×t23】m×T25】上定义的函数。对应的形状就是简单的那对( mn )。数组的形状由numpy.shape函数获得,维数由numpy.ndim函数获得。

点操作

把数组当作函数,虽然很强大,却完全忽略了我们熟悉的线性代数结构,即矩阵-向量和矩阵-矩阵运算。幸运的是,这些线性代数运算可能都是以类似的统一形式编写的:

向量-向量运算:

The dot operations

矩阵向量运算:

The dot operations

矩阵-矩阵运算:

The dot operations

向量矩阵运算:

The dot operations

基本的数学概念是归约。对于矩阵向量运算,简化公式如下:

The dot operations

一般来说,在尺寸数量分别为 mn 的两个张量 TU 之间定义的归约运算可以定义为:

The dot operations

显然,张量的形状必须是相容的,这样运算才有意义。这个要求对于矩阵-矩阵乘法来说很熟悉。矩阵 MN 的乘法 M N 只有在 M 的列数等于 N 的行数时才有意义。

归约运算的另一个结果是它产生了一个新的张量,其维度为 m + n - 2 。在下表中,我们收集了涉及矩阵和向量的常见情况的约简操作的输出:

The dot operations

表 4.1:涉及矩阵和向量的常见情况的归约运算的输出

在 Python 中,所有归约操作都是使用dot函数执行的:

angle = pi/3
M = array([[cos(angle), -sin(angle)], 
           [sin(angle), cos(angle)]])
v = array([1., 0.])
y = dot(M, v)

就像在数学教科书中一样,也是在现代 Python(3.5 版及更高版本)中,点积有时更倾向于以其运算符形式dot(M, v)编写,或者使用更方便的中缀符号M @ v。从现在开始我们坚持运算符形式;如果首选其他形式,您可以修改示例。

元素对矩阵乘法

乘法运算符*总是元素式的。与点运算无关。即使 A 是矩阵, v 是向量, Av* 依然是合法操作。

使用dot函数执行矩阵向量乘法。更多信息请参考第五章高级阵概念广播部分。

数组类型

在 NumPy 中用于操作向量、矩阵和更一般的张量的对象称为数组。在本节中,我们将研究它们的基本属性,如何创建它们,以及如何访问它们的信息。

数组属性

数组本质上有三个特性,如下表所示(表 4.2 ):

| **名称** | **描述** | | `shape` | 它描述了应该如何将数据解释为向量、矩阵或高阶张量,并给出了相应的维数。使用`shape`属性访问。 | | `dtype` | 它给出了底层数据的类型(浮点、复数、整数等)。 | | `strides` | 该属性指定数据的读取顺序。例如,矩阵可以一列一列(FORTRAN 惯例)或一行一行(C 惯例)连续存储在内存中。该属性是一个元组,其字节数必须在内存中跳过才能到达下一行,字节数必须跳过才能到达下一列。`strides`属性甚至允许对内存中的数据进行更灵活的解释,这使得数组视图成为可能。 |

表 4.2:数组的属性

考虑以下数组:

A = array([[1, 2, 3], [3, 4, 6]])
A.shape   # (2, 3)
A.dtype   # dtype('int64')
A.strides # (24, 8)

其元素有'int64'型;也就是说,它们在内存中使用 64 位或 8 字节。完整的数组以行的方式存储在内存中。因此从A[0, 0]到下一行A[1,0]中第一个元素的距离在内存中是 24 字节(三个矩阵元素)。相应地,A[0,0]A[0,1]在内存中的距离是 8 字节(一个矩阵元素)。这些值存储在属性strides中。

从列表创建数组

创建数组的一般语法是函数array。创建真实向量的语法是:

V = array([1., 2., 1.], dtype=float)

要创建具有相同数据的复杂向量:

V = array([1., 2., 1.], dtype=complex)

当没有指定类型时,类型被猜测。array功能选择允许存储所有指定值的类型:

V = array([1, 2]) # [1, 2] is a list of integers
V.dtype # int
V = array([1., 2]) # [1., 2] mix float/integer
V.dtype # float
V = array([1\. + 0j, 2.]) # mix float/complex
V.dtype # complex

静默类型转换 NumPy 静默地将浮点转换为整数,这可能会产生意想不到的结果:

a = array([1, 2, 3])
a[0] = 0.5
a # now: array([0, 2, 3])

从复杂到浮点,经常会发生同样的意外数组类型转换。

数组和 Python 括号

正如我们在第一章入门程序和程序流程一节中所注意到的,当某些左大括号或括号没有关闭时,Python 允许换行。这为数组创建提供了一种方便的语法,使其更符合人眼的感受:

 # the identity matrix in 2D
 Id = array([[1., 0.], [0., 1.]])
 # Python allows this:
 Id = array([[1., 0.],
             [0., 1.]])
 # which is more readable

访问数组条目

数组条目由索引访问。与向量系数相反,需要两个索引来访问矩阵系数。这些在一对括号中给出。这将数组语法与列表区分开来。在那里,需要两对括号来访问元素。

M = array([[1., 2.],[3., 4.]])
M[0, 0] # first row, first column: 1.
M[-1, 0] # last row, first column: 3.

基本数组切片

切片类似于列表的切片,只是现在可能有多个维度:

  • M[i,:]是由 M. 的行 i 填充的向量
  • M[:,j]是由 M.i 列填充的向量
  • M[2:4,:]只是行上2:4的一部分。
  • M[2:4,1:4]是行和列上的切片。

矩阵切片的结果如下图所示(图 4.1 ):

Basic array slicing

图 4.1:矩阵切片的结果

省略一个尺寸

如果省略索引或切片,NumPy 会假设您只接受行。M[3]是一个向量,它是第三行 M 上的视图,M[1:3]是一个矩阵,它是第二行和第三行M上的视图

更改切片的元素会影响整个数组:

v = array([1., 2., 3.])
v1 = v[:2] # v1 is array([1., 2.])
v1[0] = 0\. # if v1 is changed ...
v # ... v is changed too: array([0., 2., 3.])

一般切片规则见下表(表 4.3) :

Basic array slicing

表 4.3:一般切片规则

形状为 (4,4) 的数组M的切片操作结果如下表所示(表 4.4 ):

Basic array slicing

表 4.4:形状为(4,4)的数组 M 的切片操作结果

使用切片改变数组

您可以使用切片或直接访问来更改数组。以下仅改变 5 × 3 矩阵M中的一个元素:

M[1, 3] = 2.0 # scalar

但是我们可以改变矩阵的一整行:

M[2, :] = [1., 2., 3.] # vector

我们也可以替换一个完整的子矩阵:

M[1:3, :] = array([[1., 2., 3.],[-1.,-2., -3.]])

列矩阵和向量是有区别的。以下带有列矩阵的赋值不返回错误M[1:4, 2:3] = array([[1.],[0.],[-1.0]]),而带有向量的赋值返回Value Error M[1:4, 2:3] = array([1., 0., -1.0]) #  error

一般切片规则见表 4.2 。前面例子中的矩阵和向量必须有合适的大小以适合矩阵 M 。您也可以利用广播规则(有关更多信息,请参考第 5 章、高级数组概念广播部分)来确定替换数组的允许大小。如果替换数组没有正确的形状,将引发ValueError异常。

构造数组的函数

设置数组的常用方法是通过列表。但是也有一些生成特殊数组的简便方法,如下表所示(表 4.5 ):

| **方法** | **形状** | **生成** | | `zeros((n,m))` | *(n,m)* | 用零填充的矩阵 | | `ones((n,m)) ` | *(n,m)* | 用 1 填充的矩阵 | | `diag(v,k) ` | *(n,n)* | 向量 *v* 的(亚,超)对角矩阵 | | `random.rand(n,m) ` | *(n,m)* | (0,1)中均匀分布随机数填充的矩阵 | | `arange(n)` | *(n),* | 第一 *n* 个整数 | | `linspace(a,b,n) ` | *(n),* | 在 *a* 和 *b* 之间等间距点的向量 |

表 4.5:创建数组的命令

这些命令可能需要额外的参数。特别是zerosonesarange命令将dtype作为可选参数。默认类型为float,除了arange。还有zeros_like``ones_like等方法,都是前面方法的轻微变形。比如zeros_like(A)法相当于zeros(shape(A))

这里是identity函数,它构造一个给定大小的单位矩阵:

I = identity(3)

该命令与以下命令相同:

I = array([[ 1., 0., 0.],
           [ 0., 1., 0.],
           [ 0., 0., 1.]])

访问和更改形状

维数是向量和矩阵的区别。形状是区分不同大小的向量或不同大小的矩阵的东西。在本节中,我们将研究如何获取和更改数组的形状。

形状函数

矩阵的形状是其维度的元组。n × m 矩阵的形状是元组(n, m)。可以通过shape功能获得:

M = identity(3)
shape(M) # (3, 3)

对于向量,形状是包含向量长度的单例:

v = array([1., 2., 1., 4.])
shape(v) # (4,) <- singleton (1-tuple)

另一种方法是使用数组属性shape,它给出了相同的结果:

M = array([[1.,2.]])
shape(M) # (1,2)
M.shape # (1,2)

然而,使用shape作为函数的优点是这个函数也可以用在标量和列表上。当代码应该同时使用标量和数组时,这可能会派上用场:

shape(1.) # ()
shape([1,2]) # (2,)
shape([[1,2]]) # (1,2)

维数

数组的维数通过函数numpy.ndim或使用数组属性ndarray.ndim获得:

ndim(A) # 2
A.ndim # 2

请注意,张量T(向量、矩阵或高阶张量)的函数ndim给出的维数始终等于其形状的长度:

T = zeros((2,2,3)) # tensor of shape (2,2,3); three dimensions
ndim(T) # 3
len(shape(T)) # 3

重塑

方法reshape在不复制数据的情况下,以新的形状给出数组的新视图:

v = array([0,1,2,3,4,5])
M = v.reshape(2,3)
shape(M) # returns (2,3)
M[0,0] = 10 # now v[0] is 10

型式

重塑不复制

重塑不会创建新数组。相反,它给出了现有数组的新视图。在前面的例子中,改变M的一个元素会自动导致v中相应元素的改变。当这种行为不可接受时,您需要复制数据。

reshape方法对由arange(6)定义的数组的各种影响如下图所示:

Reshape

图 4.2:整形方法对由 arange(6)定义的数组的各种影响

如果试图用不与原始形状相乘的形状来重塑数组,则会出现错误:

 ValueError: total size of new array must be unchanged.

有时,只指定一个形状参数,并让 Python 以乘以原始形状的方式确定另一个形状参数会很方便。这通过设置自由形状参数-1来完成:

v = array([1, 2, 3, 4, 5, 6, 7, 8])
M = v.reshape(2, -1)
shape(M) # returns (2, 4)
M = v.reshape(-1, 2)
shape(M) # returns (4,2 )
M = v.reshape(3,- 1) # returns error

转置

一种特殊的重塑形式是换位。它只是切换了矩阵的两个形状元素。矩阵 A 的转置是矩阵 B ,这样:

Transpose

这可以通过以下方式解决:

A = ...
shape(A) # 3,4

B = A.T # A transpose
shape(B) # 4,3

型式

转置不复制

换位和重塑非常相似。特别是,它也不复制数据,只返回同一数组上的视图:

A= array([[ 1., 2.],[ 3., 4.]])
B=A.T
A[1,1]=5.
B[1,1] # 5

由于向量是一维的张量,也就是一个变量的函数,所以变换向量没有意义。但是,NumPy 将遵守并返回完全相同的对象:

v = array([1., 2., 3.])
v.T # exactly the same vector!

当你想转置一个向量时,你想到的可能是创建一个行或列矩阵。这是使用reshape完成的:

v.reshape(-1, 1) # column matrix containing v
v.reshape(1, -1) # row matrix containing v

堆垛

从几个(匹配的)子矩阵构建矩阵的通用方法是concatenate。它的语法是:

concatenate((a1, a2, ...), axis = 0)

当指定axis=0时,该命令垂直堆叠子矩阵(一个在另一个之上)。通过axis=1参数,它们被水平堆叠,这根据更多维度的数组进行了推广。这个函数由几个方便的函数调用,如下所示:

  • hstack:用于水平堆叠矩阵
  • vstack:用于垂直堆叠矩阵
  • columnstack:用于在列中堆叠向量

叠加向量

可以使用vstackcolumn_stack逐行或逐列堆叠向量,如下图所示:

Stacking vectors

型式

hstack将产生 v1 和 v2 的连接。

让我们考虑辛排列作为向量叠加的例子:我们有一个大小为 2 n 的向量。我们要对一个有偶数个分量的向量进行辛变换,也就是用符号变化的向量的后半部分交换前半部分:

Stacking vectors

该操作在 Python 中的解析如下:

# v is supposed to have an even length.
def symp(v):
    n = len(v) // 2 # use the integer division //
    return hstack([v[-n:], -v[:n]])

作用于数组的函数

作用于数组的函数有不同的类型。有些是元素行为,它们返回一个相同形状的数组。这些被称为通用函数。其他数组函数返回不同形状的数组。

通用功能

通用函数是作用于数组元素的函数。因此,它们具有与输入数组形状相同的输出数组。这些函数允许我们一次计算一个标量函数在整个数组上的结果。

内置通用功能

一个典型的例子是cos功能(NumPy 提供的功能):

cos(pi) # -1
cos(array([[0, pi/2, pi]])) # array([[1, 0, -1]])

请注意,通用函数以组件方式在数组上工作。对于运算符也是如此,例如乘法或指数:

2 * array([2, 4]) # array([4, 8])
array([1, 2]) * array([1, 8]) # array([1, 16])
array([1, 2])**2 # array([1, 4])
2**array([1, 2]) # array([1, 4])
array([1, 2])**array([1, 2]) # array([1, 4])

创建通用函数

如果您在其中仅使用通用函数,您的函数将自动通用。但是,如果您的函数使用的函数不是通用的,那么当您试图将标量结果应用于数组时,可能会得到标量结果,甚至会出现错误:

def const(x):
    return 1
const(array([0, 2])) # returns 1 instead of array([1, 1])

另一个例子如下:

def heaviside(x):
    if x >= 0:
        return 1.
    else: 
        return 0.

heaviside(array([-1, 2])) # error

预期的行为是应用于向量的heaviside函数将返回[heaviside(*a*), heaviside(*b*)]。唉,这不起作用,因为函数总是返回一个标量,不管输入参数的大小如何。此外,将函数用于数组输入会引发异常。NumPy 功能vectorize可以让我们快速解决这个问题:

vheaviside = vectorize(heaviside)
vheaviside(array([-1, 2])) # array([0, 1]) as expected

该方法的典型应用是在绘制函数时使用:

xvals = linspace(-1, 1, 100)
plot(xvals, vectorize(heaviside)(xvals))
axis([-1.5, 1.5, -0.5, 1.5])

下图显示了 heaviside 函数:

Create universal functions

型式

vectorize功能并不能提高的性能。它只提供了一种快速转换函数的便捷方式,因此它可以对列表和数组进行元素操作。

数组函数

有许多作用于数组的函数在组件上不起作用。这种功能的例子有maxminsum。这些函数可以对整个矩阵、行或列进行操作。当没有提供参数时,它们作用于整个矩阵。假设A为以下矩阵:

Array functions

作用于该矩阵的sum函数返回一个标量:

sum(A) # 36

该命令有一个可选参数axis。它允许我们选择沿哪个轴执行操作。例如,如果轴是 0 ,这意味着总和应该沿着第一个轴计算。形状数组( mn )沿轴 0 的和将是长度向量 n

假设我们沿着轴 0 计算A的和:

sum(A, axis=0) # array([ 6, 8, 10, 12])

这相当于计算各列的总和:

Array functions

结果是一个向量:

Array functions

现在假设我们沿着轴 1 计算总和:

A.sum(axis=1) # array([10, 26])

这相当于计算行的总和:

Array functions

结果是一个向量:

Array functions

SciPy 中的线性代数方法

SciPy 在其scipy.linalg模块中提供了大量的数值线性代数方法。这些方法中有许多是来自LAPACK的 Python 包装程序,T1 是一个公认的 FORTRAN 子程序的集合,用于解决线性方程系统和特征值问题。线性代数方法是科学计算中任何方法的核心,SciPy 使用包装器而不是纯 Python 代码的事实使得这些中心方法速度极快。我们在这里详细介绍了如何用 SciPy 解决两个线性代数问题,让你对这个模块有所了解。

用 LU 求解几个线性方程组

A 成为 n × n 矩阵,b1**b2,..., b kn 的序列-向量。我们考虑问题找到 n 向量 x i 这样:

Solving several linear equation systems with LU

我们假设向量bIT3】不是同时已知的。特别是 i 个问题必须在 b i+1 可用之前解决,这种情况相当普遍。

LU 分解是一种组织经典高斯消元法的方法,其计算分两步进行:

  • 矩阵的因式分解步骤 A 以获得三角形形式的矩阵
  • 一个相对便宜的向后和向前淘汰步骤,适用于bIT3】,并受益于更耗时的因子分解步骤

该方法还利用了这样的事实:如果 P 是置换矩阵,使得 PA 是其行被置换的原始矩阵。

两个系统

Solving several linear equation systems with LU

有相同的解决方案。

LU 因式分解找到排列矩阵 P 、下三角矩阵 L、和上三角矩阵 U ,使得:

Solving several linear equation systems with LU

这样的因式分解一直存在。此外, L 可以这样确定: L ii = 1 。因此,必须存储的来自 L 的基本数据是 L iji > j 。因此, LU 可以一起存储在一个 n × n 数组中,而关于置换矩阵 P 的信息只需要一个 n 整数向量——旋转向量。

在 SciPy 中,有两种方法来计算 LU 因子分解。标准的是scipy.linalg.lu,返回三个矩阵LUP。另一种方法是lu_factor.这就是我们在这里描述的方法,因为它将在以后与lu_solve结合使用时非常方便:

import scipy.linalg as sl
[LU,piv] = sl.lu_factor(A)

在这里,A矩阵被分解,并且带有关于LU的信息的数组连同枢轴向量一起被返回。有了这个信息,系统就可以根据枢轴向量中存储的信息进行向量 b i 的行互换,使用 U、进行后向替换,最后使用 L 进行前向替换来求解。这在 Python 中捆绑在lu_solve方法中。下面的代码片段显示了一旦执行 LU 因式分解并且其结果存储在元组(LU, piv)中,系统AxI= bIT18】是如何求解的:

import scipy.linalg as sl
xi = sl.lu_solve((LU, piv), bi)

用奇异值分解求解最小二乘问题

一个线性方程组 Ax = b ,其中 Am × n 矩阵,mT19n,称为超定线性方程组。一般来说,它没有经典解,人们寻找一个向量x **Solving a least square problem with SVDn*,其性质为:

Solving a least square problem with SVD

这里,Solving a least square problem with SVD表示欧几里德向量范数Solving a least square problem with SVD

这个问题叫做最小二乘问题。一个稳定的解决方法是基于因子分解A = UσVT,其中 U 为一个 m × m 正交矩阵,VAn×n正交矩阵,σ=(σ×T19】ij)an【t2t 这种分解称为奇异值分解 ( SVD )。

我们写作,

Solving a least square problem with SVD

用对角线 n × n 矩阵σT5】1T7】。如果我们假设 A 有满秩,那么σ1是可逆的,可以看出,Solving a least square problem with SVD。如果我们将U=[U1U2]与 U 1 同为一个 m × n 子矩阵,那么前面的方程可以简化为Solving a least square problem with SVD

SciPy 提供了一个名为svd的函数,我们用它来解决这个任务:

import scipy.linalg as sl 
[U1, Sigma_1, VT] = sl.svd(A, full_matrices = False,
                              compute_uv = True) 
xast = dot(VT.T, dot(U1.T, b) / Sigma_1)
r = dot(A, xast) - b # computes the residual
nr = sl.norm(r, 2) # computes the Euclidean norm of r

关键字full_matrices表示只需要计算 U 的部分 U 1 。由于人们经常使用svd只计算奇异值,σ ii ,我们不得不使用关键字compute_uv明确要求计算 UV 。SciPy 函数scipy.linalg.lstsq通过使用奇异值分解类似地解决最小二乘问题。

更多方法

在到目前为止的例子中,你遇到了线性代数中计算任务的几种方法,例如solve。最常见的方法是在执行import scipy.linalg as sl命令后。我们参考了他们的文档以获得进一步的参考。scipy.linalg模块的一些线性代数函数如下表所示(表 4.6 ):

| **方法** | **描述(矩阵方法)** | | `sl.det` | 矩阵的行列式 | | `sl.eig` | 矩阵的特征值和特征向量 | | `sl.inv` | 矩阵求逆 | | `sl.pinv` | 矩阵伪逆 | | `sl.norm` | 矩阵或向量范数 | | `sl.svd` | 奇异值分解 | | `sl.lu` | 逻辑单元分解 | | `sl.qr` | QR 分解 | | `sl.cholesky` | 乔莱斯基分解 | | `sl.solve` | 一般或对称线性系统的解: *Ax = b* | | `sl.solve.banded` | 带状矩阵也是如此 | | `sl.lstsq` | 最小二乘解 |

表 4.6:scipy . linalg模块的线性代数函数

先执行import scipy.linalg as sl

总结

在这一章中,我们研究了线性代数中最重要的对象——向量和矩阵。为此,我们学习了如何定义数组,并遇到了重要的数组方法。一小部分演示了如何使用scipy.linalg中的模块来解决线性代数中的中心任务。

练习

Ex。1 →考虑 4 × 3 矩阵 M :

Exercises

  1. 使用函数array在 Python 中构造这个矩阵。
  2. 使用函数arange构建相同的矩阵,然后进行适当的整形。
  3. 表达式M[2,:]的结果是什么?类似的表述M[2:]的结果是什么?

Ex。2 →给定一个向量 x ,用 Python 构造以下矩阵:

Exercises

这里, x i 是向量 x 的分量(从零开始编号)。给定一个向量 y ,用 Python 求解线性方程组 Va = y 。让 a 的分量用 a i 表示,i = 0,...,5 。编写一个函数poly,该函数以 az 为输入,并计算多项式:

Exercises

画出这个多项式,在同一个图中把点( x iy i )描绘成小星星。用向量试试你的代码:

  • x = (0.0,0.5,1.o,1.5,2.0,2.5)
  • y = (-2.0,0.5,-2.0,1.0,-0.5,1.0)

Ex。3 →矩阵 VEx。2 叫做范德蒙矩阵。可以通过vander命令直接在 Python 中设置。评估由系数向量定义的多项式可以通过 Python 命令polyval来完成。重复 Ex。2 通过使用这些命令。

Ex。4 →让 u 成为一维数组。用值ξT5【I】T6 =(u1+uI+1+uI+2)/3构造另一个数组ξ。在统计学上,这个数组叫做 u 的移动平均线。在近似理论中,它扮演着三次样条的格雷维尔横坐标的角色。尽量避免在脚本中使用 for 循环。

前。5 →。

  1. 根据例中给出的矩阵 V 构建。2 矩阵 A 通过删除 V 的第一列。
  2. 组成矩阵B =(ATA)-1ATT7】。
  3. 使用来自 Ex 的 y 计算 c = B y 。2
  4. 使用 cpolyval绘制 c 定义的多项式。再在同一幅图中画出各点(xIT8、 y i )。

Ex。6Ex。5 描述最小二乘法。重复该练习,但使用 SciPy 的scipy.linalg.lstsq方法代替。

Ex。7 →让 v 是以 3 × 1 矩阵[1 -1 1] T 的坐标形式写成的向量。构建投影矩阵:

Exercises

通过实验证明 v 是两个矩阵 PQ 的特征向量。对应的特征值是什么?

Ex。8 →在数值线性代数中 m × m 矩阵 A 具有以下性质

Exercises

在执行 LU 因子分解时,用作极端生长因子的示例。

在 Python 中为各种 m 建立该矩阵,使用命令scipy.linalg.lu计算其 LU 因子分解,并通过实验得出关于生长因子的陈述

Exercises

关于 m。

五、高级数组概念

在本章中,我们将解释数组的一些更高级的方面。首先,我们将介绍数组视图的概念,然后是布尔数组以及如何比较数组。我们简要描述索引和矢量化,解释稀疏数组,以及广播等一些特殊主题。

数组视图和副本

为了精确控制内存的使用方式,NumPy 提供了数组视图的概念。视图是较小的数组,与较大的数组共享相同的数据。这就像引用一个单独的对象一样(参见第一章入门一节基本类型)。

数组视图

一个最简单的视图示例由一个数组的切片给出:

M = array([[1.,2.],[3.,4.]])
v = M[0,:] # first row of M

前面的切片是M的视图。它与M共享相同的数据。修改v也会修改M:

v[-1] = 0.
v # array([[1.,0.]])
M # array([[1.,0.],[3.,4.]]) # M is modified as well

可以使用数组属性base访问拥有数据的对象:

v.base # array([[1.,0.],[3.,4.]])
v.base is M # True

如果数组拥有其数据,则属性基为 none:

M.base # None

切片作为视图

哪些切片将返回视图,哪些将返回副本,都有精确的规则。只有基本切片(主要是带有:的索引表达式)返回视图,而任何高级选择(例如用布尔值切片)都将返回数据的副本。例如,可以通过列表(或数组)索引来创建新的矩阵:

a = arange(4) # array([0.,1.,2.,3.])
b = a[[2,3]] # the index is a list [2,3]
b # array([2.,3.])
b.base is None # True, the data was copied
c = a[1:3]
c.base is None # False, this is just a view

在前面的例子中,数组b不是视图,而用更简单的切片获得的数组c是视图。

有一个特别简单的数组切片,它返回整个数组的视图:

N = M[:] # this is a view of the whole array M

作为视图进行置换和重塑

其他一些重要的操作返回视图。例如,转置返回一个视图:

M = random.random_sample((3,3))
N = M.T
N.base is M # True

这同样适用于所有整形操作:

v = arange(10)
C = v.reshape(-1,1) # column matrix
C.base is v # True

数组副本

有时需要明确请求复制数据。这可以简单地通过名为array的 NumPy 函数来实现:

M = array([[1.,2.],[3.,4.]])
N = array(M.T) # copy of M.T

我们可能会验证数据确实已被复制:

N.base is None # True

比较数组

比较两个数组并不像看起来那么简单。考虑下面的代码,它旨在检查两个矩阵是否彼此接近:

A = array([0.,0.])
B = array([0.,0.])
if abs(B-A) < 1e-10: # an exception is raised here
    print("The two arrays are close enough")

当执行if语句时,该代码引发异常:

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

在本节中,我们将解释为什么会这样,以及如何补救这种情况。

布尔数组

布尔数组对于高级数组索引很有用(参见用布尔数组索引一节)。布尔数组只是一个条目类型为bool的数组:

A = array([True,False]) # Boolean array
A.dtype # dtype('bool')

任何作用于数组的比较运算符都将创建一个布尔数组,而不是简单的布尔:

M = array([[2, 3],
           [1, 4]])
M > 2 # array([[False, True],
             # [False, True]])
M == 0 # array([[False, False],
             # [False, False]])
N = array([[2, 3],
           [0, 0]])
M == N # array([[True, True],
              # [False, False]])
...

请注意,因为数组比较创建布尔数组,所以不能在条件语句中直接使用数组比较,例如,if语句。解决方法是使用方法allany:

A = array([[1,2],[3,4]])
B = array([[1,2],[3,3]])
A == B # creates array([[True, True], [True, False]]) 
(A == B).all() # False
(A != B).any() # True
if (abs(B-A) < 1e-10).all():
    print("The two arrays are close enough")

检查是否相等

检查两个浮点数组的相等性并不是直截了当的,因为两个浮点可能非常接近而不相等。在 NumPy 中,可以检查与allclose是否相等。该函数检查给定精度下两个数组的相等性:

data = random.rand(2)*1e-3
small_error = random.rand(2)*1e-16
data == data + small_error # False
allclose(data, data + small_error, rtol=1.e-5, atol=1.e-8)   # True

公差以相对公差界限rtol和绝对误差界限atol给出。命令allclose是:(abs(A-B) < atol+rtol*abs(B)).all()的缩写。

注意allclose也可以应用于标量:

data = 1e-3
error = 1e-16
data == data + error # False
allclose(data, data + error, rtol=1.e-5, atol=1.e-8)  #True

数组上的布尔运算

布尔数组中不能使用andornot。事实上,这些操作符强制从数组转换为布尔值,这是不允许的。相反,我们可以使用下表中给出的运算符(表 5.1 )对布尔数组进行组件逻辑运算:

| **逻辑运算符** | **布尔数组的替换** | | `A and B` | `A & B` | | `A or B` | `A | B` | | `not A` | `~ A` |

表 5.1 逻辑运算符和、或和不适用于数组。

A = array([True, True, False, False])
B = array([True, False, True, False])
A and B # error!
A & B # array([True, False, False, False])
A | B # array([True, True, True, False])
~A # array([False, False, True, True])

以下是逻辑运算符在布尔数组中的用法示例:

假设我们有一系列的数据被一些测量误差破坏了。进一步假设我们运行一个回归,它给出了每个值的偏差。我们希望获得所有异常值和所有偏差小于给定阈值的值:

data = linspace(1,100,100) # data
deviation = random.normal(size=100) # the deviations 
           #don't forget the parentheses in next statement!
exceptional = data[(deviation<-0.5)|(deviation>0.5)] 
exceptional = data[abs(deviation)>0.5] # same result 
small = data[(abs(deviation)<0.1)&(data<5.)] # small deviation and data

数组索引

我们已经看到,可以通过切片和整数的组合来索引数组,这是基本的切片技术。然而,还有很多可能性,允许多种方式来访问和修改数组元素。

用布尔数组索引

根据数组的值,只访问和修改数组的一部分通常很有用。例如,人们可能想要访问数组的所有正元素。事实证明,使用布尔数组是可能的,布尔数组就像掩码一样,只选择数组的某些元素。这种索引的结果是总是一个向量。例如,考虑以下示例:

B = array([[True, False],
           [False, True]])
M = array([[2, 3],
           [1, 4]])
M[B] # array([2,4]), a vector

其实M[B]通话相当于M.flatten()[B]。然后,可以用另一个向量替换结果向量。例如,可以用零替换所有元素(更多信息请参考广播部分):

M[B] = 0
M # [[0, 3], [1, 0]]

或者可以用其他值替换所有选定的值:

M[B] = 10, 20
M # [[10, 3], [1, 20]]

通过结合创建布尔数组(M > 2)、智能索引(用布尔数组索引)和广播,可以使用以下优雅的语法:

M[M>2] = 0    # all the elements > 2 are replaced by 0

这里的表达广播指的是标量 0 到适当形状的向量的默认转换。

使用位置

命令where给出了一个有用的构造,它可以将布尔数组作为一个条件,并返回满足该条件的数组元素的索引,或者根据布尔数组中的值返回不同的值。

基本结构是:

where(condition, a, b)

这将在条件为True时返回a的值,在条件为False时返回b的值。

例如,考虑一个重量函数:

Using where

下面的代码实现了一个 Heaviside 函数:

def H(x):
    return where(x < 0, 0, 1)
x = linspace(-1,1,11)  # [-1\. -0.8 -0.6 -0.4 -0.2 0\. 0.2 0.4 0.6 0.8 1\. ]
print(H(x))            # [0 0 0 0 0 1 1 1 1 1 1]

第二个和第三个参数可以是与条件大小相同的数组(布尔数组),也可以是标量。我们再举两个例子来演示如何根据条件操作数组或标量中的元素:

x = linspace(-4,4,5)
# [-4\. -2.  0.  2.  4.]

print(where(x > 0, sqrt(x), 0))
# [ 0.+0.j 0.+0.j 0.+0.j 1.41421356+0.j  2.+0.j ]
print(where(x > 0, 1, -1)) # [-1 -1 -1  1  1]

如果省略第二个和第三个参数,则返回包含满足条件的元素索引的元组。

例如,考虑以下代码中只有一个参数的where的使用:

a = arange(9)
b = a.reshape((3,3))

print(where(a > 5))   # (array([6, 7, 8]),)

print(where(b > 5))   # (array([2, 2, 2]), array([0, 1, 2]))

性能和矢量化

说到 Python 代码的性能,通常归结为解释代码和编译代码之间的区别。Python 是一种解释的编程语言,基本的 Python 代码是直接执行的,不需要对机器代码进行任何中间编译。使用编译语言,代码需要在执行前被翻译成机器指令。

解释语言的好处很多,但是解释代码在速度上无法与编译代码竞争。为了让你的代码更快,你可以用像 FORTRAN、C 或 C++这样的编译语言编写一些部分。这就是 NumPy 和 SciPy 所做的。

因此,最好尽可能在解释版本上使用 NumPy 和 SciPy 中的函数。矩阵乘法、矩阵向量乘法、矩阵分解、标量积等 NumPy 数组运算的速度比任何纯 Python 等价类都要快得多。考虑标量积的简单情况。标量积比编译的 NumPy 函数dot(a,b)慢得多(对于大约有 100 个元素的数组,慢 100 多倍):

def my_prod(a,b):
    val = 0
    for aa,bb in zip(a,b):
        val += aa*bb
    return val

测量函数的速度是科学计算的一个重要方面。测量执行时间详见第十三章测试测量执行时间一节。

矢量化

为了提高性能,必须经常向量化代码。用 NumPy 切片、操作和函数替换for循环和其他较慢的代码部分可以带来显著的改进。例如,通过迭代元素将标量简单地添加到向量中是非常慢的:

for i in range(len(v)):
    w[i] = v[i] + 5

其中使用 NumPy 的加法要快得多:

w = v + 5

使用 NumPy 切片也可以在迭代for循环时显著提高速度。为了演示这一点,让我们考虑在二维数组中形成邻居的平均值:

def my_avg(A):
    m,n = A.shape
    B = A.copy()
    for i in range(1,m-1):
        for j in range(1,n-1):
            B[i,j] = (A[i-1,j] + A[i+1,j] + A[i,j-1] + A[i,j+1])/4
    return B

def slicing_avg(A):
    A[1:-1,1:-1] = (A[:-2,1:-1] + A[2:,1:-1] +
    A[1:-1,:-2] + A[1:-1,2:])/4
    return A

这些函数都给每个元素分配了四个相邻元素的平均值。使用切片的第二个版本要快得多。

除了用 NumPy 函数代替for循环和其他较慢的构造,还有一个有用的函数叫做vectorize,参考第四章线性代数-数组函数作用于数组一节。这将采用一个函数,并创建一个矢量化版本,尽可能使用函数将该函数应用于数组的所有元素。

考虑以下向量化函数的示例:

def my_func(x):
    y = x**3 - 2*x + 5
    if y>0.5:
        return y-0.5
    else:
        return 0

通过迭代数组来应用这一点非常慢:

for i in range(len(v)):
    v[i] = my_func(v[i])

相反,使用vectorize创建一个新函数,如下所示:

my_vecfunc = vectorize(my_func)

然后,该函数可以直接应用于数组:

v = my_vecfunc(v)

矢量化选项要快得多(长度为 100 的数组快 10 倍左右)。

广播

NumPy 中的广播表示猜测两个数组之间共同的兼容形状的能力。例如,当添加一个向量(一维数组)和一个标量(零维数组)时,标量被扩展为向量,以便允许添加。一般的机制叫做广播。我们将首先从数学的角度回顾这个机制,然后继续给出 NumPy 中广播的精确规则。

数学观

广播经常在数学中进行,主要是隐含的。例如 f(x) + Cf(x) + g(y) 等表达式。我们将在本节中对该技术进行明确的描述。

我们牢记函数和 NumPy 数组之间非常密切的关系,如第 4 章线性代数-数组的数学预备知识一节所述。

常量函数

广播最常见的例子之一是增加一个函数和一个常数;如果 C 是一个标量,人们通常会写道:

Constant functions

这是对符号的滥用,因为人们不应该能够添加函数和常数。然而,常量被隐式地传播给函数。常数 C 的广播版本是功能Constant functions定义的:

Constant functions

现在将两个函数加在一起是有意义的:

Constant functions

我们并不是为了学究而学究,而是因为类似的情况可能会出现在数组中,如下面的代码所示:

vector = arange(4) # array([0.,1.,2.,3.])
vector + 1\.        # array([1.,2.,3.,4.])

在这个例子中,发生的一切就好像标量1.已经被转换成与vector长度相同的数组,即array([1.,1.,1.,1.]),然后被添加到vector中。

这个例子非常简单,所以我们继续展示不太明显的情况。

几个变量的函数

一个更复杂的广播例子出现在构建几个变量的函数时。例如,假设给我们一个变量的两个函数, fg ,我们想要根据公式构造一个新的函数 F :

Functions of several variables

这显然是一个有效的数学定义。我们想把这个定义表达为两个变量中两个函数的和,这两个变量定义为

Functions of several variables

现在我们可以简单地写道:

Functions of several variables

这种情况类似于添加列矩阵和行矩阵时出现的情况:

C = arange(2).reshape(-1,1) # column
R = arange(2).reshape(1,-1) # row
C + R                       # valid addition: array([[0.,1.],[1.,2.]])

这在对两个变量的函数进行采样时特别有用,如部分典型示例所示。

一般机制

我们已经看到了如何添加一个函数和一个标量,以及如何从一个变量的两个函数构建一个两个变量的函数。现在让我们把重点放在使这成为可能的一般机制上。一般的机制包括两个步骤:重塑和延伸。

首先,函数 g 被重塑为一个函数General mechanism,该函数接受两个参数。其中一个论点是一个伪论点,我们认为它是零,作为一个惯例:

General mechanism

数学上,General mechanism的定义域现在是General mechanism然后函数 f 以类似于以下的方式被重塑:

General mechanism

现在General mechanismGeneral mechanism都取两个参数,虽然其中一个总是零。我们继续下一步,扩展。将常数转换为常数函数的步骤相同(参考常数函数示例)。

功能General mechanism扩展为:

General mechanism

功能General mechanism扩展为:

General mechanism

现在两个变量 F 的函数,由 F(x,y) = f(x) + g(y) 草率地定义,可以不用参考它的参数来定义:

General mechanism

例如,让我们描述一下前面的常数机制。常数是标量,也就是零参数的函数。因此,整形步骤是定义一个(空的)变量的函数:

General mechanism

现在,扩展步骤简单地通过以下方式进行:

General mechanism

惯例

最后一个要素是关于如何向函数添加额外参数的约定,即如何自动执行整形。按照惯例,一个函数通过在左边加零来自动重塑。

例如,如果两个参数的函数 g 必须被重新整形为三个参数,那么新的函数将被定义为:

Conventions

广播数组

我们现在重复观察数组仅仅是几个变量的函数(参考第四章线性代数-数组中的数学预备知识一节)。因此,数组广播遵循与上述数学函数完全相同的过程。广播在 NumPy 中自动完成。

在下图(图 5.1 )中,我们展示了将形状矩阵(4,3)添加到大小矩阵(1,3)时会发生什么。第二矩阵具有形状(4,3):

Broadcasting arrays

图 5.1:矩阵和向量之间的广播。

广播问题

当 NumPy 被赋予两个不同形状的数组,并被要求执行要求这两个形状相同的操作时,这两个数组被广播到一个公共形状。

假设两个数组具有形状s1T3】和s2T7】。该广播分两步进行:**

  1. 如果形状s1T3 比形状s2T7】短,则在形状 s 1 的左侧添加 1。这是一次重塑。**
  2. 当形状具有相同的长度时,数组被扩展以匹配形状 s 2 (如果可能的话)。

假设我们想要将形状(3)的向量添加到形状(4,3)的矩阵中。矢量需要广播。第一次手术是整形;向量的形状从(3)转换为(1,3)。第二个操作是扩展;形状从(1,3)转换为(4,3)。

例如,假设大小为 n 的向量将被广播到形状( m,n ):

  1. v 自动重塑为(1, n )。
  2. v 延伸至( mn )。

为了证明这一点,我们考虑由下式定义的矩阵:

M = array([[11, 12, 13, 14],
           [21, 22, 23, 24],
           [31, 32, 33, 34]])

和向量由下式给出:

v = array([100, 200, 300, 400])

现在我们可以直接添加Mv:

M + v # works directly

结果是这个矩阵:

The broadcasting problem

形状不匹配

不可能自动将长度为n的向量v传播到形状(n,m)。下图说明了这一点:

Shape mismatch

播放会失败,因为形状(n,)可能不会自动播放到形状(m, n)。解决办法是手动将v重塑为(n,1)的形状。广播现在将照常工作(仅扩展):

M + v.reshape(-1,1)

下面是另一个示例,通过以下方式定义矩阵:

M = array([[11, 12, 13, 14],
           [21, 22, 23, 24],
           [31, 32, 33, 34]])

和一个矢量:

v = array([100, 200, 300])

现在自动广播将失败,因为自动整形不起作用:

M + v # shape mismatch error

因此,解决方案是手动处理整形。在那种情况下,我们想要的是在右边加 1,也就是把向量转换成列矩阵。然后,广播直接工作:

M + v.reshape(-1,1)

形状参数-1,参见第四章线性代数-数组的访问和更改形状部分。结果就是这个矩阵:

Shape mismatch

典型例子

让我们研究一些广播可能派上用场的典型例子。

重新缩放行

假设M是一个 n × m 矩阵,我们要将每行乘以一个系数。系数存储在一个向量coeff中,该向量具有 n 个分量。在这种情况下,自动重塑将不起作用,我们必须执行:

rescaled = M*coeff.reshape(-1,1)

重新缩放列

这里的设置是相同的,但是我们想要用存储在长度为 m 的向量coeff中的系数来重新缩放每一列。在这种情况下,自动整形将起作用:

rescaled = M*coeff

显然,我们也可以手动进行整形,并通过以下方式获得相同的结果:

rescaled = M*coeff.reshape(1,-1)

两个变量的函数

假设 uv 是向量,我们想用元素Wij= uI+vj组成矩阵。这对应于函数 F(x,y) = x + y 。矩阵 W 仅由下式定义:

W=u.reshape(-1,1) + v

如果向量 uv 分别为[0,1]和[0,1,2],则结果为:

Functions of two variables

更一般地说,假设我们要对函数 w ( x,y):=cos(x)+sin(2y)。假设向量 xy 被定义,采样值的矩阵 w 由下式得到:

w = cos(x).reshape(-1,1) + sin(2*y)

请注意,这与ogrid结合使用非常频繁。从ogrid获得的向量已经被方便地成形用于广播。这允许对函数 cos(x)+sin(2y)进行以下优雅的采样:

x,y = ogrid[0:1:3j,0:1:3j] 
# x,y are vectors with the contents of linspace(0,1,3)
w = cos(x) + sin(2*y)

ogrid的语法需要一些解释。第一,ogrid是没有功能的。这是一个具有__getitem__方法的类的实例(参见第 8 章、中的属性一节)。这就是为什么它用括号代替圆括号的原因。

这两个命令是等效的:

x,y = ogrid[0:1:3j, 0:1:3j]
x,y = ogrid.__getitem__((slice(0, 1, 3j),slice(0, 1, 3j)))

前面例子中的步幅参数是一个复数。这表示是步数而不是步长。乍一看,步幅参数的规则可能会令人困惑:

  • 如果步幅是实数,那么它定义了开始和停止之间的步长,并且停止不包括在列表中。
  • 如果步幅是复数s,那么s.imag的整数部分定义了开始和停止之间的步数,停止包含在列表中。

ogrid输出的另一个例子是具有两个数组的元组,可以用于广播:

x,y = ogrid[0:1:3j, 0:1:3j]

给出:

array([[ 0\. ],
       [ 0.5],
       [ 1\. ]])
array([[ 0\. ,  0.5,  1\. ]])

这相当于:

x,y = ogrid[0:1.5:.5, 0:1.5:.5]

稀疏矩阵

具有少量非零条目的矩阵称为稀疏矩阵。例如,当在数值求解偏微分方程的上下文中描述离散微分算子时,在科学计算中会出现稀疏矩阵。

稀疏矩阵通常有很大的维数,有时大到整个矩阵(零条目)甚至无法容纳在可用的内存中。这是稀疏矩阵特殊类型的一个动机。另一个动机是可以避免零矩阵条目的更好的操作性能。

线性代数中一般的非结构化稀疏矩阵的算法数量非常有限。其中大多数本质上是迭代的,并且基于稀疏矩阵的矩阵向量乘法的有效实现。

稀疏矩阵的例子是对角矩阵或带状矩阵。这些矩阵的简单模式允许简单的存储策略;主对角线以及次对角线和超对角线存储在 1D 数组中。从稀疏表示到经典数组类型的转换可以通过命令diag完成,反之亦然。

一般来说,没有这么简单的结构,稀疏矩阵的描述需要特殊的技术和标准。在这里,我们为稀疏矩阵提供了一个面向行和一个面向列的类型,这两个类型都可以通过模块scipy.sparse获得。

Sparse matrices

图 5.2:弹性板有限元模型的刚度矩阵。像素表示 1250 × 1250 矩阵中的非零条目

稀疏矩阵格式

scipy.sparse模块从稀疏矩阵中提供许多不同的存储格式。这里我们只描述最重要的几个:中国南车、中国南车和 LIL。LIL 格式应该用于生成和修改稀疏矩阵;CSR 和 CSC 是矩阵-矩阵和矩阵-向量运算的有效格式。

压缩稀疏行

压缩稀疏行格式(CSR)使用三个数组:dataindptrindices:

  • 1D 数组data按顺序存储所有非零值。它的元素和非零元素一样多,通常用变量nnz表示。
  • 1D 数组indptr包含整数,使得indptr[i]data中元素的索引,该元素是行 i 的第一个非零元素。如果整行 i 为零,则indptr[i]==indptr[i+1]。如果原矩阵有 m 行,那么len(indptr)==m+1
  • 1D 数组indices包含列索引信息的方式是indices[indptr[i]:indptr[i+1]]是一个整数数组,行 i 中非零元素的列索引。显然,len(indices)==len(data)==nnz

让我们看一个例子:矩阵的企业社会责任格式:

Compressed sparse row

由三个数组给出:

data = (1\. 2\. 3\. 4.)
indptr = (0 2 2 3 5)
indices = (0 2 0 0 3)

模块scipy.sparse提供了一个类型csr_matrix,带有一个构造函数,可以通过以下方式使用:

  • 以 2D 数组作为参数
  • scipy.sparse中使用其他稀疏格式之一的矩阵
  • 使用形状参数(m,n),生成 CSR 格式的零矩阵
  • 通过用于data的 1D 数组和具有形状(2,len(data))的整数数组ij,使得ij[0,k]是矩阵的行索引,ij[1,k]是矩阵的data[k]的列索引
  • 三个参数dataindptrindices可以直接给构造函数

前两个选项用于转换目的,而后两个选项直接定义稀疏矩阵。

考虑 python 中的上述示例,如下所示:

import scipy.sparse as sp
A = array([[1,0,2,0],[0,0,0,0],[3.,0.,0.,0.],[1.,0.,0.,4.]])
AS = sp.csr_matrix(A)

其中,提供了以下属性:

AS.data      # returns array([ 1., 2., 3., 1., 4.]) 
AS.indptr    # returns array([0, 2, 2, 3, 5])
AS.indices   # returns array([0, 2, 0, 0, 3])
AS.nnz       # returns 5

压缩稀疏列

CSR 格式有一个面向列的孪生格式——压缩稀疏列(CSC)格式。与 CSR 格式相比,它唯一的不同是indptrindices数组的定义,这两个数组现在是与列相关的。CSC 格式的类型是csc_matrix,它的使用对应于csr_matrix,在本节前面已经解释过了。

继续 CSC 格式的相同示例:

import scipy.sparse as sp
A = array([[1,0,2,0],[0,0,0,0],[3.,0.,0.,0.],[1.,0.,0.,4.]])
AS = sp.csc_matrix(A)
AS.data         # returns array([ 1., 3., 1., 2., 4.]) 
AS.indptr       # returns array([0, 3, 3, 4, 5])
AS.indices      # returns array([0, 2, 3, 0, 3])
AS.nnz          # returns 5

基于行的链表格式

链表稀疏格式将非零矩阵条目按行存储在列表data中,使得data[k]是行 k 中非零条目的列表。如果该行中的所有条目都为 0,则它包含一个空列表。

第二个列表rows在位置 k 包含行 k 中非零元素的列索引列表。下面是基于行的链表格式(LIL) 格式的一个例子:

import scipy.sparse as sp
A = array([[1,0,2,0],[0,0,0,0], [3.,0.,0.,0.], [1.,0.,0.,4.]]) 
AS = sp.lil_matrix(A)
AS.data     # returns array([[1.0, 2.0], [], [3.0], [1.0, 4.0]], dtype=object)
AS.rows     # returns array([[0, 2], [], [0], [0, 3]], dtype=object)
AS.nnz      # returns 5

以 LIL 格式更改和切片矩阵

LIL 格式最适合切片,即提取 LIL 格式的子矩阵,并通过插入非零元素来改变稀疏模式。下一个示例演示了切片:

BS = AS[1:3,0:2]
BS.data     # returns array([[], [3.0]], dtype=object)
BS.rows     # returns array([[], [0]], dtype=object)

插入新的非零元素会自动更新属性:

AS[0,1] = 17 
AS.data # returns array([[1.0, 17.0, 2.0], [], [3.0], [1.0, 4.0]])
AS.rows              # returns array([[0, 1, 2], [], [0], [0, 3]])
AS.nnz               # returns 6

其他稀疏矩阵格式不鼓励这些操作,因为它们效率极低。

生成稀疏矩阵

NumPy 命令eyeidentitydiagrand有它们稀疏的对应物。他们提出了一个额外的论点;它指定结果矩阵的稀疏矩阵格式。

以下命令生成单位矩阵,但采用不同的稀疏矩阵格式:

import scipy.sparse as sp
sp.eye(20,20,format = 'lil') 
sp.spdiags(ones((20,)),0,20,20, format = 'csr') 
sp.identity(20,format ='csc')

sp.rand命令接受描述生成的随机矩阵密度的附加参数。密集矩阵的密度为 1,而零矩阵的密度为 0:

import scipy.sparse as sp 
AS=sp.rand(20,200,density=0.1,format=’csr’)
AS.nnz # returns 400

与 NumPy 命令zeroes没有直接对应关系。完全用零填充的矩阵是通过实例化相应的类型来生成的,其中形状参数作为构造函数参数:

import scipy.sparse as sp
Z=sp.csr_matrix((20,200))
Z.nnz    # returns 0

稀疏矩阵方法

有几种方法可以将一种稀疏类型转换为另一种类型或转换为数组:

AS.toarray # converts sparse formats to a numpy array 
AS.tocsr
AS.tocsc
AS.tolil

稀疏矩阵的类型可以通过方法issparseisspmatrix_lilisspmatrix_csrisspmatrix_csc来检查。

稀疏矩阵上的元素操作+*/**被定义为用于 NumPy 数组。不管操作数的稀疏矩阵格式如何,结果总是一个csr_matrix。将元素操作函数应用于稀疏矩阵需要首先将它们转换为 CSR 或 CSC 格式,并将函数应用于它们的data属性,如下一个示例所示。

稀疏矩阵的元素正弦可以通过对其data属性的操作来定义:

import scipy.sparse as sp
def sparse_sin(A):
    if not (sp.isspmatrix_csr(A) or sp.isspmatrix_csc(A)):
        A = A.tocsr()
A.data = sin(A.data)
return A

对于矩阵-矩阵或矩阵-向量乘法,有一种稀疏矩阵方法,dot。它返回一个csr_matrix或一个array1D 号码:

import scipy.sparse as sp
A = array([[1,0,2,0],[0,0,0,0],[3.,0.,0.,0.],[1.,0.,0.,4.]])
AS = sp.csr_matrix(A)
b = array([1,2,3,4])
c = AS.dot(b)      # returns array([ 7., 0., 3., 17.]) 
C = AS.dot(AS)     # returns  csr_matrix
d = dot(AS,b)      # does not return the expected result! 

型式

避免在稀疏矩阵上使用 NumPy 的命令dot,因为这可能会导致意想不到的结果。请使用来自scipy.sparse的命令dot

其他线性代数运算,如系统求解、最小二乘、特征值和奇异值由scipy.sparse.linalg模块提供。

总结

观点的概念是你应该从本章中学到的重要话题之一。错过这个主题会让你在调试代码时很困难。布尔数组出现在本书的不同地方。在处理数组时,它们是避免冗长的if构造和循环的方便而紧凑的工具。在几乎所有大型计算项目中,稀疏矩阵都成为一个问题。您看到了这些是如何处理的,以及哪些相关方法可用。

六、绘图

Python 中的绘图可以通过 matplotlib 模块的pyplot部分来完成。使用 matplotlib,您可以创建高质量的图形和图形,还可以绘制和可视化您的结果。Matplotlib 是开源的免费软件,【21】。matplotlib 网站还包含优秀的文档和示例, [35] 。在本节中,我们将向您展示如何使用最常见的功能。接下来几节中的示例假设您已经将模块导入为:

from matplotlib.pyplot import *

如果想使用 IPython 中的绘图命令,建议在启动 IPython shell 后直接运行魔法命令%matplotlib。这为交互式绘图做好了准备。

基本标绘

标准绘图功能为plot。调用plot(x,y)创建一个图形窗口,其图形为 y 作为 x 的函数。输入参数是长度相等的数组(或列表)。也可以使用plot(y),在这种情况下 y 中的值将相对于它们的指数绘制,即plot(y)plot(range(len(y)),y)的缩写。

下面是一个示例,展示了如何使用 200 个采样点为xϵ【-2π,2π】绘制 sin( x )并在每第四个点设置标记:

# plot sin(x) for some interval
x = linspace(-2*pi,2*pi,200)
plot(x,sin(x))

# plot marker for every 4th point
samples = x[::4]
plot(samples,sin(samples),'r*')

# add title and grid lines
title('Function sin(x) and some points plotted')
grid()

结果如下图所示(图 6.1 ):

Basic plotting

图 6.1:显示了带有网格线的函数 sin(x)的曲线图。

如您所见,标准图是一条纯蓝色曲线。每个轴会自动缩放以适应这些值,但也可以手动设置。颜色和绘图选项可以在前两个输入参数之后给出。这里,r*表示红色的星形标记。下一节将更详细地介绍格式化。title命令在绘图区域上方放置一个标题文本字符串。

多次调用plot会将图叠加在同一个窗口中。要获得新的干净图形窗口,请使用figure()figure命令可能包含一个整数,例如figure(2),可用于在图形窗口之间切换。如果没有具有该编号的图形窗口,则会创建一个新的图形窗口,否则,该窗口将被激活以进行打印,并且所有后续的打印命令都将应用于该窗口。

可以使用legend功能解释多个绘图,并为每个绘图调用添加标签。以下示例使用命令polyfitpolyval将多项式拟合到一组点,并用图例绘制结果:

# —Polyfit example—
x = range(5)
y = [1,2,1,3,5]
p2 = polyfit(x,y,2)
p4 = polyfit(x,y,4)

# plot the polynomials and points
xx = linspace(-1,5,200) 
plot(xx, polyval(p2, xx), label='fitting polynomial of degree 2')
plot(xx, polyval(p4, xx),
                label='interpolating polynomial of degree 4') 
plot(x,y,'*')

# set the axis and legend
axis([-1,5,0,6])
legend(loc='upper left', fontsize='small')

这里还可以看到如何使用axis([xmin,xmax,ymin,ymax])手动设置轴的范围。legend命令接受关于位置和格式的可选参数;这种情况下图例放在左上角,用小字号排版,如下图所示(图 6.2 )。

Basic plotting

图 6.2:拟合到相同点的两个多项式。

作为基本绘图的最后一个示例,我们演示了如何在二维中绘制散点图和对数图。

2D 点散点图示例:

# create random 2D points
import numpy
x1 = 2*numpy.random.standard_normal((2,100))
x2 = 0.8*numpy.random.standard_normal((2,100)) + array([[6],[2]])
plot(x1[0],x1[1],'*')
plot(x2[0],x2[1],'r*')
title('2D scatter plot')

Basic plotting

图 6.3(a):散点图的一个例子

以下代码是使用loglog的对数图的示例:

# log both x and y axis 
x = linspace(0,10,200) 
loglog(x,2*x**2, label = 'quadratic polynomial',
                            linestyle = '-', linewidth = 3)
loglog(x,4*x**4, label = '4th degree polynomial',
                            linestyle = '-.', linewidth = 3)
loglog(x,5*exp(x), label = 'exponential function', linewidth = 3)
title('Logarithmic plots')
legend(loc = 'best')

Basic plotting

图 6.3(b):带有对数 x 轴和 y 轴的曲线图示例

上图所示的例子(图 6.3(a)图 6.3(b) )使用了plotloglog的一些允许特殊格式化的参数。在下一节中,我们将更详细地解释参数。

格式化

图形和情节的外观可以根据您的需要进行设计和定制。一些重要的变量是linewidth,控制地块线的粗细;设置轴标签的xlabelylabelcolor为地块颜色,transparent为透明度。本节将告诉您如何使用其中的一些。下面是一个包含更多关键字的示例:

k = 0.2
x = [sin(2*n*k) for n in range(20)]
plot(x, color='green', linestyle='dashed', marker='o', 
                       markerfacecolor='blue', markersize=12, linewidth=6)

如果您只需要基本的样式更改,例如设置颜色和线条样式,可以使用一些简短的命令。下表(表 6.1 )显示了这些格式化命令的一些示例。您可以使用短字符串语法plot(...,'ro-'),或者更明确的语法plot(..., marker='o', color='r', linestyle='-')

Formatting

表 6.1:一些常见的绘图格式参数

要用我们写的'o'标记将颜色设置为绿色:

plot(x,'go')

要绘制直方图而不是常规图,使用hist命令:

# random vector with normal distribution
sigma, mu = 2, 10
x = sigma*numpy.random.standard_normal(10000)+mu 
hist(x,50,normed=1)
z = linspace(0,20,200)
plot(z, (1/sqrt(2*pi*sigma**2))*exp(-(z-mu)**2/(2*sigma**2)),'g')
# title with LaTeX formatting 
title('Histogram with '.format(mu,sigma))

Formatting

图 6.4 具有 50 个箱的正态分布和指示真实分布的绿色曲线

结果图与上图相似(图 6.4 )。标题和任何其他文本都可以使用 LaTeX 进行格式化,以显示数学公式。LaTeX 格式包含在一对$符号中。另外,请注意使用format方法进行的字符串格式化,请参考第 2 章、变量和基本类型中的字符串一节。

有时字符串格式的括号会干扰 LaTeX 括号环境。如果出现这种情况,用双支架替换 LaTeX 支架,例如x_{1}应替换为x_{{1}}。文本可能包含与字符串转义序列重叠的序列,例如,\tau将被解释为制表符\t。一个简单的解决方法是在字符串前添加r,例如r'\tau';这使它成为一个原始字符串。

使用subplot命令可以在一个图形窗口中放置几个图。考虑下面的例子,它迭代地平均正弦曲线上的噪声。

def avg(x):
    """ simple running average """
    return (roll(x,1) + x + roll(x,-1)) / 3
# sine function with noise
x = linspace(-2*pi, 2*pi,200)
y = sin(x) + 0.4*rand(200)

# make successive subplots
for iteration in range(3):
    subplot(3, 1, iteration + 1)
    plot(x,y, label = '{:d} average{}'.format(iteration, 's' if iteration > 1 else ''))
    yticks([])
    legend(loc = 'lower left', frameon = False)
    y = avg(y) #apply running average 
subplots_adjust(hspace = 0.7)

Formatting

图 6.5:在同一图形窗口中绘制多次的示例。

函数avg使用roll调用来移动数组的所有值。subplot采用三个参数:垂直图的数量、水平图的数量和指示绘制位置的索引(按行计数)。请注意,我们使用subplots_adjust命令添加额外的空间来调整两个支线剧情之间的距离。

一个有用的命令是savefig,它允许你将一个图形保存为一个图像(这也可以在图形窗口中完成)。该命令支持许多图像和文件格式,它们由文件扩展名指定为:

savefig('test.pdf')  # save to pdf

或者

savefig('test.svg')  # save to svg (editable format)

您可以将图像放在非白色背景下,例如网页。为此,可以设置transparent参数使图形背景透明:

savefig('test.pdf', transparent=True)

如果您打算将图形嵌入到 LaTeX 文档中,建议您通过将图形的边界框紧密设置在绘图周围来减少周围的空白,如下所示:

savefig('test.pdf', bbox_inches='tight')

网格和等高线

常见的任务是矩形上标量函数的图形表示:

Meshgrid and contours

为此,首先我们必须在矩形[ ab ] x [ cd 上生成一个网格。这是使用meshgrid命令完成的:

n = ... # number of discretization points along the x-axis
m = ... # number of discretization points along the x-axis 
X,Y = meshgrid(linspace(a,b,n), linspace(c,d,m))

XY(n,m)形状的数组,使得Meshgrid and contours包含网格点Meshgrid and contours的坐标,如下图(图 6.6) :

Meshgrid and contours

图 6.6:由网格离散的矩形

meshgrid离散的矩形将用于可视化迭代的行为。但是首先我们将使用它来绘制函数的水平曲线。这是通过命令contour完成的。

例如,我们选择罗森布鲁克的香蕉函数:

Meshgrid and contours

它用于挑战优化方法。函数值向香蕉形谷下降,该谷本身向函数的全局最小值(1,1)缓慢下降。

首先我们使用contour显示水平曲线。

rosenbrockfunction = lambda x,y: (1-x)**2+100*(y-x**2)**2 
X,Y = meshgrid(linspace(-.5,2.,100), linspace(-1.5,4.,100))
Z = rosenbrockfunction(X,Y) 
contour(X,Y,Z,logspace(-0.5,3.5,20,base=10),cmap='gray') 
title('Rosenbrock Function: ')
xlabel('x')
ylabel('y')

这将在第四个参数给出的级别绘制级别曲线,并使用颜色图gray。此外,我们使用从 10 0.5 到 10 3 的对数间隔步长,使用函数logscale来定义级别,如下图所示。

Meshgrid and contours

图 6.7:罗森布鲁克函数的等高线图

在前面的例子中,由关键字lambda指示的匿名函数用于保持代码紧凑。匿名函数在第 7 章函数匿名函数匿名函数 lambda 关键字一节中进行了解释。如果等级没有作为参数提供给contour,函数会自己选择合适的等级。

contourf功能执行与contour相同的功能,但根据不同的级别用颜色填充绘图。等高线图是可视化数值方法行为的理想选择。我们在这里通过展示优化方法的迭代来说明这一点。

我们继续前面的例子,描述了由鲍威尔方法【27】生成的罗森布罗克函数的最小值的步骤,我们将应用它来寻找罗森布罗克函数的最小值:

import scipy.optimize as so
rosenbrockfunction = lambda x,y: (1-x)**2+100*(y-x**2)**2
X,Y=meshgrid(linspace(-.5,2.,100),linspace(-1.5,4.,100))
Z=rosenbrockfunction(X,Y)
cs=contour(X,Y,Z,logspace(0,3.5,7,base=10),cmap='gray')
rosen=lambda x: rosenbrockfunction(x[0],x[1])
solution, iterates = so.fmin_powell(rosen,x0=array([0,-0.7]),retall=True)
x,y=zip(*iterates)
plot(x,y,'ko') # plot black bullets
plot(x,y,'k:',linewidth=1) # plot black dotted lines
title("Steps of Powell's method to compute a  minimum")
clabel(cs)

迭代法fmin_powell应用鲍威尔法求最小值。它由给定的开始值 x 0 启动,并在给出选项retall=True时报告所有迭代。经过十六次迭代,找到了解 x= 0 ,y= 0。迭代在下面的等高线图中被描述为项目符号(图 6.8 )。

Meshgrid and contours

图 6.8:罗森布鲁克函数的等高线图,带有优化方法的搜索路径

contour还创建了一个轮廓集对象,我们将其分配给变量cs。然后clabel用它来标注相应函数值的级别,如上图所示(图 6.8 )。

图像和轮廓

让我们看一些将数组可视化为图像的例子。下面的函数将为曼德勃罗分形创建一个颜色值矩阵。这里我们考虑一个不动点迭代,它依赖于一个复杂的参数 c :

Images and contours

根据该参数的选择,它可能会也可能不会创建复数值的有界序列 z n

对于 c 的每个值,我们检查 z n 是否超过规定的界限。如果在maxit次迭代中,它保持在界限之下,我们假设序列是有界限的。

请注意,在下面这段代码中,meshgrid是如何用来生成复杂参数值矩阵的 c:

def mandelbrot(h,w, maxit=20):
    X,Y = meshgrid(linspace(-2, 0.8, w), linspace(-1.4, 1.4, h))
    c = X + Y*1j
    z = c
    exceeds = zeros(z.shape, dtype=bool)

    for iteration in range(maxit):
        z  = z**2 + c
        exceeded = abs(z) > 4
        exceeds_now = exceeded & (logical_not(exceeds))  
        exceeds[exceeds_now] = True        
        z[exceeded] = 2  # limit the values to avoid overflow
    return exceeds

imshow(mandelbrot(400,400),cmap='gray')
axis('off')

命令imshow将矩阵显示为图像。所选的颜色图用白色显示了序列显示为无界的区域,用黑色显示了其他区域。这里我们使用axis('off')来关闭轴,因为这对于图像可能不是很有用。

Images and contours

图 6.9:使用 imshow 将矩阵可视化为图像的示例。

默认情况下,imshow使用插值使图像看起来更好。当矩阵很小时,这一点很明显。下图显示了使用:

imshow(mandelbrot(40,40),cmap='gray')

imshow(mandelbrot(40,40), interpolation='nearest', cmap='gray')

在第二个例子中,像素值被复制。

Images and contours

图 6.10:使用 imshow 的线性插值与使用最近邻插值的区别

有关使用 Python 处理和绘制图像的更多详细信息,请参考【30】

Matplotlib 对象

到目前为止,我们已经使用了 matplotlib 的pyplot模块。这个模块让我们可以轻松地直接使用最重要的绘图命令。大多数情况下,我们感兴趣的是创建一个图形并立即显示它。但是,有时我们想生成一个图形,稍后应该通过改变它的一些属性来修改它。这要求我们以面向对象的方式处理图形对象。在本节中,我们将介绍一些修改图形的基本步骤。对于 Python 中更复杂的面向对象的绘图方法,您必须离开pyplot并直接进入matplotlib及其大量的文档。

轴对象

当创建一个以后应该修改的图时,我们需要引用一个图形和一个轴对象。为此,我们必须先创建一个图形,然后定义一些轴及其在图形中的位置。我们不应该忘记将这些对象分配给一个变量:

fig = figure()
ax = subplot(111)

根据subplot的使用,一个图形可以有多个轴对象。在第二步中,绘图与给定的轴对象相关联:

fig = figure(1)
ax = subplot(111)
x = linspace(0,2*pi,100) 
# We set up a function that modulates the amplitude of the sin function
amod_sin = lambda x: (1.-0.1*sin(25*x))*sin(x)
# and plot both...
ax.plot(x,sin(x),label = 'sin') 
ax.plot(x, amod_sin(x), label = 'modsin')

这里我们使用了一个由lambda关键字表示的匿名函数。我们将在后面的章节匿名函数中解释这个构造-在第 7 章函数中的 lambda 关键字。事实上,这两个绘图命令用两个Lines2D对象填充列表ax.lines:

ax.lines #[<matplotlib.lines.Line2D at ...>, <matplotlib.lines.Line2D at ...>]

使用标签是一个很好的做法,这样我们以后就可以用一种简单的方式识别对象:

for il,line in enumerate(ax.lines):
    if line.get_label() == 'sin':
       break

我们现在以一种允许进一步修改的方式设置事情。我们目前得到的图如前图所示(图 6.11,左)。

修改线条属性

我们只是通过标签来识别特定的线对象。它是带有索引il的列表ax.lines列表的一个元素。它的所有属性都收集在字典中

dict_keys(['marker', 'markeredgewidth', 'data', 'clip_box', 'solid_capstyle', 'clip_on', 'rasterized', 'dash_capstyle', 'path', 'ydata', 'markeredgecolor', 'xdata', 'label', 'alpha', 'linestyle', 'antialiased', 'snap', 'transform', 'url', 'transformed_clip_path_and_affine', 'clip_path', 'path_effects', 'animated', 'contains', 'fillstyle', 'sketch_params', 'xydata', 'drawstyle', 'markersize', 'linewidth', 'figure', 'markerfacecolor', 'pickradius', 'agg_filter', 'dash_joinstyle', 'color', 'solid_joinstyle', 'picker', 'markevery', 'axes', 'children', 'gid', 'zorder', 'visible', 'markerfacecoloralt'])

这可以通过以下命令获得:

ax.lines[il].properties()

它们可以通过相应的 setter 方法进行更改。让我们更改正弦曲线的线条样式:

ax.lines[il].set_linestyle('-.')
ax.lines[il].set_linewidth(2)

我们甚至可以修改数据,如图所示:

ydata=ax.lines[il].get_ydata()
ydata[-1]=-0.5
ax.lines[il].set_ydata(ydata)

结果如下图(图 6.11,右):

Modifying line properties

图 6.11:调幅正弦函数(左)和最后一个数据点损坏的曲线(右)。

注释

一个有用的轴方法是annotate。它在给定位置设置注释,并用箭头指向图形中的另一个位置。箭头可以在字典中被赋予属性:

annot1=ax.annotate('amplitude modulated\n curve', (2.1,1.0),(3.2,0.5),
       arrowprops={'width':2,'color':'k', 'connectionstyle':'arc3,rad=+0.5', 
                   'shrink':0.05},
       verticalalignment='bottom', horizontalalignment='left',fontsize=15, 
                   bbox={'facecolor':'gray', 'alpha':0.1, 'pad':10})
annot2=ax.annotate('corrupted data', (6.3,-0.5),(6.1,-1.1),
       arrowprops={'width':0.5,'color':'k','shrink':0.1},
       horizontalalignment='center', fontsize=12)

在上面的第一个注释示例中,箭头指向坐标为( 2.1,1.0 )的点,文本的左下方坐标为( 3.2,0.5 )。如果没有另外指定,坐标是在方便的数据坐标系中给出的,这是指用于生成图的数据。

此外,我们演示了由arrowprop字典指定的几个箭头属性。您可以通过shrink键缩放箭头。设置'shrink':0.05将箭头尺寸减小 5%,以与它所指向的曲线保持一段距离。您可以使用connectionstyle键让箭头跟随样条弧或赋予它其他形状。

文本属性甚至文本周围的边界框都可以通过额外的关键字参数来标注方法,参见下图(图 6.12,左侧):

实验注释有时需要移除我们想要拒绝的尝试。因此,我们将注释对象分配给一个变量,这允许我们通过其remove方法移除注释:

annot1.remove()

填充曲线之间的区域

填充是突出曲线之间差异的理想工具,例如预期数据之上的噪声、近似与精确函数等。

填充是通过轴方法完成的

ax.fill_between(x,y1,y2)

对于我们使用的下一个数字:

axf = ax.fill_between(x, sin(x), amod_sin(x), facecolor='gray')

where是一个非常方便的参数,需要一个布尔数组来指定额外的填充条件。

axf = ax.fill_between(x, sin(x), amod_sin(x),where=amod_sin(x)-sin(x) > 0, facecolor=’gray’)

选择要填充的区域的布尔数组是amod_sin(x)-sin(x) > 0

下图显示了带有两种填充区域的曲线:

Filling areas between curves

图 6.12:带有注释和填充区域的调幅正弦函数(左)和使用 where 参数仅部分填充区域的修改图形(右)。

如果您自己测试这些命令,在尝试部分填充之前,不要忘记删除完整的填充,否则您将看不到任何变化:

axf.remove()

相关填充命令为fillfill_betweenx

刻度和标签

如果谈话、海报和出版物中的人物没有过多的不必要信息,他们看起来会好得多。你想把观众引向那些包含信息的部分。在我们的示例中,我们通过从 x 轴和 y 轴移除刻度并引入与问题相关的刻度标签来清理图片:

Ticks and ticklabels

图 6.13:振幅调制正弦函数的完整示例,带有注释和填充区域以及修改的刻度和刻度标签。

ax.set_xticks(array([0,pi/2,pi,3/2*pi,2*pi]))
ax.set_xticklabels(('$0$','$\pi/2$','$\pi$','$3/2 \pi$','$2 \pi$'),fontsize=18)
ax.set_yticks(array([-1.,0.,1]))
ax.set_yticklabels(('$-1$','$0$','$1$'),fontsize=18)

请注意,我们在字符串中使用 LaTeX 格式来表示希腊字母,正确设置公式,并使用 LaTeX 字体。增加字体大小也是一种很好的做法,这样可以将生成的图形缩小到文本文档中,而不会影响坐标轴的可读性。本指导例最终结果见上图(图 6.13 )。

制作三维图

有一些有用的matplotlib工具包和模块可以用于各种特殊用途。在本节中,我们描述了一种生成三维图的方法。

mplot3d工具包提供点、线、轮廓、表面和所有其他基本组件的三维绘图,以及三维旋转和缩放。如下例所示,通过将关键字projection='3d'添加到轴对象来制作三维图:

from mpl_toolkits.mplot3d import axes3d

fig = figure()
ax = fig.gca(projection='3d')
# plot points in 3D
class1 = 0.6 * random.standard_normal((200,3))
ax.plot(class1[:,0],class1[:,1],class1[:,2],'o')
class2 = 1.2 * random.standard_normal((200,3)) + array([5,4,0])
ax.plot(class2[:,0],class2[:,1],class2[:,2],'o')
class3 = 0.3 * random.standard_normal((200,3)) + array([0,3,2])
ax.plot(class3[:,0],class3[:,1],class3[:,2],'o')

可以看到,需要从mplot3d导入axes3D类型。生成的图显示了分散的三维数据,如下图所示(图 6.14 )

Making 3D plots

图 6.14:使用 mplot3d 工具包绘制三维数据

绘制曲面同样简单。以下示例使用内置函数get_test_data创建用于绘制曲面的样本数据。考虑以下带有透明度的曲面图示例。

X,Y,Z = axes3d.get_test_data(0.05)

fig = figure()
ax = fig.gca(projection='3d')
# surface plot with transparency 0.5 
ax.plot_surface(X,Y,Z,alpha=0.5)

α值设置透明度。曲面图如下图所示(图 6.15 )。

Making 3D plots

图 6.15:用三个 2D 投影绘制表面网格的示例。

您也可以在任何坐标投影中绘制等高线,如下例所示。

fig = figure()
ax = fig.gca(projection = '3d')
ax.plot_wireframe(X,Y,Z,rstride = 5,cstride = 5)

# plot contour projection on each axis plane
ax.contour(X,Y,Z, zdir='z',offset = -100)
ax.contour(X,Y,Z, zdir='x',offset = -40)
ax.contour(X,Y,Z, zdir='y',offset = 40)

# set axis limits
ax.set_xlim3d(-40,40)
ax.set_ylim3d(-40,40)
ax.set_zlim3d(-100,100)

# set labels
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z axis')

注意设置轴限制的命令。用于设置轴限制的标准matplotlib命令是axis([-40, 40, -40, 40]),这对于 2D 地块很有效。然而,axis([-40,40,-40,40,-40,40])不起作用。对于三维绘图,您需要使用面向对象版本的命令,ax.set_xlim3d(-40,40)和类似的命令。标注轴也是如此;请注意设置标签的命令。对于 2D 地块你可以做xlabel(’X axis’)ylabel(’Y axis’)但是没有zlabel命令。相反,在三维绘图中,您需要使用ax.set_xlabel(’X axis’)和类似的工具,如前例所示。

这段代码的结果如下

Making 3D plots

有许多选项可用于设置地块外观的格式,包括曲面的颜色和透明度。mplot3d文档网站【23】有详细信息。

用剧情拍电影

如果您有演变的数据,除了在图形窗口中显示之外,您可能还想将其保存为电影,类似于savefig命令。一种方法是使用 visvisvis 提供的visvis模块(更多信息请参考【37】)。

这里有一个简单的例子,用隐式表示法来演化一个圆。让圆用函数Making movies from plots的零电平Making movies from plots来表示。或者,考虑零点集合内的圆盘Making movies from plots。如果 f 的值以 v 的速率减小,则圆将以Making movies from plots的速率向外移动。

这可以实现为:

import visvis.vvmovie as vv

# create initial function values
x = linspace(-255,255,511)
X,Y = meshgrid(x,x)
f = sqrt(X*X+Y*Y) - 40 #radius 40

# evolve and store in a list
imlist = []
for iteration in range(200):
    imlist.append((f>0)*255)
    f -= 1 # move outwards one pixel
vv.images2swf.writeSwf('circle_evolution.swf',imlist)

结果是一部 Flash 电影(。swf 文件)的一个不断增长的黑色圆圈,如下图所示(图 6.16)* :

Making movies from plots

图 6.16:一个演化圆的例子

在本例中,使用数组列表来创建电影。visvis模块还可以保存 GIF 动画,在某些平台上还可以保存 AVI 动画(。gif 和。avi 文件 ) ,也有可能直接从人物窗口捕捉电影画面。然而,这些选项需要在您的系统上安装更多的软件包(例如,PyOpenGLPIL,Python 图像库)。详见visvis网页上的文档。

另一种选择是使用savefig创建图像,每帧一个。

# create initial function values
x = linspace(-255,255,511)
X,Y = meshgrid(x,x)
f = sqrt(X*X+Y*Y) - 40 #radius 40
for iteration in range(200):
    imshow((f>0)*255)
    gray()
    axis('off')
    savefig('circle_evolution_{:d}.png'.format(iteration))
    f -= 1

然后,可以使用标准的视频编辑软件(例如美柯德或 ImageMagick)来组合这些图像。这种方法的优点是,您可以通过保存高分辨率图像来制作高分辨率视频。

总结

图形表示是呈现数学结果或算法行为的最简洁形式。本章为您提供了绘图的基本工具,并向您介绍了以面向对象的方式处理图形对象(如图形、轴和线)的更复杂的方法。

在本章中,您学习了如何制作图,不仅是经典的 x/y 图,还有 3D 图和直方图。我们给了你一个拍电影的开胃菜。您还看到了如何修改绘图,将它们视为具有相关方法和属性的图形对象,这些方法和属性可以设置、删除或修改。

练习

Ex。1 →编写一个函数,给定椭圆的中心坐标( x,y )、半轴 ab 旋转角度θ绘制椭圆。

Ex。2 →写一个短程序,取一个 2D 数组,比如前面的 Mandelbrot 轮廓图像,用相邻值的平均值迭代替换每个值。在图形窗口中更新数组的等高线图,以动画显示等高线的演变。解释行为。

Ex。3 →考虑一个带有整数值的 N × N 矩阵或图像。地图

Exercises

是点的环形正方形网格映射到自身的一个示例。这有一个有趣的特性,它通过剪切扭曲图像,然后使用 modulu 函数mod将图像之外的部分移回。迭代应用,这导致随机图像的方式,最终返回原来的。执行以下顺序:

Exercises

并将前 N 步保存到文件中或在图形窗口中绘制出来。

作为示例图像,您可以使用来自scipy.misc的经典 512 × 512 Lena 测试图像。

from scipy.misc import lena
I = lena()

结果应该如下所示:

| ![Exercises](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/sci-comp-py3/img/lena_cat_0.jpg) | ![Exercises](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/sci-comp-py3/img/lena_cat_1.jpg) | … | ![Exercises](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/sci-comp-py3/img/lena_cat_128.jpg) | … | ![Exercises](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/sci-comp-py3/img/lena_cat_256.jpg) | … | ![Exercises](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/sci-comp-py3/img/lena_cat_511.jpg) | ![Exercises](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/sci-comp-py3/img/lena_cat_512.jpg) | | Zero | one | | One hundred and twenty-eight | | Two hundred and fifty-six | | Five hundred and eleven | Five hundred and twelve |

型式

计算 xy 映射,并使用数组索引(参考第 5 章高级数组概念中的数组索引部分)来复制像素值。

Ex。4 →图像读取和绘制。SciPy 自带imread功能(在scipy.misc模块)读取图像,(参考第 12 章输入输出读写图像一节)。写一个简短的程序,从文件中读取一个图像,并在给定的灰度值下绘制覆盖在原始图像上的图像轮廓。

型式

您可以通过对颜色通道进行平均来获得图像的灰度版本,如下所示:mean(im,axis=2)

Ex。5 →图像边缘。2D 拉普拉斯的过零点是图像边缘的良好指示。修改上一练习中的程序,使用scipy.ndimage模块中的gaussian_laplacelaplace功能计算 2D 拉普拉斯算子,并将边缘覆盖在图像顶部。

Ex。6 →使用orgid代替meshgrid,重新表述曼德罗德分形示例(参见图像和轮廓部分)另请参见第 5 章高级数组概念双变量函数中的解释ogridorgidmgridmeshgrid有什么区别?

七、函数

本章介绍函数,这是编程中的一个基本构件。我们展示了如何定义它们,如何处理输入和输出,如何正确使用它们,以及如何将它们视为对象。

基础

在数学中,一个函数被写成一个映射,它唯一地将一个元素 y 从范围 R 分配给域 D 中的每个元素 x

这由 f : D → R 表示

或者,当考虑特定元素 xy 时,可以写 f : x → y

在这里, f 被称为函数的名称, f(x) 是其应用于 x 时的值。在这里, x 有时被称为 f. 我们先看一个例子,然后再考虑 Python 中的函数。

例如, D = ℝ x ℝ和 y = f(x 1 ,x2)= x1-x2。这个函数把两个实数映射到它们的差上。

在数学中,函数可以有数字、向量、矩阵,甚至其他函数作为自变量。下面是一个带有混合参数的函数示例:

Basics

在这种情况下,会返回一个数字。使用函数时,我们必须区分两个不同的步骤:

  • 函数的定义
  • 函数的求值,即对于给定值 xf(x) 的计算

第一步完成一次,而第二步可以针对各种参数执行多次。编程语言中的函数遵循相同的概念,并将其应用于各种类型的输入参数,例如字符串、列表或任何对象。我们通过再次考虑给定的例子来演示函数的定义:

def subtract(x1, x2):
    return x1 - x2

关键字def表示我们要定义一个函数。subtract是函数的名称,x1*x2* 是它的参数。冒号表示我们正在使用一个 block 命令,函数返回的值跟在return关键字后面。现在,我们可以评估这个函数。调用此函数时,其参数由输入参数代替:

r = subtract(5.0, 4.3)

计算结果 0.7 并分配给r变量。

参数和参数

定义函数时,其输入变量称为函数的参数。执行函数时使用的输入称为其参数。

传递参数-按位置和关键字

我们将再次考虑前面的例子,其中函数采用两个参数,即x1x2

它们的名字用来区分这两个数字,在这种情况下,不改变结果就不能互换。第一个参数定义从中减去第二个参数的数字。调用subtract时,每个参数都被一个参数代替。只有争论的顺序很重要;参数可以是任何对象。例如,我们可以称之为:

z = 3 
e = subtract(5,z)

除了这种调用函数的标准方式(即通过位置传递参数)之外,有时使用关键字传递参数可能会更方便。参数的名称是关键字;考虑以下实例:

z = 3 
e = subtract(x2 = z, x1 = 5)

这里,参数是按名称而不是按调用中的位置分配给参数的。调用函数的两种方式可以结合起来,这样由 position 给出的参数排在第一位,由 keyword 给出的参数排在最后。我们使用功能plot来显示这一点,该功能在第 6 章标绘中有描述:

plot(xp, yp, linewidth = 2,label = 'y-values')

改变论点

参数的目的是为函数提供必要的输入数据。更改函数内部的参数值通常不会影响函数外部的参数值:

def subtract(x1, x2):
    z = x1 - x2
    x2 = 50.
    return z
a = 20.
b = subtract(10, a)    # returns -10
a    # still has the value 20

这适用于所有不可变的参数,如字符串、数字和元组。如果可变参数(如列表或字典)被更改,情况就不同了。

例如,将可变输入参数传递给函数,并在函数内部更改它们,也可以在函数外部更改它们:

def subtract(x):
    z = x[0] - x[1]
    x[1] = 50.
    return z
a = [10,20]
b = subtract(a)    # returns -10
a    # is now [10, 50.0]

这样的函数滥用它的参数来返回结果。我们强烈建议您不要这样构造,并且建议您不要更改函数内部的输入参数(更多信息请参考默认参数部分)。

访问在本地命名空间之外定义的变量

Python 允许函数访问在其任何封闭程序单元中定义的变量。与局部变量相反,这些变量被称为全局变量。后者只能在函数中访问。例如,考虑以下代码:

import numpy as np # here the variable np is defined
def sqrt(x):
    return np.sqrt(x) # we use np inside the function

这个功能不应该被滥用。下面的代码是一个不应该做什么的例子:

a = 3
def multiply(x):
    return a * x # bad style: access to the variable a defined outside

当改变变量a时,函数multiply默认改变其行为:

a=3
multiply(4)  # returns 12
a=4
multiply(4)  # returns 16

在这种情况下,最好通过参数列表提供变量作为参数:

def multiply(x, a):
    return a * x

使用闭包时,全局变量可能很有用。名称空间和范围在第 11 章名称空间、范围和模块中有更广泛的讨论。

默认参数

有些函数可以有许多参数,其中一些可能只在非标准情况下有用。如果参数可以自动设置为标准(默认)值,这将是切实可行的。我们通过查看scipy.linalg模块中的命令norm来演示默认参数的使用。它计算矩阵和向量的各种范数。

以下片段要求计算 3 × 3 单位矩阵的弗罗贝纽斯范数是等价的(关于矩阵范数的更多信息可以在【10】中找到):

import scipy.linalg as sl
sl.norm(identity(3))
sl.norm(identity(3), ord = 'fro')
sl.norm(identity(3), 'fro')

注意,在第一次调用中,没有给出关于ord关键字的信息。Python 如何知道它应该计算 Frobenius 范数而不是另一个范数,例如欧几里德 2-范数?

上一个问题的答案是使用默认值。默认值是函数定义已经给出的值。如果调用函数时没有提供这个参数,Python 会使用程序员在定义函数时提供的值。

假设我们调用的函数subtract只有一个参数;我们会收到一条错误消息:

TypeError: subtract() takes exactly 2 arguments (1 given)

为了允许省略参数x2,函数的定义必须提供默认值,例如:

def subtract(x1, x2 = 0): 
    return x1 - x2

总而言之,参数可以作为位置参数和关键字参数给出。必须首先给出所有的位置参数。只要省略的参数在函数定义中有默认值,就不需要提供所有关键字参数。

小心可变默认参数

默认参数是根据函数定义设置的。使用默认值时,更改函数内部的可变参数会产生副作用,例如:

def my_list(x1, x2 = []):
    x2.append(x1)
    return x2
my_list(1)  # returns [1]
my_list(2)  # returns [1,2]

可变参数数

列表和字典可用于定义或调用具有可变数量参数的函数。让我们定义一个列表和一个字典如下:

data = [[1,2],[3,4]]    
style = dict({'linewidth':3,'marker':'o','color':'green'})

然后我们可以使用带星号(*)的参数调用plot函数:

plot(*data,**style)

*为前缀的变量名,如前面例子中的*data,意味着提供了一个在函数调用中被解包的列表。这样,列表会生成位置参数。类似地,以**为前缀的变量名,如示例中的**style,将字典解包为关键字参数。参考下图(图 7.1 ):

Variable number of arguments

图 7.1:函数调用中的星号参数

您可能还想使用相反的过程,其中所有给定的位置参数都打包成一个列表,所有关键字参数在传递给函数时都打包成一个字典。

在函数定义中,这由分别以***为前缀的参数表示。您会经常在代码文档中找到*args**kwargs参数,参见图 7.2

Variable number of arguments

图 7.2:函数定义中的星形参数

返回值

Python 中的函数总是返回单个对象。如果一个函数必须返回多个对象,这些对象将被打包并作为单元组对象返回。

例如,下面的函数取一个复数 z ,并根据欧拉公式将其极坐标表示返回为幅度 r 和角度Return values:

Return values

Python 的对应物是这样的:

def complex_to_polar(z):
    r = sqrt(z.real ** 2 + z.imag ** 2)
    phi = arctan2(z.imag, z.real)
    return (r,phi)  # here the return object is formed

这里,我们使用sqrt(x) NumPy 函数作为数字x的平方根,使用arctan2(x,y)作为表达式 tan -1 ( x/y )。

让我们试试我们的功能:

z = 3 + 5j  # here we define a complex number
a = complex_to_polar(z)
r = a[0]
phi = a[1]

最后三个语句可以用一行写得更优雅:

r,phi = complex_to_polar(z)

我们可以通过调用polar_to_comp来测试我们的功能;参考练习 1

如果一个函数没有return语句,则返回值None。在许多情况下,函数不需要返回值。这可能是因为传递给函数的变量可能会被修改。例如,考虑以下函数:

def append_to_list(L, x):
    L.append(x)

前面的函数不返回任何内容,因为它修改了作为参数给出的一个对象。我们在参数和参数部分提到了为什么这是有用的。有许多方法的行为方式是相同的。仅提及列表方法,appendextendreversesort方法不返回任何内容(即返回None)。当一个对象被一个方法以这种方式修改时,这种修改被就地调用。除非通过查看代码或文档,否则很难知道一个方法是否改变了一个对象。

函数或方法不返回任何内容的另一个原因是当它打印出消息或写入文件时。

执行在第一个出现的return语句处停止。该语句后的行是死代码,永远不会执行:

def function_with_dead_code(x):
    return 2 * x
    y = x ** 2 # these two lines ...
    return y   # ... are never executed!

递归函数

在数学中,许多函数都是递归定义的。在这一节中,我们将展示这个概念如何在编程函数时使用。这使得程序与其数学对应物的关系非常清楚,这可能会降低程序的可读性。

然而,我们建议您谨慎使用这种编程技术,尤其是在科学计算领域。在大多数应用中,更直接的迭代方法更有效。从下面的例子中,这一点将立即变得清楚。

切比雪夫多项式由三项递归定义:

Recursive functions

这样的递归需要初始化,即 T 0 ( x ) =1、TT8】1(x)=x .

在 Python 中,这三项递归可以通过以下函数定义来实现:

def chebyshev(n, x):
    if n == 0:
        return 1.
    elif n == 1:
        return x
    else:
        return 2\. * x * chebyshev(n - 1, x) 
                      - chebyshev(n - 2 ,x)

该函数的调用方式如下:

chebyshev(5, 0.52) # returns 0.39616645119999994

这个例子也说明了大幅浪费计算时间的风险。函数求值的数量随着递归级别呈指数增长,并且这些求值中的大多数只是以前计算的重复。虽然使用递归程序来演示代码和数学定义之间的强关系可能很有诱惑力,但是生产代码将避免这种编程技术(参考练习 6)。我们还提到了一种叫做记忆化的技术(更多细节请参考【22】),它将递归编程与缓存技术相结合,以保存复制的函数求值。

递归函数通常有一个级别参数。在前面的例子中,它是 n. 它用于控制功能的两个主要部分:

  • 基本情况,这里,前两个if分支
  • 递归体,其中使用较小的级别参数调用函数本身一次或多次。

递归函数的执行传递的级别数称为递归深度。这个数量不能太大;否则计算可能不再有效,最终会出现以下错误:

RuntimeError: maximum recursion depth exceeded

最大递归深度取决于您使用的计算机的内存。当函数定义中缺少初始化步骤时,也会出现此错误。我们鼓励对非常小的递归深度使用递归程序(有关更多信息,请参考第 9 章迭代无限迭代一节。

功能文档

您应该在开始时使用字符串来记录您的函数。这称为文档字符串:

def newton(f, x0):
    """
    Newton's method for computing a zero of a function
    on input:
    f  (function) given function f(x)
    x0 (float) initial guess 
    on return:
    y  (float) the approximated zero of f
    """
     ...

调用help(newton)时,这个文档字符串会和这个函数的调用一起显示出来:

Help on function newton in module __main__:

newton(f, x0)
     Newton's method for computing a zero of a function
     on input:
     f  (function) given function f(x)
     x0 (float) initial guess
     on return:
     y  (float) the approximated zero of f

文档字符串在内部保存为给定函数的属性__doc__。在示例中,它是newton.__doc__。您应该在文档字符串中提供的最小信息是函数的目的以及输入和输出对象的描述。有工具可以通过收集程序中的所有文档字符串来自动生成完整的代码文档(更多信息请参考 [32] )。

函数是对象

函数是对象,就像 Python 中的其他东西一样。人们可以传递函数作为参数,更改它们的名称,或者删除它们。例如:

def square(x):
    """
    Return the square of x
    """
    return x ** 2
square(4) # 16
sq = square # now sq is the same as square
sq(4) # 16
del square # square doesn't exist anymore
print(newton(sq, .2)) # passing as argument

在科学计算中应用算法时,将函数作为参数传递是非常常见的。计算给定函数的零点的scipy.optimize中的函数fsolve或计算积分的scipy.integrate中的quad就是典型的例子。

函数本身可以有不同类型的不同数量的参数。因此,当将您的函数f作为参数传递给另一个函数g时,请确保fg的文档字符串中描述的形式完全相同。

fsolve的文档字符串给出了其func参数的信息:

func -- A Python function or method which takes at least one
               (possibly vector) argument.

部分应用

让我们从一个双变量函数的例子开始。

函数Partial application可以看作是两个变量的函数。人们通常认为ω不是自由变量,而是函数族的固定参数:

Partial application

这种解释将两个变量中的一个函数简化为一个变量中的一个函数t,给定一个固定的参数值 ω 。通过固定(冻结)函数的一个或几个参数来定义新函数的过程称为部分应用。

使用 Python 模块functools可以轻松创建部分应用,该模块正是为此目的提供了一个名为partial的函数。我们通过构造一个函数来说明这一点,该函数返回给定频率的正弦值:

import functools
def sin_omega(t, freq):
    return sin(2 * pi * freq * t)

def make_sine(frequency):
    return functools.partial(sin_omega, freq = frequency)

使用闭包

使用函数是对象的观点,部分应用可以通过编写一个函数来实现,这个函数本身返回一个新的函数,减少了输入参数的数量。例如,该函数可以定义如下:

def make_sine(freq):
    "Make a sine function with frequency freq"
    def mysine(t):
        return sin_omega(t, freq)
    return mysine

在本例中,内部函数mysine可以访问变量freq;它既不是这个函数的局部变量,也不是通过参数列表传递给它的。Python 允许这样的构造(参见第 11 章命名空间、范围和模块中的命名空间部分)。

匿名函数 lambda 关键字

Python 中使用关键字 lambda 定义匿名函数,即;函数没有名字,用一个表达式描述。您可能只想对一个可以用简单表达式表示的函数执行操作,而不需要命名该函数,也不需要用冗长的def块定义该函数。

名称λ源于微积分和数理逻辑的一个特殊分支,Anonymous functions - the  lambda keyword-微积分。

例如,为了计算下面的表达式,我们可以使用 SciPy 的函数quad,它要求函数被积分作为它的第一个参数,积分界限作为接下来的两个参数:

Anonymous functions - the  lambda keyword

这里,要集成的函数只是一个简单的单行函数,我们使用lambda关键字来定义它:

import scipy.integrate as si
si.quad(lambda x: x ** 2 + 5, 0, 1)

语法如下:

lambda parameter_list: expression

lambda函数的定义只能由一个表达式组成,特别是不能包含循环。lambda与其他函数一样,函数是对象,可以分配给变量:

parabola = lambda x: x ** 2 + 5
parabola(3) # gives 14

λ结构总是可替换的

需要注意的是,lambda 构造只是 Python 中的语法糖。任何 lambda 结构都可以用显式函数定义来代替:

parabola = lambda x: x**2+5 
# the following code is equivalent
def parabola(x):
    return x ** 2 + 5

使用构造的主要原因是为了非常简单的函数,而完整的函数定义会过于繁琐。

lambda函数提供了第三种构造闭包的方法,正如我们继续前面的例子所展示的:

我们使用sin_omega函数计算不同频率下正弦函数的积分:

import scipy.integrate as si
for iteration in range(3):
    print(si.quad(lambda x: sin_omega(x, iteration*pi), 0, pi/2.) )

作为装饰者的功能

在部分应用部分,我们看到了如何使用一个函数来修改另一个函数。装饰器是 Python 中的语法元素,它方便地允许我们改变函数的行为,而不改变函数本身的定义。让我们从以下情况开始:

假设我们有一个决定矩阵稀疏程度的函数:

def how_sparse(A):
    return len(A.reshape(-1).nonzero()[0])

如果该函数不是以数组对象作为输入调用的,它将返回一个错误。更准确地说,它不适用于不实现reshape方法的对象。例如,how_sparse函数不适用于列表,因为列表没有reshape方法。下面的 helper 函数修改任何带有一个输入参数的函数,以便尝试对数组进行类型转换:

def cast2array(f):
    def new_function(obj):
        fA = f(array(obj))
        return fA
    return new_function

因此,修改后的函数how_sparse = cast2array(how_sparse)可以应用于任何可以转换为数组的对象。如果用类型转换功能修饰how_sparse的定义,也可以实现同样的功能。还建议考虑functools.wraps(详见【8】):

@cast2array
def how_sparse(A):
    return len(A.reshape(-1).nonzero()[0])

要定义装饰器,您需要一个可调用的对象,例如修改要装饰的函数定义的函数。主要目的是:

  • 通过从不直接为其功能服务的函数中分离出部分来提高代码的可读性(例如,记忆)
  • 将一系列相似函数的公共序言和结尾部分放在一个公共位置(例如,类型检查)
  • 能够轻松地关闭和打开功能的附加功能(例如,测试打印、跟踪)

总结

函数不仅是使程序模块化的理想工具,而且也反映了数学思维。您学习了函数定义的语法,以及如何区分定义和调用函数。

我们认为函数是可以被其他函数修改的对象。使用函数时,熟悉变量范围的概念以及信息如何通过参数传递到函数中是很重要的。

有时,用所谓的匿名函数动态定义函数是很方便的。为此,我们引入了关键字 lambda。

练习

例 1 →编写一个函数polar_to_comp,该函数接受两个参数 rExercises并返回复数Exercises指数函数使用 NumPy 函数exp

Ex 2 →在 Python 模块functools的描述中(参见【8】了解更多关于 functools 的详细信息)您可以找到以下 Python 函数:

def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = keywords.copy()
        newkeywords.update(fkeywords)
        return func(*(args + fargs), **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

解释并测试该功能。

Ex 3 →为函数how_sparse编写装饰器,通过将小于 1.e-16 的元素设置为零来清理输入矩阵 A (考虑第节中作为装饰器的示例)。

Ex 4 → A 连续函数 ff(A)f(b)<0 在区间【 a,b 中改变其符号,并且在该区间中至少有一个根(零)。这样的根可以用等分法找到。此方法从给定的时间间隔开始。然后研究子区间中的符号变化,

Exercises

如果符号在第一个子区间 b 中发生变化,则重新定义为:

Exercises

否则,它会以相同的方式重新定义为:

Exercises

并且重复该过程直到 b-a 小于给定的公差。

  • 将此方法实现为将作为参数的函数:
    • –功能 f
    • –初始间隔[ a,b ]
    • –公差
  • 该功能bisec应返回最终间隔及其中点。
  • 在区间[1.1,1.4] 以及在[1.3,1.4]中交替使用函数arctan和多项式 f(x) = 3 x 2 -5 测试该方法。

Ex。5 →两个整数的最大公约数可以用如下递归描述的欧几里德算法计算:

Exercises

写一个计算两个整数的最大公约数的函数。编写另一个函数,使用以下关系计算这些数字的最小公倍数:

Exercises

Ex。6 →研究切比雪夫多项式的递归实现,考虑递归函数部分的例子。以非递归方式重写程序,研究计算时间与多项式次数的关系(另见timeit模块)。

八、类

在数学中,当我们写罪的时候,我们指的是一个数学对象,我们从初等微积分中知道许多方法。例如:

  • 我们可能希望在 x= 0.5 处计算 sin x ,即计算 sin(0.5),它返回一个实数
  • 我们可能想计算它的导数,这给了我们另一个数学对象,cos
  • 我们可能需要计算泰勒多项式的前三个系数

这些方法不仅可以应用于 sin,还可以应用于其他足够平滑的函数。然而,还有其他的数学对象(例如数字 5 )这些方法对它们没有意义。具有相同方法的对象被组合在抽象类中,例如函数。每一个可以应用于函数的语句和方法都特别适用于 s in 或 cos。这类的其他例子可能是有理数,它有分母和分子方法;一个区间,它有左右边界法;一个无穷序列,我们可以问它是否有极限,等等。

在这种情况下,sin 被称为类的实例。数学短语让 g 是一个函数...在本文中称为实例化。这里, g 是函数的名称;可以分配给它的许多属性之一。另一个属性可能是它的域。

数学对象 p(x) = 2x 2 - 5 就像正弦函数一样。每种功能方法都适用于 p ,但是我们也可以为 p 定义特殊的方法。例如,我们可能会要求 p 的系数。这些方法可以用来定义多项式的种类。由于多项式是函数,它们还继承了函数类的所有方法。

在数学中,我们经常对完全不同的运算使用同一个运算符符号。例如,在 5+4 和 sin + cos 中,运算符符号+有不同的含义。通过使用相同的符号,人们试图表达数学运算的相似性。我们通过将这些术语应用于数学示例,从面向对象编程中引入了这些术语:

  • 实例和实例化
  • 遗产
  • 方法
  • 属性
  • 操作员超载

在本章中,我们将展示如何在 Python 中使用这些概念。

课程介绍

我们将用有理数的例子来说明类的概念,即形式为q = qNqD的数,其中qT10】N 和qT14】D 都是整数。

Introduction to classes

图 8.1:一个类声明的例子

我们这里使用有理数只是作为类概念的一个例子。关于 Python 中有理数的未来工作,请使用分数模块(请参考【6】)。

类语法

一个类的定义是由一个带有class关键字的块命令、类的名称和块中的一些语句组成的(参见图 8.1 ):

class RationalNumber: 
      pass

这个类的一个实例(或者换句话说,类型为RationalNumber的对象)是由

r = RationalNumber()

查询type(a)返回答案<class'__main__.RationalNumber'>。如果我们想调查一个对象是否是这个类的实例,我们可以使用这个:

if isinstance(a, RationalNumber):
    print('Indeed it belongs to the class RationalNumber')  

到目前为止,我们已经生成了一个RationalNumber类型的对象,它还没有数据。此外,没有定义对这些对象执行操作的方法。这将是下一部分的主题。

_ _ init _ _ 方法

现在我们为示例类提供一些属性;也就是说,我们给它定义数据。在我们的例子中,这个数据是分母和分子的值。为此,我们必须定义一个方法,__init__,用于用这些值初始化类:

class RationalNumber:
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator

在解释我们添加到类中的特殊__init__函数之前,我们演示一个RationalNumber对象的实例化:

q = RationalNumber(10, 20)    # Defines a new object
q.numerator    # returns 10
q.denominator    # returns 20

使用类名创建一个类型为RationalNumber的新对象,就像它是一个函数一样。这种说法做了两件事:

  • 它首先创建一个空对象q
  • 然后对其应用__init__功能;即执行q.__init__(10, 20)

__init__的第一个参数是指新对象本身。在函数调用时,第一个参数被对象的实例替换。这适用于该类的所有方法,而不仅仅适用于特殊方法__init__。第一个参数的特殊作用体现在命名为self的惯例上。在前面的例子中,__init__函数定义了新对象的两个属性,numeratordenominator

属性和方法

使用类的主要原因之一是对象可以组合在一起并绑定到一个公共对象。我们在看有理数时已经看到了这一点;分母和分子是我们绑定到RationalNumber类实例的两个对象。它们被称为实例的属性。一个对象是一个类实例的一个属性的事实从它们被引用的方式中变得显而易见,我们之前已经默认使用了这一点:

<object>.attribute

以下是实例化和属性引用的一些示例:

q = RationalNumber(3, 5) # instantiation
q.numerator     # attribute access
q.denominator

a = array([1, 2])    # instantiation
a.shape

z = 5 + 4j    # instantiation
z.imag

一旦定义了一个实例,我们就可以设置、更改或删除该特定实例的属性。语法与常规变量相同:

q = RationalNumber(3, 5) 
r = RationalNumber(7, 3)
q.numerator = 17
del r.denominator

更改或删除属性可能会产生不希望的副作用,甚至会使对象变得无用。我们将在相互依赖的属性一节中了解更多信息。由于函数也是对象,我们也可以将函数用作属性;它们被称为实例的方法:

<object>.method(<arguments...>)

例如,让我们向类RationalNumber添加一个方法,将数字转换为浮点数:

class RationalNumber:
...
    def convert2float(self):
        return float(self.numerator) / float(self.denominator)

同样,该方法将对象本身的引用self作为其第一个(也是唯一一个)参数。我们将此方法用于常规函数调用:

q = RationalNumber(10, 20)    # Defines a new object
q.convert2float() # returns 0.5   

这相当于以下调用:

RationalNumber.convert2float(q)

再次注意,对象实例是作为函数的第一个参数插入的。第一个参数的使用解释了如果该特定方法与其他参数一起使用时会出现的错误消息:

q.convert2float(15)调用会引发以下错误信息:

TypeError: convert2float() takes exactly 1 argument (2 given)

这不起作用的原因是q.convert2float(15)精确地等同于RationalNumber.convert2float(q,15),这是失败的,因为RationalNumber.convert2float只接受一个参数。

特殊方法

特殊的方法__repr__让我们能够定义对象在 Python 解释器中的表示方式。对于有理数,该方法的可能定义如下:

class RationalNumber:
 ...
    def __repr__(self):
        return '{} / {}'.format(self.numerator,self.denominator)

定义了这个方法,只需输入q返回 10 / 20。

我们希望有一种方法可以将两个有理数相加。第一次尝试可能会产生这样的方法:

class RationalNumber:
...
    def add(self, other):
        p1, q1 = self.numerator, self.denominator
        if isinstance(other, int):
            p2, q2 = other, 1
        else:
            p2, q2 = other.numerator, other.denominator
        return RationalNumber(p1 * q2 + p2 * q1, q1 * q2)

对此方法的调用采用以下形式:

q = RationalNumber(1, 2)
p = RationalNumber(1, 3)
q.add(p)   # returns the RationalNumber for 5/6

如果我们能写q + p就更好了。但到目前为止,RationalNumber类型没有定义加号。这是通过使用__add__特殊方法完成的。因此,仅仅将add重命名为__add__就允许使用有理数的加号:

q = RationalNumber(1, 2)
p = RationalNumber(1, 3)
q + p # RationalNumber(5, 6)

表达式q + p实际上是表达式q.__add__(p)的别名。在表(表 8.1) 中,你会发现二元运算符的特殊方法,如+-*

| **操作员** | **方法** | **操作员** | **方法** | | `+` | `__add__` | `+=` | `__iadd__` | | `*` | `__mul__` | `*=` | `__imul__` | | `-` | `__sub__` | `-=` | `__isub__` | | `/` | `__truediv__` | `/=` | `__itruediv__` | | `//` | `__floordiv__` | `//=` | `__ifloordiv__` | | `**` | `__pow__` | | | | `==` | `__eq__` | `!=` | `__ne__` | | `<=` | `__le__` | `<` | `__lt__` | | `>=` | `__ge__` | `>` | `__gt__` | | `()` | `__call__` | `[]` | `__getitem__` |

表 8.1:部分 Python 运算符及对应的类方法,可以找到完整的列表【31】

新类的这些运算符的实现称为运算符重载。运算符重载的另一个例子是检查两个有理数是否相同的方法:

class RationalNumber:
...
    def __eq__(self, other):
        return self.denominator * other.numerator == 
            self.numerator * other.denominator

它是这样使用的:

p = RationalNumber(1, 2) # instantiation
q = RationalNumber(2, 4) # instantiation
p == q # True

属于不同类的对象之间的操作需要特别小心:

p = RationalNumber(1, 2) # instantiation
p + 5  # corresponds to p.__add__(5)  
5 + p  # returns an error

默认情况下,+运算符调用左操作数的方法__add__。我们对它进行了编程,使得它既允许int类型的对象,也允许RationalNumber类型的对象。在语句5 + p中,操作数被转换,并调用内置int类型的__add__方法。这个方法返回一个错误,因为它不知道如何处理有理数。这个案子可以用__radd__的方法处理,我们现在就用这个方法装备RationalNumber班。方法__radd__叫做反向加法。

反向操作

如果像+这样的操作应用于两个不同类型的操作数,则首先调用左操作数的对应方法(在本例中为__add__)。如果这引发异常,则调用右操作数的反向方法(此处为__radd__)。如果该方法不存在,则会引发TypeError异常。

考虑一个反向操作的例子。为了启用操作 5+ p ,其中 pRationalNumber的一个实例,我们定义如下:

class RationalNumber:
   ....
    def __radd__(self, other):
        return self + other

注意__radd__交换参数的顺序;selfRationalNumber类型的对象,而其他是必须转换的对象。

将类实例与方括号 ( )或[,]一起使用时,会调用一个特殊方法__call____getitem__中的一个方法,为该实例提供一个函数或一个可迭代对象的行为(请参考 T able 8.1 中的这些和其他特殊方法):

class Polynomial:
...
    def __call__(self, x):
        return self.eval(x)

现在可以如下使用:

p = Polynomial(...)
p(3.) # value of p at 3.

如果类提供了迭代器,__getitem__特殊方法就有意义了(在考虑下面的例子之前,建议参考第 9 章迭代器一节迭代器)。

这个递归uI+1= a1T6】uIT9】+aT12】0T14】uIT18】-1 被称为一个三 - 术语递归。它在应用数学,特别是正交多项式的构造中起着重要的作用。我们可以通过以下方式将三项递归设置为一个类:

import itertools

class  Recursion3Term:
    def __init__(self, a0, a1, u0, u1):
        self.coeff = [a1, a0]
        self.initial = [u1, u0]
    def __iter__(self):
        u1, u0 = self.initial
        yield u0  # (see also Iterators section in Chapter 9) 
        yield u1
        a1, a0 = self.coeff
        while True :
            u1, u0 = a1 * u1 + a0 * u0, u1
            yield u1
    def __getitem__(self, k):
        return list(itertools.islice(self, k, k + 1))[0]

这里,__iter__方法定义了一个生成器对象,它允许我们使用类的一个实例作为迭代器:

r3 = Recursion3Term(-0.35, 1.2, 1, 1)
for i, r in enumerate(r3):
    if i == 7:
        print(r)  # returns 0.194167
        break

__getitem__方法使我们能够直接访问迭代,就像r3是一个列表一样:

r3[7] # returns 0.194167

请注意,我们在编码__getitem__方法时使用了itertools.islice(更多信息,请参考第 9 章、迭代迭代器部分)。在第 5 章高级数组概念双变量函数一节中给出了一个使用__getitem__和切片以及函数ogrid的例子。

相互依赖的属性

只需为实例的属性赋值,就可以更改(或创建)它们。但是,如果其他属性依赖于刚刚更改的属性,则最好同时更改这些属性:

让我们考虑一个从三个给定点为平面三角形定义一个对象的类。建立这样一个类的第一次尝试如下:

class Triangle:
    def __init__(self,  A, B, C):
        self.A = array(A)
        self.B = array(B)
        self.C = array(C)
        self.a = self.C - self.B
        self.b = self.C - self.A
        self.c = self.B - self.A
    def area(self):
        return abs(cross(self.b, self.c)) / 2

这个三角形的一个实例是这样创建的:

tr = Triangle([0., 0.], [1., 0.], [0., 1.])

它的面积是这样计算的:

tr.area() # returns 0.5

如果我们改变一个属性,比如点 B ,对应的边 ac 不会自动更新,计算的面积是错误的:

tr.B = [12., 0.]
tr.area() # still returns 0.5, should be 6 instead.

补救方法是定义一个在属性改变时执行的方法;这样的方法称为 setter 方法。相应地,人们可能会要求在请求属性值时执行的方法;这种方法称为 getter 方法。

属性函数

函数property将一个属性链接到这样的 getter、setter 和 deleter 方法。它也可以用于将文档字符串分配给属性:

attribute = property(fget = get_attr, fset = set_attr, 
                     fdel = del_attr, doc = string)

我们用 setter 方法继续前面的例子,并再次考虑Trinagle类。如果以下陈述包含在Triangle

B = property(fget = get_B, fset = set_B, fdel = del_B, doc = ’The point B of a triangle’)

命令

tr.B = <something>

调用 setter 方法set_B

让我们修改三角形类:

class Triangle:
    def __init__(self, A, B, C):
        self._A = array(A)
        self._B = array(B)
        self._C = array(C)
        self._a = self._C - self._B
        self._b = self._C - self._A
        self._c = self._B - self._A
    def area(self):
        return abs(cross(self._c, self._b)) / 2.
    def set_B(self, B):
        self._B = B
        self._a = self._C - self._B
        self._c = self._B - self._A
    def get_B(self):
        return self._B
    def del_Pt(self):
        raise Exception('A triangle point cannot be deleted')
    B = property(fget = get_B, fset = set_B, fdel = del_Pt)

如果属性B改变,则方法set_B将新值存储在内部属性_B中,并改变所有相关属性:

tr.B = [12., 0.]
tr.area() # returns 6.0

这里使用deleter方法的方式是为了防止删除属性:

del tr.B # raises an exception

使用下划线作为属性名称的前缀是一种约定,用于指示不是为直接访问而设计的属性。它们旨在保存由设置器和获取器处理的属性的数据。这些属性在其他编程语言的意义上不是私有的;它们只是不打算被直接访问。

结合和非结合方法

我们现在将仔细看看属于方法的属性。让我们考虑一个例子:

class A:
    def func(self,arg):
        pass

一个小小的检查向我们展示了func的性质在创建一个实例后是如何变化的:

A.func  # <unbound method A.func>
instA = A()  # we create an instance
instA.func  #  <bound method A.func of ... >

例如,调用A.func(3)会导致如下错误消息:

TypeError: func() missing 1 required positional argument: 'arg'

instA.func(3)按预期执行。创建实例时,func方法绑定到该实例。self参数将实例赋值。将方法绑定到实例使该方法可用作函数。在此之前,是没有用的。类方法,我们稍后会考虑,在这方面是不同的。

类属性

类声明中指定的属性称为类属性。考虑以下示例:

class Newton:
    tol = 1e-8 # this is a class attribute
    def __init__(self,f):
        self.f = f # this is not a class attribute
    ....

类属性对于模拟默认值很有用,并且可以在必须重置值时使用:

N1 = Newton(f)
N2 = Newton(g)

两个实例都有一个属性tol,其值在类定义中初始化:

N1.tol # 1e-8
N2.tol # 1e-8

更改类属性会自动影响所有实例的所有相应属性:

Newton.tol = 1e-10
N1.tol # 1e-10
N2.tol # 1e-10

更改一个实例的tol不会影响另一个实例:

N2.tol = 1.e-4
N1.tol  # still 1.e-10

但是现在N2.tol脱离了类属性。更改Newton.tolN2.tol不再有任何影响;

Newton.tol = 1e-5 # now all instances of the Newton classes have 1e-5
N1.tol # 1.e-5
N2.tol # 1e-4 but not N2.

类方法

我们在前面关于绑定和未绑定方法的章节中看到了方法是如何绑定到类的实例或者作为未绑定方法保持状态的。类方法不同。它们总是绑定方法。他们与类本身息息相关。

我们将首先描述语法细节,然后给出一些例子来说明这些方法可以用来做什么。要表明一个方法是一个类方法,装饰行在方法定义之前:

@classmethod

虽然标准方法通过使用它们的第一个参数来引用实例,但是类方法的第一个参数引用类本身。按照惯例,标准方法的第一个参数叫做self,类方法的第一个参数叫做cls

  • 标准案例:
      class A:
          def func(self,*args):
               <...>
  • 类方法案例:
      class B:
          @classmethod
          def func(cls,*args):
               <...>

实际上,类方法对于在创建实例之前执行命令可能是有用的,例如在预处理步骤中。请参见以下示例:

在本例中,我们展示了如何在创建实例之前使用类方法准备数据:

class Polynomial:
    def __init__(self, coeff):
        self.coeff = array(coeff)
    @classmethod
    def by_points(cls, x, y):
        degree = x.shape[0] - 1
        coeff = polyfit(x, y, degree)
        return cls(coeff) 
    def __eq__(self, other):
        return allclose(self.coeff, other.coeff)

该类被设计为通过指定其系数来创建多项式对象。或者,by_points类方法允许我们通过插值点定义多项式。即使没有多项式实例可用,我们也可以将插值数据转换为多项式系数:

p1 = Polynomial.by_points(array([0., 1.]), array([0., 1.]))
p2 = Polynomial([1., 0.])

print(p1 == p2)  # prints True

本章后面的示例中介绍了类方法的另一个示例。在该示例中,类方法用于访问与该类的几个(或所有)实例相关的信息。

子类化和继承

在本节中,我们将介绍面向对象编程的一些核心概念:抽象类、子类和继承。为了指导您理解这些概念,我们考虑另一个数学示例:求解微分方程的一步方法。普通初值问题的一般形式是

Subclassing and inheritance

数据为右侧函数 f 、初始值 x 0 、关注区间【 t 0 、t e 】。这个问题的解决是一个功能Subclassing and inheritance。一种数值算法将该解作为离散值 u i 的向量 u 给出,近似为x(tI)。这里,Subclassing and inheritance是自变量 t 的离散值,在物理模型中常表示时间。

一步法通过递归步骤构造解值 u i :

Subclassing and inheritance

这里,φ是表征单个方法的阶跃函数(参见【28】):

  • 显式欧拉 : Subclassing and inheritance
  • 中点法则 : Subclassing and inheritance
  • 龙格-库塔 4 : Subclassing and inheritanceSubclassing and inheritance

我们在这里所做的是描述数学算法的典型方式。我们首先根据方法的思想来描述它,以抽象的方式给出它的步骤。为了实际使用它,我们必须填写一个具体方法的参数,在这个例子中,函数φ。这也是面向对象编程中解释事物的方式。首先,我们用方法的抽象描述建立一个类:

class OneStepMethod:
    def __init__(self, f, x0, interval, N):
        self.f = f
        self.x0 = x0
        self.interval = [t0, te] = interval
        self.grid = linspace(t0, te, N)
        self.h = (te - t0) / N

    def generate(self):
        ti, ui = self.grid[0], self.x0
        yield ti, ui
        for t in self.grid[1:]:
            ui = ui + self.h * self.step(self.f, ui, ti)
            ti = t
            yield ti, ui

    def solve(self):
        self.solution = array(list(self.generate()))

    def plot(self):
        plot(self.solution[:, 0], self.solution[:, 1])

    def step(self, f, u, t):
        raise NotImplementedError()

这个抽象类及其方法被用作单个方法的模板:

class ExplicitEuler(OneStepMethod):
    def step(self, f, u, t):
        return f(u, t)

class MidPointRule(OneStepMethod):
    def step(self, f, u, t):
        return f(u + self.h / 2 * f(u, t), t + self.h / 2)

请注意,在类定义中,我们用作模板的抽象类的名称OneStepMethod是作为额外的参数给出的:

class ExplicitEuler(OneStepMethod)

该类称为父类。父类的所有方法和属性都被子类继承,只要它们没有被覆盖。如果它们在子类中被重新定义,它们将被覆盖。step方法在子类中被重新定义,而方法generate对于整个族是通用的,因此从父族继承而来。在考虑进一步的细节之前,我们将演示如何使用这三个类:

def f(x, t):
    return -0.5 * x

euler = ExplicitEuler(f, 15., [0., 10.], 20)
euler.solve()
euler.plot()
hold(True)
midpoint = MidPointRule(f, 15., [0., 10.], 20)

midpoint.solve()
midpoint.plot()

使用星形运算符可以避免常见参数列表的重复(详见第七章函数变参数一节):

...
argument_list = [f, 15., [0., 10.], 20]
euler = ExplicitEuler(*argument_list)
...
midpoint = MidPointRule(*argument_list)
...

请注意,抽象类从未用于创建实例。由于step方法没有完全定义,调用它会引发类型为NotImplementedError的异常。

有时必须访问父类的方法或属性。这是使用命令super完成的。当子类使用自己的__init__方法来扩展父类的__init__时,这很有用:

例如,让我们假设我们想要给每个求解器类一个带有求解器名称的字符串变量。为此,我们为求解器提供了一个__init__方法,因为它覆盖了父级的__init__方法。在两种方法都应该使用的情况下,我们必须通过命令super来引用父方法:

class ExplicitEuler(OneStepMethod):
    def __init__(self,*args, **kwargs):
        self.name='Explicit Euler Method'
        super(ExplicitEuler, self).__init__(*args,**kwargs)
    def step(self, f, u, t):
        return f(u, t)

请注意,可以显式使用父类的名称。而super的使用允许我们更改父类的名称,而不必更改对父类的所有引用。

封装

有时使用继承是不切实际的,甚至是不可能的。这激发了封装的使用。我们将通过考虑 Python 函数来解释封装的概念,即 Python 类型function的对象,我们将其封装在一个新的类Function中,并提供一些相关的方法:

class Function:
    def __init__(self, f):
        self.f = f
    def __call__(self, x):
        return self.f(x)
    def __add__(self, g):
        def sum(x):
            return self(x) + g(x)
        return type(self)(sum) 
    def __mul__(self, g): 
        def prod(x):
            return self.f(x) * g(x)
        return type(self)(prod)
    def __radd__(self, g):
        return self + g
    def __rmul__(self, g):
        return self * g

请注意,__add____mul__操作应该返回同一个类的实例。这是通过return type(self)(sum)声明实现的,在这种情况下,这是一种更一般的写作形式return Function(sum)。我们现在可以通过继承来派生子类:

作为一个例子,切比雪夫多项式可以在区间[1,-1]中通过以下方式计算:

Encapsulation

我们构建一个切比雪夫多项式作为Function类的实例:

T5 = Function(lambda x: cos(5 * arccos(x)))
T6 = Function(lambda x: cos(6 * arccos(x)))

切比雪夫多项式在某种意义上是正交的:

Encapsulation

使用这种结构可以很容易地进行检查:

import scipy.integrate as sci

weight = Function(lambda x: 1 / sqrt((1 - x ** 2)))
[integral, errorestimate] = 
        sci.quad(weight * T5 * T6, -1, 1) # (6.510878470473995e-17, 1.3237018925525037e-14)

没有封装,像写weight * T5 * T6一样简单的乘法函数是不可能的。

装饰类

第 7 章功能功能作为装饰者一节中,我们看到了如何通过应用另一个功能作为装饰者来修改功能。在前面的例子中,我们看到了只要类被提供了__call__方法,它们是如何表现为函数的。我们将在这里用它来展示类如何被用作装饰器。

让我们假设我们想要以这样一种方式改变一些函数的行为,即在调用函数之前,打印所有的输入参数。这可能对调试有用。我们以这种情况为例来解释装饰器类的使用:

class echo:
    text = 'Input parameters of {name}n'+
        'Positional parameters {args}n'+
        'Keyword parameters {kwargs}n'
    def __init__(self, f):
        self.f = f
    def __call__(self, *args, **kwargs):
        print(self.text.format(name = self.f.__name__,
              args = args, kwargs = kwargs))
        return self.f(*args, **kwargs)

我们使用这个类来修饰函数定义,

@echo
def line(m, b, x):
    return m * x + b

像往常一样调用函数,

line(2., 5., 3.)
line(2., 5., x=3.)

在第二次调用中,我们获得了以下输出:

Input parameters of line
Positional parameters (2.0, 5.0)
Keyword parameters {'x': 3.0}

11.0

这个例子表明类和函数都可以用作装饰器。类允许更多的可能性,因为它们也可以用来收集数据。

事实上,我们注意到:

  • 每个修饰的函数都会创建装饰器类的一个新实例。
  • 一个实例收集的数据可以通过类属性保存并供另一个实例访问(参见第 8 章中的属性一节)。

最后一点强调与功能装饰者的区别。我们现在通过一个装饰器来展示这一点,该装饰器对函数调用进行计数,并将结果存储在以函数为键的字典中。

为了分析算法的性能,统计特定函数的调用可能是有用的。我们可以在不改变函数定义的情况下获取计数器信息。该代码是对【4】中给出的一个例子的轻微修改。

class CountCalls:
    """
    Decorator that keeps track of the number of times 
    a function is called.
    """
    instances = {} 
    def __init__(self, f):
        self.f = f
        self.numcalls = 0
        self.instances[f] = self
    def __call__(self, *args, **kwargs):
        self.numcalls += 1
        return self.f(*args, **kwargs)
    @classmethod
    def counts(cls):
        """
        Return a dict of {function: # of calls} for all 
        registered functions.
        """
        return dict([(f.__name__, cls.instances[f].numcalls) 
                                    for f in cls.instances])

这里,我们使用类属性CountCalls.instances来存储每个单独实例的计数器。让我们看看这个装饰器是如何工作的:

@CountCalls
def line(m, b, x):
    return m * x + b
@CountCalls 
def parabola(a, b, c, x):
    return a * x ** 2 + b * x + c
line(3., -1., 1.)
parabola(4., 5., -1., 2.)

CountCalls.counts() # returns {'line': 1, 'parabola': 1}
parabola.numcalls # returns 1

总结

现代计算机科学中最重要的编程概念之一是面向对象编程。在本章中,我们学习了如何将对象定义为类的实例,我们提供了方法和属性。方法的第一个参数,通常用self表示,起着重要而特殊的作用。您看到了可用于为自己的类定义基本操作的方法,如+*

虽然在其他编程语言中,属性和方法可以防止意外使用,但 Python 允许通过特殊的 getter 和 setter 方法隐藏属性和访问这些隐藏属性的技术。为此,你遇到了一个重要的功能,property

练习

Ex。1 →给类RationalNumber写一个方法simplify。这个方法应该以元组的形式返回分数的简化版本。

Ex。2 →为了给结果提供置信区间,在数值数学中引入了一种特殊的微积分,即所谓的区间算法;(参考【3、14】)。定义一个名为Interval的类,为其提供加减乘除的方法(仅限正整数)。这些操作遵循以下规则:

Exercises

为该类提供允许类型为 a + I、a I、I + a、I a 的操作的方法,其中 I 是一个区间, a 是一个整数或浮点数。首先将整数或浮点数转换为区间[a,a]。(提示:您可能希望为此使用函数装饰器;(参见第七章、功能作为装饰者的功能一节)。此外,实现__contains__方法,该方法使您能够使用“区间”类型的对象I的语法x in I来检查给定的数字是否属于区间。通过对一个区间应用多项式f=lambda x: 25*x**2-4*x+1来测试你的类。

Ex。3 →考虑类作为装饰者一节下的例子。扩展这个例子,得到一个函数装饰器,计算某个函数被调用的频率。

Ex。4 →比较两种实现类 RationalNumber中反向加法__radd__的方法:特殊方法一节例子中给出的方法和这里给出的方法:

class RationalNumber:
    ....
    def __radd__(self, other):
        return other + self

你认为这个版本会有错误吗?错误是什么,你如何解释?通过执行以下命令来测试您的答案:

q = RationalNumber(10, 15)
5 + q

Ex。4 →将装饰者类CountCalls视为装饰者一节中的示例。为这个类提供一个方法reset,将字典中所有函数CountCalls.instances的计数器设置为零。如果字典换成空字典会怎么样?

九、重复

在本章中,我们将介绍使用循环和迭代器的迭代。我们将展示如何将它用于列表和生成器的示例。迭代是计算机有用的基本操作之一。传统上,迭代是通过for循环来实现的。for循环是指令块重复一定次数。在循环内部,可以访问一个循环变量,其中存储了迭代号。

Python 的习惯用法略有不同。Python 中的for循环主要是为了穷尽一个列表,也就是枚举一个列表的元素。如果使用包含第一个 n 个整数的列表,效果类似于刚才描述的重复效果。

一个for循环一次只需要列表中的一个元素。因此,希望对能够按需创建这些元素的对象使用for循环,一次一个。这就是迭代器在 Python 中实现的功能。

for 语句

for语句的主要目的是遍历列表:

for s in ['a', 'b', 'c']:
    print(s), # a b c

在本例中,循环变量 s 被连续分配给列表中的一个元素。请注意,循环变量在循环结束后可用。这有时可能有用;例如,参考章节中的例子控制回路内的流量。

for循环最常见的用途之一是使用功能range将给定任务重复定义的次数(参见第 1 章入门列表部分)。

for iteration in range(n): # repeat the following code n times
    ...

如果循环的目的是遍历列表,许多语言(包括 Python)提供以下模式:

for k in range(...):
    ...
    element = my_list[k]

如果该代码的目的是浏览列表my_list,前面的代码不会使它非常清楚。因此,更好的表达方式如下:

for element in my_list:
    ...

现在乍看之下很清楚,前面的代码通过了my_list列表。请注意,如果您真的需要索引变量 k ,您可以用以下代码替换前面的代码:

for k, element in enumerate(my_list):
    ...

这段代码的目的是在保持索引变量 k 可用的同时通过my_list。类似的数组结构是命令ndenumerate

控制回路内的流量

有时需要跳出循环,或者直接进入下一个循环迭代。这两个操作由breakcontinue命令执行。顾名思义,break关键字打破了这个循环。循环中断时会出现两种情况:

  • 循环被完全执行。
  • 该循环在完全执行之前被留下(break)。

对于第一种情况,可以在else块中定义特殊动作,如果遍历了整个列表,则执行该块。如果for循环的目的是寻找某样东西并停止,这通常是有用的。例如,在列表中搜索一个满足某个属性的元素。如果没有找到这样的元素,则执行else块。

这是科学计算中的一个常见用法。通常,我们使用的迭代算法不能保证成功。在这种情况下,最好使用(大的)有限循环,这样程序就不会陷入无限循环。for / else构造允许这样的实现:

maxIteration = 10000
for iteration in range(maxIteration):
    residual = compute() # some computation
    if residual < tolerance:
        break
else: # only executed if the for loop is not broken
    raise Exception("The algorithm did not converge")
print("The algorithm converged in {} steps".format(iteration+1))

迭代器

for循环主要用于遍历列表,但它一次只选取列表中的一个元素。特别是,不需要将整个列表存储在内存中,循环就能正常工作。允许for循环在没有列表的情况下工作的机制是迭代器。

可迭代对象产生对象(传递到for循环)。这样的物体obj可以在for回路中使用,如下所示:

for element in obj:
    ...

迭代器的概念概括了列表的概念。列表给出了可迭代对象的最简单的例子。生成的对象只是存储在列表中的对象:

L = ['A', 'B', 'C']
for element in L:
    print(element)

可迭代对象不需要产生现有对象。相反,这些物体可以在飞行中产生。

典型的可迭代对象是函数range返回的对象。这个函数的工作方式就好像它会生成一个整数列表,但是相反,连续的整数是在需要时动态生成的:

for iteration in range(100000000):
    # Note: the 100000000 integers are not created at once
    if iteration > 10:
        break

如果真的需要一个 0 到 100,000,000 之间的所有整数的列表,那么它必须是显式形成的:

l=list(range(100000000))

发电机

您可以使用yield关键字创建自己的迭代器。例如,小于 n 的奇数发生器可以定义为:

def odd_numbers(n):
    "generator for odd numbers less than n"
    for k in range(n):
        if k % 2 == 1:
            yield k

那么您可以按如下方式使用它:

g = odd_numbers(10)
for k in g:
    ...    # do something with k

或者甚至像这样:

for k in odd_numbers(10):
    ... # do something with k

迭代器是一次性的

迭代器的一个显著特点是它们只能使用一次。为了再次使用迭代器,您必须创建一个新的迭代器对象。请注意,可迭代对象可以根据需要多次创建新的迭代器。让我们来研究一下列表的情况:

L = ['a', 'b', 'c']
iterator = iter(L)
list(iterator) # ['a', 'b', 'c']
list(iterator) # [] empty list, because the iterator is exhausted

new_iterator = iter(L) # new iterator, ready to be used
list(new_iterator) # ['a', 'b', 'c']

每次调用生成器对象时,都会创建一个新的迭代器。因此,当迭代器用完时,必须再次调用生成器来获得新的迭代器:

g = odd_numbers(10)
for k in g:
    ... # do something with k

# now the iterator is exhausted:
for k in g: # nothing will happen!!
    ...

# to loop through it again, create a new one:
g = odd_numbers(10)
for k in g:.
    ...

迭代器工具

这里有几个迭代器工具,它们经常会派上用场:

  • enumerate用于枚举另一个迭代器。它产生一个新的迭代器,产生对(迭代,元素),其中iteration存储迭代的索引:
      A = ['a', 'b', 'c']
      for iteration, x in enumerate(A):
          print(iteration, x)
      # result: (0, 'a') (1, 'b') (2, 'c')
  • reversed通过向后遍历列表,从列表中创建一个迭代器。请注意,这不同于创建反向列表:
      A = [0, 1, 2]
      for elt in reversed(A):,
          print(elt)
          # result: 2 1 0
  • itertools.count可能是整数的无限迭代器:
      for iteration in itertools.count():
          if iteration > 100:
              break # without this, the loop goes on forever
          print("integer {}".format(iteration))
          # prints the 100 first integer
  • intertools.islice使用熟悉的slicing语法截断迭代器;参考第三章集装箱类型。一个应用正在从无限迭代器创建有限迭代器:
      from itertools import count, islice
      for iteration in islice(count(), 10): 
          # same effect as range(10)
          ...

例如,让我们通过将islice与一个无限发生器相结合来找到一些奇数。首先,我们修改奇数的生成器,使其成为无限生成器:

def odd_numbers():
    k=-1
    while True:
        k+=1
        if k%2==1:
        yield k

然后,我们用它和islice一起得到一些奇数的列表:

list(itertools.islice(odd_numbers(),10,30,8)) # returns [21, 37, 53]

递归序列的生成器

假设一个序列由一个归纳公式给出。例如,考虑由递归公式定义的斐波那契数列:un= unT6】-1+uT11】n-2

该序列取决于两个初始值,即 u 0u 1 ,尽管对于标准斐波那契序列,这些数字分别取为 0 和 1。编写这种序列生成程序的一种巧妙方法是使用生成器,如下所示:

def fibonacci(u0, u1):
    """
    Infinite generator of the Fibonacci sequence.
    """
    yield u0
    yield u1
    while True:
        u0, u1 = u1, u0+u1
        yield u1

例如,可以这样使用:

# sequence of the 100 first Fibonacci numbers:
list(itertools.islice(fibonacci(0, 1), 100))

算术几何平均值

基于迭代计算算术和几何平均的迭代称为 AGM 迭代(更多信息请参考【1,第 598 页】):

 Arithmetic geometric mean

它具有迷人的特性:

 Arithmetic geometric mean

右边的积分叫做第一类完全椭圆积分。我们现在开始计算这个椭圆积分。我们使用生成器来描述迭代:

def arithmetic_geometric_mean(a, b):
    """
    Generator for the arithmetic and geometric mean
    a, b initial values
    """ 
    while True:    # infinite loop
         a, b = (a+b)/2, sqrt(a*b)
         yield a, b

当序列{ a i }收敛时,由{ c i }定义的序列{ c i }收敛到 0-这一事实将用于终止计算椭圆积分的程序中的迭代:

def elliptic_integral(k, tolerance=1e-5):
    """
    Compute an elliptic integral of the first kind.
    """
    a_0, b_0 = 1., sqrt(1-k**2)
    for a, b in arithmetic_geometric_mean(a_0, b_0):
        if abs(a-b) < tolerance:
            return pi/(2*a)

我们必须确保算法停止。请注意,该代码完全依赖于算术几何平均迭代收敛(快速)的数学陈述。在实际计算中,我们在应用理论结果时必须小心,因为它们在有限精度的算术中可能不再有效。使前面的代码安全的正确方法是使用itertools.islice。安全代码如下(参见控制回路内流量一节下的示例,了解for / else语句的另一个典型用法):

from itertools import islice
def elliptic_integral(k, tolerance=1e-5, maxiter=100):
    """
    Compute an elliptic integral of the first kind.
    """
    a_0, b_0 = 1., sqrt(1-k**2)
    for a, b in islice(arithmetic_geometric_mean(a_0, b_0), 
                                                  maxiter):
        if abs(a-b) < tolerance:
            return pi/(2*a)
    else:
        raise Exception("Algorithm did not converge")

作为一种应用,椭圆积分可用于计算长度为 L摆的周期,该摆以角度θ开始(更多信息,请参考【18,第 114 页】),使用:

 Arithmetic geometric mean

使用这个公式,钟摆的周期很容易得到:

def pendulum_period(L, theta, g=9.81):
    return 4*sqrt(L/g)*elliptic_integral(sin(theta/2))

收敛加速度

我们给出了一个应用发电机加速收敛的例子。本演示紧跟 Python 生成器技巧Pramode C.E 给出的示例(更多信息请参考【9】)。

请注意,一个生成器可能会将另一个生成器作为输入参数。例如,假设我们已经定义了一个生成收敛序列元素的生成器。然后,由于欧拉艾特肯,通常称为艾特肯的δ2-方法(参考【33】),有可能通过加速技术来改善收敛。它通过定义将序列 s i 转换为另一个序列

Convergence acceleration

两个序列具有相同的极限,但是序列Convergence acceleration收敛得明显更快。一种可能的实现如下:

def Euler_accelerate(sequence):
    """
    Accelerate the iterator in the variable `sequence`.
    """
    s0 = next(sequence) # Si
    s1 = next(sequence) # Si+1
    s2 = next(sequence) # Si+2
    while True:
        yield s0 - ((s1 - s0)**2)/(s2 - 2*s1 + s0)
  s0, s1, s2 = s1, s2, next(sequence)

例如,我们使用经典系列:

Convergence acceleration

向π/4 收敛。我们在下面的代码中将这个系列实现为一个生成器:

def pi_series():
    sum = 0.
    j = 1
    for i in itertools.cycle([1, -1]):
        yield sum
        sum += i/j
        j += 2

我们现在可以使用这个序列的加速版本:

Euler_accelerate(pi_series())

因此,该加速序列的第一 N 元素通过以下方式获得:

itertools.islice(Euler_accelerate(pi_series()), N)

例如,下图(图 9.1 )显示了由上述公式定义的序列的标准版本及其加速版本的误差对数的收敛速度:

Convergence acceleration

图 9.1:定义的序列与其加速版本之间的比较

列出填充模式

在本节中,我们将比较不同的列表填充方式。它们在计算效率和代码可读性方面是不同的。

列表填充用追加法

无处不在的编程模式是计算元素并将它们存储在列表中:

L = []
for k in range(n):
    # call various functions here
    # that compute "result"
    L.append(result)

这种方法有许多缺点:

  • 迭代的次数是预先决定的。如果有break指令,那么前面的代码负责生成值和决定何时停止。这是不可取的,也缺乏灵活性。
  • 它假设用户想要所有迭代的整个计算历史。假设我们只对所有计算值的总和感兴趣。如果有许多计算值,存储它们是没有意义的,因为一次添加一个会更有效。

迭代器列表

迭代器为我们之前讨论的问题提供了一个优雅的解决方案:

def result_iterator():
    for k in itertools.count(): # infinite iterator
        # call various functions here
        # that compute "result"
        ...
        yield result

使用迭代器,我们将生成计算值的任务分开,而不用担心停止条件或存储。如果该代码的用户想要存储 n 个第一值,可以使用list构造函数轻松完成:

L = list(itertools.islice(result_iterator(), n)) # no append needed!

如果用户想要第一个 n 个生成值的总和,建议采用以下结构:

# make sure that you do not use scipy.sum here
s = sum(itertools.islice(result_iterator(), n))

我们在这里做的是一方面分离元素的生成,另一方面存储这些元素。

如果目的确实是建立一个列表,并且当每一步的结果不依赖于先前计算的元素时,可以使用列表理解语法(更多信息,请参考第 3 章容器类型列表部分):

L = [some_function(k) for k in range(n)]

当迭代计算依赖于先前计算值的值时,列表理解没有帮助。

存储生成的值

使用迭代器来填充列表在大多数情况下都会很好地工作,但是当计算新值的算法容易抛出异常时,这种模式就会变得复杂;如果迭代器在这个过程中引发了异常,那么列表将不可用!下面的例子说明了这个问题。

假设我们生成由Storing generated values递归定义的序列。如果初始数据 u 0 大于 1,则该序列迅速发散至无穷大。让我们用一个生成器来生成它:

import itertools
def power_sequence(u0):
    u = u0
    while True:
        yield u
        u = u**2

如果试图通过执行获得序列的第一个 20 元素(由 u 0 = 2 初始化),

list(itertools.islice(power_sequence(2.), 20))

将引发异常,并且没有可用的列表,甚至没有引发异常之前的元素列表。目前还没有办法从可能有故障的发电机获得一个部分填充的列表。唯一的办法是使用封装在异常捕获块中的追加方法(更多详细信息,请参考第 10 章错误处理中的异常一节):

generator = power_sequence(2.)
L = []
for iteration in range(20):
    try:
        L.append(next(generator))
    except Exception:
        ...

当迭代器表现为列表时

一些列表操作也适用于迭代器。我们现在将检查列表理解列表压缩的等效项(更多详细信息,请参考第 3 章、容器类型列表部分)。

生成器表达式

这相当于对生成器的列表理解。这样的构造称为生成器表达式:

g = (n for n in range(1000) if not n % 100)
# generator for  100, 200, ... , 900

这对于计算和或积特别有用,因为那些运算是增量的;他们一次只需要一个元素:

sum(n for n in range(1000) if not n % 100) # returns 4500 

在这段代码中,您注意到sum函数有一个参数,这是一个生成器表达式。请注意,当生成器仅用作函数的参数时,Python 语法允许我们省略生成器的括号。

*让我们计算黎曼泽塔函数 ζ ,其表达式为

Generator expression

使用生成器表达式,我们可以在一行中计算这个系列的部分和:

sum(1/n**s for n in itertools.islice(itertools.count(1), N))

请注意,我们还可以定义序列 1nnsT3 的生成器,如下所示:

def generate_zeta(s):
    for n in itertools.count(1):
        yield 1/n**s

然后我们简单地获得第一个 N 个项的和,使用:

def zeta(N, s):
    # make sure that you do not use the scipy.sum here
    return sum(itertools.islice(generate_zeta(s), N))

我们指出,我们使用这种计算ζ(ζ)函数的方法,以优雅的方式演示了发电机的使用。这当然不是评估该函数的最准确和计算效率最高的方法。

压缩迭代器

我们在部分列表第 3 章容器类型中看到,可以通过将两个容器拉在一起来创建一个列表。迭代器也存在相同的操作:

xg = x_iterator()  # some iterator
yg = y_iterator()  # another iterator

for x, y in zip(xg, yg):
    print(x, y)

一旦其中一个迭代器用完,压缩迭代器就会停止。这与列表上的压缩操作的行为相同。

迭代器对象

正如我们前面提到的,一个for循环只需要一个可迭代的对象。尤其是列表是可重复的。这意味着列表能够从其内容创建迭代器。事实上,这对于任何对象(不仅仅是列表)都是正确的:任何对象都可能是可重复的。

这是通过__iter__方法实现的,该方法应该返回一个迭代器。这里我们举一个例子,其中__iter__方法是一个生成器:

class OdeStore:
    """
    Class to store results of ode computations
    """
    def __init__(self, data):
        "data is a list of the form [[t0, u0], [t1, u1],...]"
        self.data = data

    def __iter__(self):
        "By default, we iterate on the values u0, u1,..."
        for t, u in self.data:
            yield u

store = OdeStore([[0, 1], [0.1, 1.1], [0.2, 1.3]])
for u in store:
    print(u)
# result: 1, 1.1, 1.3
list(store) # [1, 1.1, 1.3]

如果您试图对不可迭代的对象使用迭代器的功能,将会引发异常:

>>> list(3)
TypeError: 'int' object is not iterable

在这个例子中,列表函数试图通过调用__iter__方法迭代对象 3 。但是这个方法不是为整数实现的,因此引发了异常。如果我们试图循环通过一个不可迭代的对象,也会发生同样的情况:

>>> for iteration in 3: pass
TypeError: 'int' object is not iterable

无限迭代

无限迭代或者通过无限迭代器,通过while循环,或者通过递归获得。显然,在实际情况下,某些条件会停止迭代。有限迭代的区别在于,通过粗略地检查代码,不可能说迭代是否会停止。

while 循环

while循环可用于重复一个代码块,直到满足一个条件:

while condition:
    <code>

一个while循环相当于以下代码:

for iteration in itertools.count():
    if not condition:
        break
    <code>

因此while循环相当于无限迭代器,如果满足某个条件,可能会停止。这种构造的危险是显而易见的:如果条件永远不满足,代码可能会陷入无限循环。

科学计算的问题在于,人们并不总是确定算法会收敛。例如,牛顿迭代可能根本不会收敛。如果该算法在一个while循环中实现,那么对于一些初始条件的选择,相应的代码将陷入无限循环。

因此,我们建议有限迭代器通常更适合这样的任务。以下结构通常有利地取代了while环的使用:

maxit = 100
for nb_iterations in range(max_it):
    ...
else:
    raise Exception("No convergence in {} iterations".format(maxit))

第一个优点是,无论发生什么,代码都保证在有限的时间内执行。第二个优点是变量nb_iterations包含算法收敛所需的迭代次数。

递归

当一个函数调用自己时,就会发生递归(参见第 7 章函数递归函数一节)。

在进行递归时,递归深度,也就是迭代的次数,将你的计算机带到了极限。我们在这里通过考虑一个简单的递归来演示这一点,它实际上根本不包含任何计算。它只给迭代赋值零:

def f(N):
    if N == 0: 
        return 0
    return f(N-1)

根据您的系统,该程序可能会阻塞 N ≥ 10000 (使用了太多内存)。结果是 Python 解释器崩溃了,没有进一步的异常。Python 提供了一种机制,可以在检测到过高的递归深度时引发异常。该最大递归深度可以通过执行以下操作来更改:

import sys 
sys.setrecursionlimit(1000)

递归极限的实际值可以通过sys.getrecursionlimit()得到。

但是请注意,选择过高的数字可能会危及代码的稳定性,因为 Python 可能会在达到最大深度之前崩溃。因此,保持递归极限不变通常是明智的。

相比之下,以下非递归程序运行千万次迭代没有任何问题:

for iteration in range(10000000):
    pass

我们主张,如果可能的话,应该在 Python 中避免递归。这显然只适用于有合适的替代迭代算法可用的情况。第一个原因是深度 N 的递归同时涉及 N 函数调用,这可能会导致很大的开销。第二个原因是它是一个无限迭代,也就是说,在递归结束之前,很难给出必要步骤数的上限。

请注意,在一些非常特殊的情况下(树遍历),递归是不可避免的。此外,在某些情况下(递归深度很小),递归程序由于可读性可能更好。

总结

在这一章中,我们研究了迭代器,这是一种非常接近迭代方法的数学描述的编程构造。你看到了yield关键字,遇到了有限和无限迭代器。

我们展示了迭代器可以被耗尽。更多的特殊方面,如迭代器理解和递归迭代器被引入,并通过例子进行了演示。

练习

Ex。1 →计算总和的值:

Exercises

Ex。2 →创建一个生成器,用于计算由以下关系定义的序列:

Exercises

Ex。3 →生成所有偶数。

Ex。4 →让Exercises。微积分中显示Exercises。通过实验确定最小数量 n ,使得Exercises。为此任务使用生成器。

Ex。5 →生成所有小于给定整数的素数。使用名为厄拉多塞筛的算法。

Ex。6 →应用显式欧拉方法求解微分方程Exercises得到递归:

Exercises

编写一个生成器,计算给定初始值 u 0 和给定时间步长值 h 的解数值 u n

Ex。7 →使用公式计算π:

Exercises

积分可以用复合梯形法则来近似,即通过以下公式:

Exercises

其中Exercises

为值 y i = f(x i ) 编程一个生成器,并通过对一个又一个项求和来评估公式。将您的结果与 SciPy 的quad功能进行比较。

Ex。8 →让 x = [1,2,3]和 y = [-1,-2,-3]。代码zip(*zip(x, y))有什么作用?解释它是如何工作的。

Ex。9 →完全椭圆积分可以通过函数scipy.special.ellipk计算。编写一个函数,计算 AGM 迭代所需的迭代次数,直到结果符合给定的公差(注意ellipk中的输入参数 m 对应于算术几何平均)一节定义中的 k 2

Ex。10 →考虑以下定义的顺序:

Exercises

它单调收敛到零:E1T10】E2T11。。。> 0 。通过分部积分,可以看出序列 E n 实现了以下递归:

Exercises

使用适当的生成器计算递归的前 20 项,并将结果与通过scipy.integrate.quad数值积分获得的结果进行比较。通过反转递归来执行相同的操作:

Exercises

使用exp函数评估指数函数。你观察到了什么?你有解释吗?(参考【29】)

Exercises

图 9.2:逼近 sin(x)的函数的收敛性研究

Ex。11 →正弦函数可由欧拉表示为

Exercises

编写一个生成函数值 P k (x) 的生成器。设置x=linspace(-1,3.5*pi,200) 并通过图形演示 P k (x) 对增加 k 来说有多好。上图(图 9.2 )显示了可能的结果(参考[【11,Th。5.2,第 65 页]](16.html "Appendix . References") )。*

十、错误处理

在本章中,我们将介绍错误、异常以及如何查找和修复它们。处理异常是编写可靠和可用代码的重要部分。我们将介绍基本的内置异常,并展示如何使用和处理异常。我们将介绍调试,并向您展示如何使用内置的 Python 调试器。

有哪些例外?

程序员(即使是有经验的程序员)发现的一个错误是代码有不正确的语法,这意味着代码指令的格式不正确。

考虑一个语法错误的例子:

>>> for i in range(10)
  File “<stdin>”, line 1
    for i in range(10)
                      ^
SyntaxError: invalid syntax

出现错误是因为for声明末尾缺少冒号。这是引发异常的一个示例。在SyntaxError的情况下,它告诉程序员代码有不正确的语法,并打印错误发生的行,箭头指向该行中的问题所在。

Python 中的异常是从名为Exception的基类派生(继承)而来的。Python 有许多内置的例外。一些常见的异常类型列于表 10.1、(内置异常的完整列表参见【38】)。

以下是两个常见的例外示例。如你所料,当你试图除以 0 时ZeroDivisionError被提升。

def f(x):
    return 1/x

>>> f(2.5)
0.4 
>>> f(0)

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "exception_tests.py", line 3, in f
    return 1/x
ZeroDivisionError: integer division or modulo by zero
| 例外 | **描述** | | `IndexError` | 索引越界,例如,当 v 只有 5 个元素时,`v[10]` | | `KeyError` | 对未定义的字典键的引用 | | `NameError` | 找不到名称,例如未定义的变量 | | `LinAlgError` | `linalg`模块中的错误,例如,当用奇异矩阵求解系统时 | | `ValueError` | 不兼容的数据值,例如,当使用不兼容数组的`dot`时 | | `IOError` | 输入/输出操作失败,例如,“找不到文件” | | `ImportError` | 导入时找不到模块或名称 |

表 10.1:一些常用的内置异常及其含义

零除会引发ZeroDivisionError并打印出发生错误的文件、行和函数名。

正如我们之前看到的,数组只能包含相同数据类型的元素。如果您试图分配一个不兼容类型的值,则会引发ValueError。值错误的示例:

>>> a = arange(8.0) 
>>> a 
array([ 0., 1., 2., 3., 4., 5., 6., 7.]) 
>>> a[3] = 'string'
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module>
ValueError: could not convert string to float: string

这里,引发ValueError是因为数组包含浮点,并且不能为元素分配字符串值。

基本原则

让我们看看如何使用异常的基本原则,用raise引发异常,用try语句捕捉异常。

引发异常

创建错误被称为引发异常。在前一节中,您看到了一些异常示例。您也可以定义自己的预定义类型或无类型的例外。使用如下命令可以引发异常:

raise Exception("Something went wrong")

当出现问题时,很容易打印出错误消息,例如:

print("The algorithm did not converge.")

出于多种原因,不建议这样做。首先,打印输出很容易被遗漏,尤其是当信息被隐藏在打印到控制台的许多其他信息中时。其次,更重要的是,它会使您的代码无法被其他代码使用。调用代码将无法知道发生了错误,因此无法处理它。

由于这些原因,最好还是引发异常。异常应始终包含描述性消息,例如:

raise Exception("The algorithm did not converge.")

这条信息对用户来说会很明显。它还为调用代码提供了知道发生了错误并可能找到补救方法的机会。

下面是一个典型的例子,检查函数内部的输入,以确保它在继续之前是可用的。例如,对负值和正确数据类型的简单检查确保了计算阶乘的函数的预期输入:

def factorial(n):
  if not (n >=0 and isinstance(n,(int,int32,int64))):
    raise ValueError("A positive integer is expected")
    ...

如果给出了不正确的输入,函数的用户将立即知道错误是什么,处理异常是用户的责任。请注意在引发预定义异常类型时异常名称的使用,在这种情况下ValueError后跟消息。通过指定异常的类型,调用代码可以根据引发的错误类型决定以不同的方式处理错误。

总而言之,引发异常总是比打印错误消息更好。

捕捉异常

处理异常被称为捕获异常。使用tryexcept命令检查异常。

异常会停止程序执行流程,并寻找最近的try封闭块。如果没有捕获到异常,程序单元被留下,它继续在调用栈中较高的程序单元中搜索下一个封闭的try块。如果没有找到任何块,并且没有处理异常,则执行完全停止;将显示标准追溯信息。

让我们看一个try语句的例子:

try:
    <some code that might raise an exception>
except ValueError:
    print("Oops, a ValueError occurred")

在这种情况下,如果try块中的代码引发了类型为ValueError的错误,将会捕获异常并打印except块中的消息。如果在try块内没有发生异常,except块被完全跳过,继续执行。

except语句可以捕捉多个异常。这是通过简单地将它们分组到一个元组中来完成的,如下所示:

except (RuntimeError, ValueError, IOError):

try块也可以有多个except语句。这使得根据类型以不同的方式处理异常成为可能。让我们看一个多种异常类型的例子:

try:
    f = open('data.txt', 'r')
    data = f.readline()
    value = float(data)
except OSError as oe:
    print("{}:  {}".format(oe.strerror, oe.filename))
except ValueError:
    print("Could not convert data to float.")

例如,如果文件不存在,这里将捕捉到一个OSError;例如,如果文件第一行中的数据与浮点数据类型不兼容,则会捕获一个ValueError

在本例中,我们通过关键字asOSError分配给变量oe。这允许在处理该异常时访问更多细节。这里我们打印了错误字符串oe.strerror和相关文件的名称oe.filename。根据类型的不同,每种错误类型都可以有自己的一组变量。如果文件不存在,在前面的示例中,消息将是:

I/O error(2): No such file or directory

另一方面,如果文件存在,但您没有打开它的权限,消息将是:

I/O error(13): Permission denied

这是捕获异常时格式化输出的有用方法。

try - except组合可以通过可选的elsefinally模块进行扩展。使用else的例子可以在第 13 章测试测试等分算法部分看到。当最终需要进行清理工作时,将tryfinally结合起来会给出一个有用的结构:

确保文件正确关闭的示例:

try:
    f = open('data.txt', 'r')
    # some function that does something with the file
    process_file_data(f) 
except: 
    ... 
finally:
    f.close()

这将确保无论在处理文件数据时引发什么异常,文件最终都是关闭的。在try语句中没有处理的异常在finally块之后被保存和引发。这个组合用在with语句中;请参见上下文管理器-附带声明一节。

用户定义的异常

除了内置的 Python 异常,还可以定义自己的异常。这种用户定义的异常应该继承自Exception基类。当您定义自己的类时,如第 14 章综合示例多项式一节中的多项式类,这可能会很有用。

看看这个简单的用户定义异常的小例子:

class MyError(Exception):
    def __init__(self, expr):
        self.expr = expr
    def __str__(self):
        return str(self.expr)

try:
   x = random.rand()
   if x < 0.5:
      raise MyError(x)
except MyError as e:
   print("Random number too small", e.expr)
else:
   print(x)

生成一个随机数。如果该数字低于 0.5,将引发异常,并打印一条消息,指出该值太小。如果没有引发异常,则打印该数字。

在本例中,您还看到了在try语句中使用else的情况。如果没有异常发生,else下的区块将被执行。

建议您使用以Error结尾的名称定义异常,如标准内置异常的命名。

上下文管理器 with 语句

Python 中有一个非常有用的构造,用于在处理上下文(如文件或数据库)时简化异常处理。该语句将try ... finally结构封装在一个简单的命令中。下面是一个使用with读取文件的例子:

with open('data.txt', 'r') as f:
    process_file_data(f)

这将尝试打开文件,对文件运行指定的操作(例如,读取),并关闭文件。如果在执行process_file_data的过程中出现任何问题,文件会被正确关闭,然后引发异常。这相当于:

f = open('data.txt', 'r')
try: 
    # some function that does something with the file 
    process_file_data(f) 
except:
    ... 
finally:
    f.close()

我们将在第 12 章输入输出文件处理一节中,在读写文件时使用这个选项。

前面的文件读取示例是使用上下文管理器的示例。上下文管理器是 Python 对象,有两种特殊的方法,_ _enter_ __ _exit_ _。实现这两种方法的类的任何对象都可以用作上下文管理器。在这个例子中,文件对象f是一个上下文管理器,因为有f._ _enter_ _f._ _exit_ _方法。

_ _enter_ _方法应该实现初始化指令,比如打开文件或者数据库连接。如果此方法有一个返回语句,则使用as构造访问返回的对象。否则省略as关键字。_ _exit_ _方法包含清理指令,例如,关闭文件或提交事务并关闭数据库连接。如需了解更多解释和自写上下文管理器示例,请参阅第 13 章、测试使用上下文管理器计时一节。

有一些 NumPy 函数可以用作上下文管理器。例如load功能支持某些文件格式的上下文管理器。NumPy 的函数errstate可以用作上下文管理器,指定代码块内的浮点错误处理行为。

下面是使用 errstate 和上下文管理器的一个示例:

import numpy as np      # note, sqrt in NumPy and SciPy 
                        # behave differently in that example
with errstate(invalid='ignore'):
    print(np.sqrt(-1)) # prints 'nan'

with errstate(invalid='warn'):
    print(np.sqrt(-1)) # prints 'nan' and 
                   # 'RuntimeWarning: invalid value encountered in sqrt'

with errstate(invalid='raise'):
    print(np.sqrt(-1)) # prints nothing and raises FloatingPointError

参考第 2 章变量和基本类型无限和非数字部分,了解本例的更多细节,参考第 13 章测试部分,了解另一个示例。

查找错误:调试

软件代码中的错误有时被称为bug。调试是发现和修复代码中的错误的过程。这个过程可以在不同的复杂程度下进行。最有效的方法是使用一种叫做调试器的工具。进行单元测试是早期识别错误的好方法,请参考第 13 章、测试使用单元测试一节。当不清楚问题在哪里或者是什么的时候,调试器是非常有用的。

虫子

通常有两种错误:

  • 会引发异常,但不会被捕获。
  • 代码运行不正常。

第一种情况通常更容易解决。第二个可能更难,因为问题可能是一个错误的想法或解决方案,一个错误的实现,或者两者的结合。

我们只关心接下来的第一种情况,但是同样的工具可以用来帮助找到为什么代码没有做它应该做的事情。

堆栈

当引发异常时,您会看到调用堆栈。调用堆栈包含调用引发异常的代码的所有函数的跟踪。

一个简单的堆栈示例:

def f():
   g()
def g():
   h()
def h():
   1//0

f()

这种情况下的堆栈是fgh。运行这段代码生成的输出如下所示:

Traceback (most recent call last):
  File "stack_example.py", line 11, in <module>
    f() 
  File "stack_example.py", line 3, in f
    g() 
  File "stack_example.py", line 6, in g
    h() File "stack_example.py", line 9, in h
    1//0 
ZeroDivisionError: integer division or modulo by zero

打印错误。显示了导致错误的函数序列。第 11 行的函数f被调用,依次调用gh。这就造成了ZeroDivisionError

堆栈跟踪报告程序执行中某一点的活动堆栈。堆栈跟踪允许您跟踪调用到给定点的函数序列。通常这是在引发了未捕获的异常之后。这有时被称为事后分析,堆栈跟踪点就是异常发生的地方。另一种选择是手动调用堆栈跟踪来分析您怀疑有错误的代码段,可能是在异常发生之前。

Python 调试器

Python 自带一个名为 pdb 的内置调试器。一些开发环境集成了调试器。以下过程在大多数情况下仍然适用。

使用调试器最简单的方法是在您想要调查的代码点启用堆栈跟踪。下面是基于第七章函数返回值一节中提到的例子触发调试器的一个简单例子:

import pdb

def complex_to_polar(z):
    pdb.set_trace() 
    r = sqrt(z.real ** 2 + z.imag ** 2)
    phi = arctan2(z.imag, z.real)
    return (r,phi)
z = 3 + 5j 
r,phi = complex_to_polar(z)

print(r,phi)

pdb.set_trace()命令启动调试器并启用后续命令的跟踪。前面的代码将显示这一点:

> debugging_example.py(7)complex_to_polar()
-> r = sqrt(z.real ** 2 + z.imag ** 2) 
(Pdb)

调试器提示用(Pdb)表示。调试器停止程序执行,并给你一个提示,让你检查变量,修改变量,单步执行命令,等等。

每一步都会打印当前行,因此您可以跟踪自己的位置以及接下来会发生什么。通过命令n(下一步)来执行命令,如下所示:

> debugging_example.py(7)complex_to_polar() 
-> r = sqrt(z.real ** 2 + z.imag ** 2) 
(Pdb) n 
> debugging_example.py(8)complex_to_polar() 
-> phi = arctan2(z.imag, z.real) 
(Pdb) n 
> debugging_example.py(9)complex_to_polar() 
-> return (r,phi) 
(Pdb) 
...

命令n(下一步)将继续到下一行并打印该行。如果需要同时查看多行,列表命令l(列表)显示当前行及周围代码:

在调试器中列出周围的代码:

> debugging_example.py(7)complex_to_polar() 
-> r = sqrt(z.real ** 2 + z.imag ** 2) 
(Pdb) l
  2
  3 import pdb
  4
  5 def complex_to_polar(z):
  6 pdb.set_trace()
  7 -> r = sqrt(z.real ** 2 + z.imag ** 2)
  8 phi = arctan2(z.imag, z.real)
  9 return (r,phi)
 10
 11 z = 3 + 5j
 12 r,phi = complex_to_polar(z) 
(Pdb)

变量的检查可以通过使用命令p(打印)后跟变量名将其值打印到控制台来完成。打印变量的示例:

> debugging_example.py(7)complex_to_polar() 
-> r = sqrt(z.real ** 2 + z.imag ** 2) 
(Pdb) p z 
(3+5j) (Pdb) n 
> debugging_example.py(8)complex_to_polar() 
-> phi = arctan2(z.imag, z.real) 
(Pdb) p r 
5.8309518948453007 
(Pdb) c 
(5.8309518948453007, 1.0303768265243125)

p(打印)命令将打印变量;命令c(继续)继续执行。

在执行过程中更改变量是有用的。只需在调试器提示符下分配新值,然后单步执行或继续执行。

一个改变变量的例子:

> debugging_example.py(7)complex_to_polar() 
-> r = sqrt(z.real ** 2 + z.imag ** 2) 
(Pdb) z = 2j 
(Pdb) z 
2j 
(Pdb) c 
(2.0, 1.5707963267948966)

这里,变量z被赋予一个新值,在剩余代码中使用。请注意,最终打印结果已经更改。

概述-调试命令

表 10.2 中,显示了最常见的调试命令。有关命令的完整列表和描述,请参见文档【25】了解更多信息。请注意,任何 Python 命令也可以工作,例如,为变量赋值。

型式

短变量名

如果要检查名称与调试器的任何短命令一致的变量,例如h,必须使用!h显示该变量。

| **命令** | **动作** | | `h` | 帮助(没有参数,它打印可用的命令) | | `l` | 列出当前行周围的代码 | | `q` | 退出(退出调试器,执行停止) | | `c` | 继续执行 | | `r` | 继续执行,直到当前函数返回 | | `n` | 继续执行,直到下一行 | | `p ` | 计算并打印当前上下文中的表达式 |

表 10.2:调试器最常见的调试命令。

IPython 中的调试

IPython 附带了名为ipdb的调试器版本。在撰写本书时,差异非常小,但这可能会改变。

IPython 中有一个命令,在出现异常时自动打开调试器。这在尝试新想法或代码时非常有用。如何在 IPython 中自动打开调试器的示例:

In [1]: %pdb # this is a so - called IPython magic command 
Automatic pdb calling has been turned ON

In [2]: a = 10

In [3]: b = 0

In [4]: c = a/b
___________________________________________________________________
ZeroDivisionError                  Traceback (most recent call last) 
<ipython-input-4-72278c42f391> in <module>() 
—-> 1 c = a/b

ZeroDivisionError: integer division or modulo by zero 
> <ipython-input-4-72278c42f391>(1)<module>()
      -1 c = a/b
ipdb>

IPython 提示符下的 IPython 魔法命令%pdb在引发异常时自动启用调试器。这里调试器提示显示ipdb来指示调试器正在运行。

总结

本章中的关键概念是异常和错误。我们展示了一个异常是如何在另一个程序单元中被捕获的。您可以定义自己的异常,并为它们配备消息和给定变量的当前值。

代码可能会返回意外结果,但不会引发异常。定位错误结果来源的技术称为调试。我们介绍了调试方法,并希望鼓励您对它们进行培训,以便在需要时随时可用。认真调试的需求来得比你预期的要早。

十一、名称空间、范围和模块

在本章中,我们将介绍 Python 模块。模块是包含函数和类定义的文件。本章还解释了命名空间的概念以及跨函数和模块的变量范围。

命名空间

Python 对象的名称,如变量、类、函数和模块的名称,都收集在名称空间中。模块和类有自己的命名空间,与这些对象同名。这些命名空间是在导入模块或实例化类时创建的。模块命名空间的生存期与当前 Python 会话一样长。类实例的命名空间的生存期到该实例被删除为止。

函数在执行(调用)时会创建一个本地命名空间。当函数通过常规返回或异常停止执行时,它被删除。本地命名空间未命名。

名称空间的概念将变量名放在其上下文中。例如,有几个名为sin的函数,它们通过它们所属的命名空间来区分,如下面的代码所示:

import math
import scipy
math.sin
scipy.sin

它们确实不同,因为scipy.sin是一个接受列表或数组作为输入的通用函数,其中math.sin只接受浮点数。命令dir(<name of the namespace>)可以获得一个包含特定名称空间中所有名称的列表。它包含两个特殊的名字__name____doc__。前者指的是模块的名称,后者指的是它的文档字符串:

math.__name__ # returns math
math.__doc__ # returns 'This module is always ...'

有一个特殊的命名空间__builtin__,它包含 Python 中可用的名称,没有任何import。它是一个命名空间,但是在引用内置对象时不需要给出它的名称:

'float' in dir(__builtin__) # returns True
float is __builtin__.float # returns True

变量的范围

在程序的一个部分定义的变量不需要在其他部分知道。已知某个变量的所有程序单元称为该变量的作用域。我们先举个例子;让我们考虑两个嵌套函数:

e = 3
def my_function(in1):
    a = 2 * e
    b = 3
    in1 = 5
    def other_function():
       c = a
       d = e
       return dir()
    print("""
          my_function's namespace: {} 
          other_function's namespace: {}
          """.format(dir(),other_function()))
    return a

执行my_function(3)会导致:

my_function's namespace: ['a', 'b', 'in1', 'other_function'] 
other_function's namespace: ['a', 'c', 'd']

变量e位于包含函数my_function 的程序单元的名称空间中。变量a在这个函数的名称空间中,它本身包含了最里面的函数other_function。对于这两个函数,e是一个全局变量。

一个好的做法是只通过参数列表向函数传递信息,而不使用前面例子中的构造。在第七章函数匿名函数一节中可以找到一个例外,其中全局变量用于闭包。通过给变量赋值,变量会自动变成局部变量:

e = 3
def my_function():
    e = 4
    a = 2
    print("my_function's namespace: {}".format(dir()))

执行

e = 3
my_function()
e # has the value 3

给出:

my_function's namespace: ['a', 'e']

其中e变成了局部变量。事实上,这段代码现在有两个属于不同名称空间的变量e

通过使用global声明语句,函数中定义的变量可以成为全局变量,也就是说,它的值甚至可以在该函数之外访问。global声明的使用演示如下:

def fun():
    def fun1():
        global a
        a = 3
    def fun2():
        global b
        b = 2
        print(a)
    fun1()
    fun2() # prints a
    print(b)

型式

避免使用全局

建议避免使用这种构造和global的使用。这类代码很难调试和维护。类的使用(详见第八章,使得global主要过时。

模块

在 Python 中,模块只是一个包含类和函数的文件。通过在会话或脚本中导入文件,函数和类变得可用。

简介

默认情况下,Python 附带许多不同的库。您可能还想为特定目的安装更多这样的软件,例如优化、绘图、读/写文件格式、图像处理等。NumPy 和 SciPy 是这种库的两个重要例子,matplotlib 用于绘图是另一个例子。在本章的最后,我们将列出一些有用的库。

要使用库,您可以:

  • 仅从库中加载某些对象,例如从 NumPy:

            from numpy import array, vander
    
  • 或者加载整个库:

            from numpy import *
    
  • Or give access to an entire library by creating a namespace with the library name:

            import numpy
            ...
            numpy.array(...)
    

    在库中的函数前面加上名称空间,就可以访问该函数,并将该函数与其他同名对象区分开来。

此外,命名空间的名称可以与import命令一起指定:

import numpy as np
...
np.array(...)

您使用的选项会影响代码的可读性以及出错的可能性。一个常见的错误是阴影:

from scipy.linalg import eig
A = array([[1,2],[3,4]])
(eig, eigvec) = eig(A)
...
(c, d) = eig(B) # raises an error

避免这种意外影响的方法是使用import:

import scipy.linalg as sl
A = array([[1,2],[3,4]])
(eig, eigvec) = sl.eig(A) # eig and sl.eig are different objects
...
(c, d) = sl.eig(B)

在本书中,我们使用了许多命令、对象和函数。这些是通过以下语句导入本地命名空间的:

from scipy import *

以这种方式导入对象不会使从中导入对象的模块变得明显。下表给出了一些例子(表 11.1 ):

| **图书馆** | **方法** | | `numpy` | `array`、`arange`、`linspace`、`vstack`、`hstack`、`dot`、`eye`、`identity`和`zeros`。 | | `numpy.linalg` | `solve`、`lstsq`、`eig`和`det`。 | | `matplotlib.pyplot` | `plot`、`legend`和`cla`。 | | `scipy.integrate` | `quad`。 | | `copy` | `copy`和`deepcopy`。 |

表 11.1:导入对象的示例

IPython 中的模块

IPython 在代码开发下使用。一个典型的场景是,您处理一个文件,其中包含一些您在开发周期中更改的函数或类定义。要将此类文件的内容加载到 shell 中,可以使用import,但文件只加载一次。更改文件对以后的导入没有影响。这就是 IPyhthon 的魔法指令run登场的地方。

IPython 魔法命令

IPython 有一个名为run的特殊魔法命令,它会执行一个文件,就像你在 Python 中直接运行它一样。这意味着文件的执行独立于 IPython 中已经定义的内容。当您想要测试作为独立程序的脚本时,这是从 IPython 中执行文件的推荐方法。您必须以与从命令行执行相同的方式在执行的文件中导入所有需要的内容。在myfile.py中运行代码的一个典型例子是:

from numpy import array
...
a = array(...)

该脚本文件由exec(open('myfile.py').read())在 Python 中执行。或者,在 IPython 中,如果您想确保脚本独立于之前的导入运行,可以使用魔法命令run myfile。文件中定义的所有内容都被导入到 IPython 工作区。

变量 name

在任何模块中,特殊变量__name__被定义为当前模块的名称。在命令行中(在 IPython 中),该变量被设置为__main__,这允许以下技巧:

# module
import ...

class ...

if __name__ == "__main__":
   # perform some tests here

测试将仅在文件直接运行时运行,而不是在文件导入时运行。

一些有用的模块

有用的 Python 模块有很多。在下表中,我们给出了一个非常简短的列表,重点是与数学和工程应用相关的模块(表 11.2) :

| **模块** | **描述** | | `scipy` | 科学计算中使用的函数 | | `numpy` | 支持数组和相关方法 | | `matplotlib` | 使用导入子模块 pyplot 进行绘图和可视化 | | `functools` | 函数的部分应用 | | `itertools` | 迭代器工具提供特殊的功能,比如对生成器进行切片 | | `re` | 高级字符串处理的正则表达式 | | `sys` | 系统特定功能 | | `os` | 操作系统界面,如目录列表和文件处理 | | `datetime` | 表示日期和日期增量 | | `time` | 返回挂钟时间 | | `timeit` | 测量执行时间 | | `sympy` | 计算机算术包(符号计算) | | `pickle` | 酸洗,特殊文件输入和输出格式 | | `shelves` | 架子,特殊文件输入和输出格式 | | `contextlib` | 上下文管理器工具 |

表 11.2:工程应用中有用的 Python 包的非穷举列表

总结

我们在书的开头告诉你,你必须导入 SciPy 和其他有用的模块。现在你完全明白进口意味着什么。我们介绍了名称空间,并讨论了importfrom ... import *之间的区别。变量的范围已经在早期的第 7 章函数中介绍过了,但是现在你对这个概念的重要性有了更完整的了解。

十二、输入和输出

在本章中,我们将介绍一些处理数据文件的选项。根据数据和所需的格式,有几种读写选项。我们将展示一些最有用的替代方案。

文件处理

在许多情况下,文件输入/输出(输入和输出)是必不可少的。例如:

  • 使用测量或扫描的数据。测量值存储在需要读取以进行分析的文件中。
  • 与其他程序交互。将结果保存到文件中,以便在其他应用中导入,反之亦然。
  • 存储信息供将来参考或比较。
  • 与他人共享数据和结果,可能在其他平台上使用其他软件。

在本节中,我们将介绍如何用 Python 处理文件输入/输出。

与文件交互

在 Python 中,file类型的对象表示存储在磁盘上的物理文件的内容。可以使用以下语法创建新的file对象:

myfile = open('measurement.dat','r') # creating a new file object from an existing file

例如,可以通过以下方式访问文件的内容:

print(myfile.read())

文件对象的使用需要小心。问题是文件必须先关闭,然后才能被其他应用重新读取或使用,这是使用以下语法完成的:

myfile.close() # closes the file object

然而,事情并没有那么简单,因为在执行对close的调用之前可能会触发异常,这将跳过结束代码(考虑以下示例)。确保文件被正确关闭的一个简单方法是使用上下文管理器。在第 10 章错误处理例外一节中,使用with关键字对该结构进行了更详细的解释。以下是它如何用于文件:

with open('measurement.dat','r') as myfile: 
     ... # use myfile here

这确保了当一个人退出with块时文件是关闭的,即使在块内部引发了异常。该命令适用于上下文管理器对象。我们建议您在第 10 章错误处理例外部分阅读更多关于上下文管理器的内容。这里有一个例子说明了为什么with结构是可取的:

myfile = open(name,'w')
myfile.write('some data')
a = 1/0
myfile.write('other data')
myfile.close()

文件关闭前会引发异常。文件保持打开状态,并且不能保证文件中写入了什么数据或何时写入。因此,实现相同结果的正确方法是:

with open(name,'w') as myfile:
    myfile.write('some data')
    a = 1/0
    myfile.write('other data')

在这种情况下,文件会在引发异常(这里是ZeroDivisionError)后干净地关闭。还要注意,没有必要显式关闭文件。

文件是可重复的

特别是,文件是可迭代的(参见第 9 章迭代迭代器部分)。文件重复它们的行:

with open(name,'r') as myfile:
    for line in myfile:
        data = line.split(';')
        print('time {} sec temperature {} C'.format(data[0],data[1]))

文件的行作为字符串返回。字符串方法split是将字符串转换为字符串列表的可能工具。例如:

data = 'aa;bb;cc;dd;ee;ff;gg'
data.split(';') # ['aa', 'bb', 'cc', 'dd', 'ee', 'ff', 'gg']

data = 'aa bb cc dd ee ff gg'
data.split(' ') # ['aa', 'bb', 'cc', 'dd', 'ee', 'ff', 'gg']

由于myfile对象是可迭代的,我们也可以直接提取到列表中,如下所示:

data = list(myfile)

文件模式

从这些文件处理的例子中可以看出,open函数至少有两个参数。第一个显然是文件名,第二个是描述文件使用方式的字符串。打开文件有几种这样的模式;基本的有:

with open('file1.dat','r') as ...  # read only
with open('file2.dat','r+') as ...  # read/write
with open('file3.dat','rb') as ...  # read in byte mode  
with open('file4.dat','a') as ...  # append (write to the end of the file)
with open('file5.dat','w') as ... # (over-)write the file
with open('file6.dat','wb') as ... # (over-)write the file in byte mode

'r''r+''a'模式要求文件存在,而'w'如果不存在同名文件,将创建一个新文件。用'r''w'阅读和写作是最常见的,正如你在前面的例子中看到的。

考虑一个例子,使用追加'a'模式打开一个文件并在文件末尾添加数据,而不修改已经存在的内容。注意断线,\n:

with open('file3.dat','a') as myfile:
    myfile.write('something new\n')

NumPy 方法

NumPy 内置了将 NumPy 数组数据读写到文本文件的方法。这些是numpy.loadtxtnumpy.savetxt

savetxt

将数组写入文本文件很简单:

savetxt(filename,data)

有两个有用的参数作为字符串给出,fmtdelimiter,它们控制格式和列之间的分隔符。默认值是分隔符为空格,格式为%.18e,对应于所有数字的指数格式。格式参数的使用如下:

x = range(100) # 100 integers
savetxt('test.txt',x,delimiter=',')   # use comma instead of space
savetxt('test.txt',x,fmt='%d') # integer format instead of float with e

loadtxt

从文本文件读取数组是在以下语法的帮助下完成的:

filename = 'test.txt'
data = loadtxt(filename)

由于数组中的每一行都必须具有相同的长度,因此文本文件中的每一行都必须具有相同数量的元素。与savetxt类似,默认值为float,分隔符为space。可以使用dtypedelimiter参数进行设置。另一个有用的参数是comments,可以用来标记数据文件中的注释使用了什么符号。使用格式化参数的示例如下:

data = loadtxt('test.txt',delimiter=';')    # data separated by semicolons
data = loadtxt('test.txt',dtype=int,comments='#') # read to integer type, 
                                               #comments in file begin with a hash character

腌制

您刚才看到的读写方法在写入之前将数据转换为字符串。复杂类型(如对象和类)不能用这种方式编写。使用 Python 的 pickle 模块,您可以将任何对象以及多个对象保存到文件中。

数据可以以明文(ASCII)格式保存,也可以使用稍微高效的二进制格式保存。主要有两种方法:dump,将 Python 对象的腌制表示保存到文件中;以及load,从文件中检索腌制对象。基本用法是这样的:

import pickle
with open('file.dat','wb') as myfile:
    a = random.rand(20,20)
    b = 'hello world'
    pickle.dump(a,myfile)    # first call: first object
    pickle.dump(b,myfile)    # second call: second object

import pickle
with open('file.dat','rb') as myfile:
    numbers = pickle.load(myfile) # restores the array
    text = pickle.load(myfile)    # restores the string

请注意这两个对象的返回顺序。除了两种主要方法之外,将 Python 对象序列化为字符串而不是文件有时也很有用。这是通过dumpsload完成的。考虑一个序列化数组和字典的示例:

a = [1,2,3,4]
pickle.dumps(a) # returns a bytes object
b = {'a':1,'b':2}
pickle.dumps(b) # returns a bytes object

使用dumps的一个很好的例子是当你需要将 Python 对象或 NumPy 数组写入数据库时。这些通常支持存储字符串,这使得无需任何特殊模块就可以轻松地写入和读取复杂的数据和对象。除了 pickle 模块,还有一个优化版叫做cPickle。它是用 C 写的,如果你需要快速阅读和写作,它是一个选项。pickle 和 cPickle 产生的数据相同,可以互换。

货架

字典中的对象可以通过键来访问。访问文件中的特定数据也有类似的方法,首先为其分配一个密钥。这可以通过使用模块架来实现:

from contextlib import closing
import shelve as sv
# opens a data file (creates it before if necessary)
with closing(sv.open('datafile')) as data:
    A = array([[1,2,3],[4,5,6]])     
    data['my_matrix'] = A  # here we created a key

文件处理一节中,我们看到内置的open命令生成了一个上下文管理器,我们看到了为什么这对处理外部资源(如文件)很重要。与此命令相反,sv.open本身并不创建上下文管理器。需要来自contextlib模块的closing命令将其转换为合适的上下文管理器。考虑以下恢复文件的示例:

from contextlib import closing
import shelve as sv
with closing(sv.open('datafile')) as data: # opens a data file
    A = data['my_matrix']  # here we used the key
    ...

搁置对象具有所有字典方法,例如键和值,并且可以像字典一样使用。请注意,只有在调用了closesync方法后,更改才会写入文件。

读写 Matlab 数据文件

SciPy 能够使用该模块以 Matlab 的.mat文件格式读写数据。命令是loadmatsavemat。要加载数据,请使用以下语法:

import scipy.io
data = scipy.io.loadmat('datafile.mat')

变量数据现在包含一个字典,键对应于保存在.mat文件中的变量名。变量是 NumPy 数组格式。保存到.mat文件包括创建一个包含所有要保存的变量(变量名和值)的字典。接下来的命令是savemat:

data = {}
data['x'] = x
data['y'] = y
scipy.io.savemat('datafile.mat',data)

这将在读入 Matlab 时保存名称相同的 NumPy 数组xy

读写图像

SciPy 附带了一些处理图像的基本功能。模块功能将图像读取到 NumPy 数组。该函数将数组保存为图像。下面将把一个 JPEG 图像读入一个数组,打印形状和类型,然后用一个调整大小的图像创建一个新的数组,并将新的图像写入文件:

import scipy.misc as sm

# read image to array
im = sm.imread("test.jpg") 
print(im.shape)   # (128, 128, 3)
print(im.dtype)   # uint8

# resize image
im_small = sm.imresize(im, (64,64))
print(im_small.shape)   # (64, 64, 3)

# write result to new image file
sm.imsave("test_small.jpg", im_small)

请注意数据类型。图像几乎总是以范围 0 的像素值存储...255 作为 8 位无符号整数。第三个形状值显示图像有多少颜色通道。在这种情况下, 3 表示它是一个彩色图像,其值按以下顺序存储:红色im[0]、绿色im[1]、蓝色im[2]。灰度图像只有一个通道。

为了处理图像,SciPy 模块scipy.misc包含许多有用的基本图像处理功能,如滤波、变换和测量。

总结

当处理大量数据的测量和其他来源时,文件处理是不可避免的。与其他程序和工具的通信也是通过文件处理来完成的。

你学会了像其他人一样用readlineswrite等重要方法将文件视为 Python 对象。我们展示了如何通过特殊属性保护文件,这些属性可能只允许读或写访问。

写入文件的方式通常会影响进程的速度。我们看到了如何通过酸洗或使用shelve方法存储数据。

十三、测试

在这一章中,我们将集中讨论科学编程测试的两个方面。第一个方面是在科学计算中测试什么这个经常很难的话题。第二个方面涉及如何测试的问题。我们将区分手动测试和自动测试。手动测试是每个程序员为了快速检查一个实现是否工作而做的事情。自动化测试是这个想法的精炼的、自动化的变体。我们将介绍一些可用于一般自动测试的工具,着眼于科学计算的特殊情况。

手动测试

在代码开发过程中,为了测试它的功能,您会做很多小测试。这可以称为手动测试。通常,您会通过在交互环境中手动测试函数来测试给定的函数是否完成了它应该做的事情。例如,假设您实现了等分算法。这是一种寻找标量非线性函数的零点(根)的算法。要启动算法,必须给定一个区间,其属性是函数在区间边界上取不同的符号,更多信息请参见练习 4第 7 章函数

然后,您将测试该算法的实现,通常是通过检查:

  • 当函数在区间边界具有相反符号时,找到了一个解
  • 当函数在区间边界具有相同的符号时,会引发异常

尽管看起来很有必要,但手动测试并不令人满意。一旦你确信代码做了它应该做的事情,你就可以用相对较少的演示例子来说服其他人相信代码的质量。在那个阶段,人们经常对开发过程中进行的测试失去兴趣,它们被遗忘甚至删除。一旦你改变了一个细节,事情就不再正常工作,你可能会后悔你以前的测试不再可用。

自动测试

开发任何代码的正确方法是使用自动测试。优点是:

  • 在每次代码重构之后和任何新版本发布之前,大量测试的自动重复。
  • 代码使用的静默文档。
  • 代码测试覆盖范围的文档:事情是在变更前工作还是某个方面从未测试过?

程序中的变化,特别是不影响其功能的结构变化,叫做代码重构。

我们建议与代码并行开发测试。好的测试设计本身就是一门艺术,很少有投资能像好的测试投资那样保证开发时间节约的良好回报。

现在,我们将考虑自动化测试方法来实现一个简单的算法。

测试等分算法

让我们检查一下等分算法的自动化测试。利用该算法,可以找到实值函数的零点。在第七章功能练习 4 一节有描述。算法的实现可以具有以下形式:

def bisect(f, a, b, tol=1.e-8):
    """
    Implementation of the bisection algorithm 
    f real valued function
    a,b interval boundaries (float) with the property 
    f(a) * f(b) <= 0
    tol tolerance (float)
    """
    if f(a) * f(b)> 0:
        raise ValueError("Incorrect initial interval [a, b]") 
    for i in range(100):
        c = (a + b) / 2.
        if f(a) * f(c) <= 0:
            b = c
        else:
            a = c
        if abs(a - b) < tol:
            return (a + b) / 2
    raise Exception(
          'No root found within the given tolerance {}'.format(tol))

我们假设这存储在bisection.py文件中。作为第一个测试用例,我们测试发现函数 f ( x ) = x 的零点为:

def test_identity():
    result = bisect(lambda x: x, -1., 1.) 
    expected = 0.
    assert allclose(result, expected),'expected zero not found'

test_identity()

在这段代码中,你第一次遇到 Python 关键字assert。如果其第一个参数返回False值,则引发AssertionError异常。它可选的第二个参数是一个包含附加信息的字符串。我们使用函数allclose来测试浮点数是否相等。

让我们评论一下测试函数的一些特性。我们使用断言来确保如果代码的行为不符合预期,将会引发异常。我们必须在test_identity()线手动运行测试。

有许多工具可以自动化这种调用。

现在让我们设置一个测试,当函数在区间两端具有相同的符号时,该测试检查bisect是否引发异常。现在,我们假设引发的异常是ValueError异常。在以下示例中,我们将检查初始间隔[ ab ]。对于等分算法,它应该满足一个符号条件:

def test_badinput():
    try:
        bisect(lambda x: x,0.5,1)
    except ValueError:
        pass
    else:
        raise AssertionError()

test_badinput()

在这种情况下,如果异常不是ValueError类型,则会引发AssertionError。有一些工具可以简化前面的构造,以检查是否引发了异常。

另一个有用的测试是边缘案例测试。在这里,我们测试参数或用户输入,这可能会产生程序员无法预见的数学上未定义的情况或程序状态。例如,如果两个界限相等会发生什么?如果 a > b 会发生什么?

def test_equal_boundaries():
    result = bisect(lambda x: x, 0., 0.)
    expected = 0.
    assert allclose(result, expected), \
                   'test equal interval bounds failed'

def test_reverse_boundaries():
    result = bisect(lambda x: x, 1., -1.)
    expected = 0.
    assert allclose(result, expected),\
                 'test reverse interval bounds failed'

test_equal_boundaries()
test_reverse_boundaries()

使用单元测试包

标准的unittest Python 包极大地方便了自动化测试。这个包要求我们重写测试以兼容。第一个测试必须在class中重写,如下所示:

from bisection import bisect
import unittest

class TestIdentity(unittest.TestCase):
    def test(self):
        result = bisect(lambda x: x, -1.2, 1.,tol=1.e-8)
        expected = 0.
        self.assertAlmostEqual(result, expected)

if __name__=='__main__':
    unittest.main()

让我们检查一下与之前实现的区别。首先,测试现在是一个方法和一个类的一部分。该类必须从unittest.TestCase继承。测试方法的名称必须以test开头。注意,我们现在可以使用unittest包的断言工具之一,即assertAlmostEqual。最后,使用unittest.main运行测试。我们建议将测试写在与要测试的代码分开的文件中。这就是为什么它以import开头。测试通过并返回如下:

Ran 1 test in 0.002s
OK

如果我们使用宽松的公差参数运行它,例如1.e-3,将会报告测试失败:

F
======================================================================
FAIL: test (__main__.TestIdentity)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-11-e44778304d6f>", line 5, in test
    self.assertAlmostEqual(result, expected)
AssertionError: 0.00017089843750002018 != 0.0 within 7 places
----------------------------------------------------------------------
Ran 1 test in 0.004s
FAILED (failures=1)

测试可以也应该作为测试类的方法组合在一起,如下例所示:

import unittest
from bisection import bisect

class TestIdentity(unittest.TestCase):
    def identity_fcn(self,x):
        return x
    def test_functionality(self):
        result = bisect(self.identity_fcn, -1.2, 1.,tol=1.e-8)
        expected = 0.
        self.assertAlmostEqual(result, expected)
    def test_reverse_boundaries(self):
        result = bisect(self.identity_fcn, 1., -1.)
        expected = 0.
        self.assertAlmostEqual(result, expected)
    def test_exceeded_tolerance(self):
        tol=1.e-80
        self.assertRaises(Exception, bisect, self.identity_fcn,
                                               -1.2, 1.,tol)
if __name__=='__main__':
    unittest.main()

这里,在上一次测试中,我们使用了unittest.TestCase.assertRaises方法。它测试异常是否被正确引发。它的第一个参数是异常类型,例如,ValueErrorException,它的第二个参数是函数的名称,预计会引发异常。剩下的参数是这个函数的参数。命令unittest.main()创建一个TestIdentity类的实例,并从test开始执行这些方法。

测试设置和拆卸方法

unittest.TestCase提供了两个特殊的方法,setUptearDown,它们在每次调用测试方法之前和之后运行。测试发电机时需要这样做,每次测试后发电机都会耗尽。我们通过测试一个程序来演示这一点,该程序检查给定字符串首次出现的文件中的行:

class NotFoundError(Exception):
  pass

def find_string(file, string):
    for i,lines in enumerate(file.readlines()):
        if string in lines:
            return i
    raise NotFoundError(
         'String {} not found in File {}'.format(string,file.name))

我们假设该代码保存在find_in_file.py文件中。测试必须准备一个文件,并在测试后打开和删除它,如下例所示:

import unittest
import os # used for, for example, deleting files

from find_in_file import find_string, NotFoundError

class TestFindInFile(unittest.TestCase):
    def setUp(self):
        file = open('test_file.txt', 'w')
        file.write('aha')
        file.close()
        self.file = open('test_file.txt', 'r')
    def tearDown(self):
        self.file.close()
        os.remove(self.file.name)
    def test_exists(self):
        line_no=find_string(self.file, 'aha')
        self.assertEqual(line_no, 0)
    def test_not_exists(self):
        self.assertRaises(NotFoundError, find_string,
                                              self.file, 'bha')

if __name__=='__main__':
    unittest.main()

每次测试前setUp运行,然后tearDown执行。

参数化测试

人们经常想用不同的数据集重复同样的测试。当使用unittest的功能时,这要求我们使用注入的相应方法自动生成测试用例:

为此,我们首先用一个或几个将要使用的方法构建一个测试用例,当我们稍后设置测试方法时。让我们再次考虑二等分方法,并检查它返回的值是否真的是给定函数的零。

我们首先构建测试用例和我们将用于测试的方法,如下所示:

class Tests(unittest.TestCase):
    def checkifzero(self,fcn_with_zero,interval):
        result = bisect(fcn_with_zero,*interval,tol=1.e-8)
        function_value=fcn_with_zero(result)
        expected=0.
        self.assertAlmostEqual(function_value, expected)

然后我们动态地创建测试函数作为这个类的属性:

test_data=[
           {'name':'identity', 'function':lambda x: x,
                                     'interval' : [-1.2, 1.]},
           {'name':'parabola', 'function':lambda x: x**2-1,
                                        'interval' :[0, 10.]},
           {'name':'cubic', 'function':lambda x: x**3-2*x**2,
                                       'interval':[0.1, 5.]},
               ] 
def make_test_function(dic):
        return lambda self :\
                   self.checkifzero(dic['function'],dic['interval'])
for data in test_data:
    setattr(Tests, "test_{name}".format(name=data['name']),
                                           make_test_function(data))
if __name__=='__main__':
  unittest.main()

在本例中,数据以字典列表的形式提供。make_test_function函数动态生成一个测试函数,该函数使用一个特定的数据字典,用之前定义的方法checkifzero执行测试。最后,命令setattr用于制作类Tests的这些测试函数方法。

断言工具

在这一部分,我们收集了最重要的工具来提高一个AssertionError。我们从unittest看到了assert命令和两个工具,即assertAlmostEqual。下表(表 13.1 )总结了最重要的断言工具和相关模块:

| **断言工具及应用示例** | **模块** | | `assert 5==5` | – | | `assertEqual(5.27, 5.27)` | `unittest.TestCase` | | `assertAlmostEqual(5.24, 5.2,places = 1)` | `unittest.TestCase` | | `assertTrue(5 > 2)` | `unittest.TestCase` | | `assertFalse(2 < 5)` | `unittest.TestCase` | | `assertRaises(ZeroDivisionError,lambda x: 1/x,0.)` | `unittest.TestCase` | | `assertIn(3,{3,4})` | `unittest.TestCase` | | `assert_array_equal(A,B)` | `numpy.testing` | | `assert_array_almost_equal(A, B, decimal=5)` | `numpy.testing` | | `assert_allclose(A, B, rtol=1.e-3,atol=1.e-5)` | `numpy.testing` |

表 13.1:Python、unittest 和 NumPy 中的断言工具

浮动比较

两个浮点数不应该与==比较,因为一次计算的结果往往会因为舍入误差而略有偏差。出于测试目的,有许多工具可以测试浮点数的相等性。首先,allclose检查两个数组是否几乎相等。它可以用于测试功能,如图所示:

self.assertTrue(allclose(computed, expected))

这里,self指的是一个unittest.Testcase实例。numpytesting中也有测试工具。这些是通过使用以下方式导入的:

import numpy.testing

使用numpy.testing.assert_array_allmost_equalnumpy.testing.assert_allclose测试两个标量或两个数组是否相等。如上表所示,这些方法描述所需精度的方式不同。

QR 因式分解将给定矩阵分解为正交矩阵 Q 和上三角矩阵 R 的乘积,如下例所示:

import scipy.linalg as sl
A=rand(10,10)
[Q,R]=sl.qr(A)

方法应用正确吗?我们可以通过验证 Q 确实是一个正交矩阵来检验这一点:

import numpy.testing as npt 
npt.assert_allclose(
               dot(Q.T,self.Q),identity(Q.shape[0]),atol=1.e-12)

此外,我们可以通过检查 A = QR 来执行健全性测试:

import numpy.testing as npt
npt.assert_allclose(dot(Q,R),A))

所有这些都可以收集到一个unittest测试用例中,如下所示:

import unittest
import numpy.testing as npt
from scipy.linalg import qr
from scipy import *

class TestQR(unittest.TestCase):
    def setUp(self):
        self.A=rand(10,10)
        [self.Q,self.R]=qr(self.A)
    def test_orthogonal(self):
        npt.assert_allclose(
            dot(self.Q.T,self.Q),identity(self.Q.shape[0]),
                                                        atol=1.e-12)
    def test_sanity(self):
            npt.assert_allclose(dot(self.Q,self.R),self.A)

if __name__=='__main__':
    unittest.main()

注意在assert_allclose中,参数atol默认为零,当处理具有小元素的矩阵时,这通常会导致问题。

单元和功能测试

到目前为止,我们只使用了功能测试。功能测试检查功能是否正确。对于二等分算法,这种算法实际上在有一个零的时候会找到一个零。在这个简单的例子中,单元测试是什么并不清楚。虽然这看起来有点做作,但是仍然可以对二等分算法进行单元测试。它将展示单元测试如何经常导致更加条块分割的实现。

因此,在二等分法中,我们想要检查,例如,在每一步中,间隔的选择是否正确。怎么做?请注意,用当前的实现是绝对不可能的,因为算法隐藏在函数内部。一种可能的补救方法是只运行二等分算法的一个步骤。由于所有的步骤都是相似的,我们可能会说我们已经测试了所有可能的步骤。我们还需要能够在算法的当前步骤检查当前界限ab。所以我们必须添加要作为参数运行的步骤数,并更改函数的返回接口。我们将按如下所示进行操作:

def bisect(f,a,b,n=100):
  ...
  for iteration in range(n):
    ...
  return a,b

注意,为了适应这种变化,我们必须改变现有的单元测试。我们现在可以添加一个单元测试,如图所示:

def test_midpoint(self):
  a,b = bisect(identity,-2.,1.,1)
  self.assertAlmostEqual(a,-0.5)
  self.assertAlmostEqual(b,1.)

调试

在测试时,调试有时是必要的,特别是如果不能立即弄清楚为什么给定的测试没有通过。在这种情况下,能够在交互会话中调试给定的测试是非常有用的。然而,unittest.TestCase类的设计使这变得困难,这阻碍了测试用例对象的简单实例化。解决方案是创建一个仅用于调试目的的特殊实例。

假设,在上面TestIdentity类的例子中,我们想要测试test_functionality方法。这将通过以下方式实现:

test_case = TestIdentity(methodName='test_functionality')

现在,该测试可以通过以下方式单独运行:

test_case.debug()

这将运行这个单独的测试,并允许调试。

测试发现

如果您编写一个 Python 包,各种测试可能会在包中展开。discover模块查找、导入并运行这些测试用例。命令行的基本调用是:

python -m unittest discover

它开始在当前目录中寻找测试用例,并向下递归目录树来寻找名称中包含'test'字符串的 Python 对象。该命令接受可选参数。最重要的是-s修改开始目录和-p定义识别测试的模式:

python -m unittest discover -s '.' -p 'Test*.py'

测量执行时间

为了在代码优化上做出决定,人们通常必须比较几种代码选择,并根据执行时间来决定应该优先选择哪种代码。此外,在比较不同算法时,讨论执行时间是一个问题。在这一节中,我们提供了一种简单易行的方法来测量执行时间。

带魔法功能的计时

测量单个语句执行时间最简单的方法是使用 IPython 的神奇函数%timeit

外壳 IPython 为标准 Python 增加了额外的功能。这些额外的函数被称为魔术函数。

由于单个语句的执行时间可能非常短,因此该语句被放在一个循环中执行几次。通过使用最少的测量时间,可以确保计算机上运行的其他任务不会对测量结果产生太大影响。让我们考虑从数组中提取非零元素的四种可选方法,如下所示:

A=zeros((1000,1000))
A[53,67]=10

def find_elements_1(A):
    b = []
    n, m = A.shape
    for i in range(n):
        for j in range(m):
            if abs(A[i, j]) > 1.e-10:
                b.append(A[i, j])
    return b

def find_elements_2(A):
    return [a for a in A.reshape((-1, )) if abs(a) > 1.e-10]

def find_elements_3(A):
    return [a for a in A.flatten() if abs(a) > 1.e-10]

def find_elements_4(A):
    return A[where(0.0 != A)]

用 IPython 的神奇功能%timeit测量时间会得到以下结果:

In [50]: %timeit -n 50 -r 3 find_elements_1(A)
50 loops, best of 3: 585 ms per loop

In [51]: %timeit -n 50 -r 3 find_elements_2(A)
50 loops, best of 3: 514 ms per loop

In [52]: %timeit -n 50 -r 3 find_elements_3(A)
50 loops, best of 3: 519 ms per loop

In [53]: %timeit -n 50 -r 3 find_elements_4(A)
50 loops, best of 3: 7.29 ms per loop

参数-n控制在测量时间之前执行语句的频率,-r参数控制重复次数。

用 Python 模块计时

Python 提供了一个timeit模块,可以用来测量执行时间。它要求首先构造一个时间对象。它由两个字符串构成,一个字符串包含设置命令,另一个字符串包含要执行的命令。我们采用与前面例子中相同的四种选择。数组和函数定义现在被写在一个名为setup_statements的字符串中,四次对象的构造如下:

import timeit
setup_statements="""
from scipy import zeros
from numpy import where
A=zeros((1000,1000))
A[57,63]=10.

def find_elements_1(A):
    b = []
    n, m = A.shape
    for i in range(n):
        for j in range(m):
            if abs(A[i, j]) > 1.e-10:
               b.append(A[i, j])
    return b

def find_elements_2(A):
    return [a for a in A.reshape((-1,)) if abs(a) > 1.e-10]

def find_elements_3(A):
    return [a for a in A.flatten() if abs(a) > 1.e-10]

def find_elements_4(A):
    return A[where( 0.0 != A)]
"""
experiment_1 = timeit.Timer(stmt = 'find_elements_1(A)',
                            setup = setup_statements)
experiment_2 = timeit.Timer(stmt = 'find_elements_2(A)',
                            setup = setup_statements)
experiment_3 = timeit.Timer(stmt = 'find_elements_3(A)',
                            setup = setup_statements)
experiment_4 = timeit.Timer(stmt = 'find_elements_4(A)',
                            setup = setup_statements)

计时器对象有一个repeat方法。它需要repeatnumber参数。它循环执行定时器对象的语句,测量时间,并重复与repeat参数对应的实验:

我们继续前面的示例,并测量执行时间,如下所示:

t1 = experiment_1.repeat(3,5) 
t2 = experiment_2.repeat(3,5) 
t3 = experiment_3.repeat(3,5) 
t4 = experiment_4.repeat(3,5) 
# Results per loop in ms
min(t1)*1000/5 # 615 ms
min(t2)*1000/5 # 543 ms
min(t3)*1000/5 # 546 ms
min(t4)*1000/5 # 7.26 ms

与前面例子中的方法相反,我们获得了所有获得的测量值的列表。由于计算时间可能会根据计算机的总负载而变化,因此可以将此列表中的最小值视为执行语句所需计算时间的良好近似值。

使用上下文管理器计时

最后,我们提出了第三种方法。它展示了上下文管理器的另一个应用。我们首先构建一个上下文管理器对象来测量经过的时间,如图所示:

import time
class Timer:
    def __enter__(self):
        self.start = time.time()
        # return self
    def __exit__(self, ty, val, tb):
        end = time.time()
        self.elapsed=end-self.start
        print('Time elapsed {} seconds'.format(self.elapsed))
        return False

回想一下_ _enter_ __ _exit_ _方法使这个类成为一个上下文管理器。_ _exit_ _方法的参数tyvaltb在正常情况下None。如果在执行过程中引发异常,它们会获取异常类型、异常值和追溯信息。return False表示到目前为止还没有捕捉到异常。

我们现在展示使用上下文管理器来测量前面示例中四个备选方案的执行时间:

with Timer():
  find_elements_1(A)

这将显示类似Time elapsed 15.0129795074 ms的信息。

如果计时结果应该可以在变量中访问,enter方法必须返回Timer实例(取消对return语句的注释),并且必须使用with ... as ...构造:

with Timer() as t1:
    find_elements_1(A)
t1.elapsed # contains the result

总结

没有测试就没有程序开发!我们展示了良好组织和记录测试的重要性。一些专业人员甚至通过首先指定测试来开始开发。自动测试的一个有用工具是模块unittest,我们已经详细解释过了。虽然测试提高了代码的可靠性,但是需要分析来提高性能。替代的编码方式可能会导致很大的性能差异。我们展示了如何测量计算时间以及如何定位代码中的瓶颈。

练习

Ex。1 →两个矩阵 AB 叫做相似,如果存在一个矩阵 S ,这样 B = S -1 A SAB 具有相同的特征值。写一个测试,通过比较两个矩阵的特征值来检查它们是否相似。是功能测试还是单元测试?

Ex。2 →创建两个高维向量。比较各种方式计算dot乘积的执行时间:

  • SciPy 功能:dot(v,w)
  • 生成器和总和:sum((x*y for x,y in zip(v,w)))
  • 综合清单及合计:sum([x*y for x,y in zip(v,w)])

Ex。3 →让 u 成为向量。矢量 v 带分量

Exercises

叫做 u 的移动平均线。确定计算 v 的两种选择中哪一种更快:

v = (u[:-2] + u[1:-1] + u[2:]) / 3

或者

v = array([(u[i] + u[i + 1] + u[i + 2]) / 3
  for i in range(len(u)-3)])

十四、综合示例

在这一章中,我们提出了一些全面和更长的例子,并简要介绍了理论背景和它们的完整实现。通过这一点,我们想向你展示如何在实践中使用本书中定义的概念。

多项式

首先,我们将通过设计一个多项式类来展示 Python 构造的强大功能。我们将给出一些理论背景,这将引导我们得到一个需求列表,然后我们将给出代码,并给出一些注释。

注意,这个类在概念上不同于类numpy.poly1d

理论背景

一个多项式:p(x)= anxn+an-1xn-1T13】+…+aT16】1T18】x+aT20】0 由其度、其表示及其系数定义。上式所示的多项式表示称为单项表示。在该表示中,多项式被写成单项式的线性组合,xIT25。或者,多项式可以写成:

  • Newton representation with the coefficients ci and n points, x0, …, xn-1:

    p(x)= c**+【c】x【x】-我...。+c【n】t30(x0)(xn-)

  • Lagrange representation with the coefficients yiand n+1 points, x0, … , xn:

    p(x)=yT6】0T8】lT10】0(x)+yT16】1T18】lT20】1(x)+……+ynln

    具有基本功能:

    Theoretical background

有无限多种表现形式,但我们在这里仅限于这三种典型的表现形式。

多项式可以由插值条件确定:

p(xIT5)=yT8】IT10】I= 0,…, n

给定不同的值 x i 和任意值 y i 作为输入。在拉格朗日公式中,插值多项式是直接可用的,因为它的系数是插值数据。牛顿表示中的插值多项式的系数可以通过一个递归公式获得,称为除差公式:

c i ,0 = y i、

Theoretical background

最后,设置Theoretical background

单项表示中的插值多项式的系数是通过求解一个线性系统获得的:

Theoretical background

以给定多项式 p (或其倍数)作为特征多项式的矩阵称为伴随矩阵。伴随矩阵的特征值是多项式的零点(根)。计算 p 零点的算法可以通过首先建立其伴随矩阵,然后用eig计算特征值来构造。牛顿表示的多项式的伴随矩阵如下:

Theoretical background

任务

我们现在可以制定一些编程任务:

  1. pointsdegreecoeffbasis属性写一个名为PolyNomial的类,其中:
    • points是元组列表( x i ,yIT6)
    • degree是对应插值多项式的次数
    • coeff包含多项式系数
    • basis是一个字符串,说明使用了哪种表示法
  2. 为类提供一种在给定点计算多项式的方法。
  3. 为该类提供一个名为plot的方法,在给定的时间间隔内绘制多项式。
  4. 写一个名为__add__的方法,返回两个多项式之和的多项式。请注意,只有在单项情况下,总和才可以通过对系数求和来计算。
  5. 写一个计算以单项形式表示的多项式系数的方法。
  6. 写一个计算多项式伴随矩阵的方法。
  7. 写一个通过计算伴随矩阵的特征值来计算多项式零点的方法。
  8. 写一个计算多项式的方法,该多项式是给定多项式的第I次导数。
  9. 写一个检查两个多项式是否相等的方法。可以通过比较所有系数来检查相等性(零前导系数应该无关紧要)。

多项式类

现在让我们基于多项式的单项公式设计一个多项式基类。多项式可以通过给出其关于单项式基的系数或给出插值点列表来初始化,如下所示:

import scipy.linalg as sl

class PolyNomial:
    base='monomial'
    def __init__(self,**args):
        if 'points' in args:
            self.points = array(args['points'])
            self.xi = self.points[:,0]
            self.coeff = self.point_2_coeff()
            self.degree = len(self.coeff)-1
        elif 'coeff' in args:
            self.coeff = array(args['coeff'])
            self.degree = len(self.coeff)-1
            self.points = self.coeff_2_point()
        else:
            self.points = array([[0,0]])
            self.xi = array([1.])
            self.coeff = self.point_2_coeff()
            self.degree = 0

新类的__init__方法使用了第 7 章函数参数和参数一节中讨论的**args构造。如果没有给定参数,则假设多项式为零。如果多项式由插值点给出,则通过求解范德蒙系统来计算系数的方法如下:

def point_2_coeff(self):
    return sl.solve(vander(self.x),self.y)

如果给定了 k 系数,则 k 插值点通过以下方式构造:

def coeff_2_point(self):
    points = [[x,self(x)] for x in linspace(0,1,self.degree+1)]
    return array(points)

self(x)命令进行多项式求值,这是通过提供一种方法__call__来完成的:

def __call__(self,x):
    return polyval(self.coeff,x)

(参考第 8 章、特殊方法一节的例子。)这里,该方法使用命令polyval。下一步,为了方便起见,我们只是增加了两种方法,我们用property装饰器来装饰(参见第 7 章功能作为装饰器的功能部分】 :

@property
def x(self):
    return self.points[:,0]
@property
def y(self):
    return self.points[:,1]

让我们解释一下这是怎么回事。我们定义了一种提取数据的 x 值的方法,用于定义多项式。类似地,定义了提取数据的 y 值的方法。使用property装饰器,调用方法的结果就好像它只是多项式的一个属性一样。有两种编码选择:

  1. We use a method call:

          def x(self):
              return self.interppoints[:,0]
    

    这允许通过调用p.x()访问 x 值。

  2. 我们使用property装饰机。它允许我们通过以下语句访问 x 值:p.x

我们选择第二种变体。定义一个__repr__方法(参考第八章属性一节)总是一个好的做法。至少对于结果的快速检查,这种方法是有用的:

def __repr__(self):
    txt  = 'Polynomial of degree {degree} \n'
    txt += 'with coefficients {coeff} \n in {base} basis.'
    return txt.format(coeff=self.coeff, degree=self.degree,
                                            base=self.base)

我们现在提供一种绘制多项式的方法,如下所示:

margin = .05
plotres = 500
def plot(self,ab=None,plotinterp=True):
    if ab is None: # guess a and b
       x = self.x
       a, b = x.min(), x.max()
       h = b-a
       a -= self.margin*h
       b += self.margin*h
    else:
       a,b = ab
    x = linspace(a,b,self.plotres)
    y = vectorize(self.__call__)(x)
    plot(x,y)
    xlabel('$x$')
    ylabel('$p(x)$')
    if plotinterp:
        plot(self.x, self.y, 'ro')

注意vectorize命令的使用(参见第 4 章线性代数-数组函数作用于数组一节。__call__方法专用于单项表示,如果一个多项式用另一个基表示,则必须改变。多项式伴随矩阵的计算也是如此:

def companion(self):
    companion = eye(self.degree, k=-1)
    companion[0,:] -= self.coeff[1:]/self.coeff[0]
    return companion

一旦伴随矩阵可用,多项式的零点由特征值给出:

def zeros(self):
   companion = self.companion()
   return sl.eigvals(companion)

为此,功能eigvals必须首先从scipy.linalg导入。我们来举一些用法的例子。

首先,我们从给定的插值点创建一个多项式实例:

p = PolyNomial(points=[(1,0),(2,3),(3,8)])

多项式关于单项式基的系数可作为p的属性获得:

p.coeff # returns array([ 1., 0., -1.])

这对应于多项式The polynomial class。由p.plot(-3.5,3.5)得到的多项式默认图,结果如下图(图 14.1 ):

The polynomial class

图 14.1:多项式作图法的结果

最后,我们计算多项式的零点,在这种情况下是两个实数:

pz = p.zeros() # returns array([-1.+0.j, 1.+0.j])

可以通过在以下几点评估多项式来验证结果:

p(pz) # returns array([0.+0.j, 0.+0.j])

牛顿多项式

NewtonPolyNomial类定义了一个关于牛顿基描述的多项式。我们通过使用super命令(参见第 8 章中的子类化和继承一节),让它从多项式基类继承一些常见的方法,例如polynomial.plotpolynomial.zeros,甚至__init__方法的部分内容:

class NewtonPolynomial(PolyNomial):
    base = 'Newton'
    def __init__(self,**args):
        if 'coeff' in args:
            try:
                self.xi = array(args['xi'])
            except KeyError: 
                raise ValueError('Coefficients need to be given'
                'together with abscissae values xi')
        super(NewtonPolynomial, self).__init__(**args)

一旦给定插值点,系数的计算通过以下方式进行:

def point_2_coeff(self):
    return array(list(self.divdiff()))

这里,我们使用除差来计算多项式的牛顿表示,它在这里被编程为生成器:

def divdiff(self): 
    xi = self.xi
    row = self.y
    yield row[0]
    for level in range(1,len(xi)):
        row = (row[1:] - row[:-1])/(xi[level:] - xi[:-level])
        if allclose(row,0): # check: elements of row nearly zero
           self.degree = level-1
           break
        yield row[0]

让我们简单检查一下这是如何工作的:

pts = array([[0.,0],[.5,1],[1.,0],[2,0.]]) # here we define the
  interpolation data: (x,y) pairs
pN = NewtonPolynomial(points=pts) # this creates an instance of the
  polynomial class
pN.coeff # returns the coefficients array([ 0\. , 2\. , -4\. ,
  2.66666667])
print(pN)

print函数执行基类的__repr__方法并返回以下文本:

Polynomial of degree 3
 with coefficients [ 0.     2.    -4.      2.66666667]
 in Newton basis.

多项式求值不同于基类的相应方法。Newton.PolyNomial.__call__方法需要覆盖Polynomial.__call__:

def __call__(self,x):
    # first compute the sequence 1, (x-x_1), (x-x_1)(x-x_2),...
    nps = hstack([1., cumprod(x-self.xi[:self.degree])])
    return dot(self.coeff, nps)

最后,我们给出伴随矩阵的代码,它覆盖父类的相应方法,如下所示:

def companion(self):
    degree = self.degree
    companion = eye(degree, k=-1)
    diagonal = identity(degree,dtype=bool)
    companion[diagonal] = self.x[:degree]
    companion[:,-1] -= self.coeff[:degree]/self.coeff[degree]
    return companion

注意布尔数组的使用。这些练习将进一步建立在这个基础上。

光谱聚类

特征向量的一个有趣的应用是对数据进行聚类。使用从距离矩阵导出的矩阵的特征向量,可以将未标记的数据分成组。谱聚类方法的名字来源于这个矩阵的谱的使用。 n 元素的距离矩阵(例如,数据点之间的成对距离)是 n × n 对称矩阵。给定这样一个 n × n 距离矩阵 M 和距离值MijT7】,我们可以如下创建数据点的拉普拉斯矩阵:

Spectral clustering

这里,I 是单位矩阵, D 是包含 M 行和的对角矩阵,

Spectral clustering

数据聚类是从 L 的特征向量中获得的。在只有两类数据点的最简单情况下,第一个特征向量(即对应于最大特征值的特征向量)通常足以分离数据。

下面是一个简单的两类聚类的例子。下面的代码创建一些 2D 数据点,并基于拉普拉斯矩阵的第一特征向量对它们进行聚类:

import scipy.linalg as sl

# create some data points
n = 100
x1 = 1.2 * random.randn(n, 2)
x2 = 0.8 * random.randn(n, 2) + tile([7, 0],(n, 1))
x = vstack((x1, x2))

# pairwise distance matrix
M = array([[ sqrt(sum((x[i] - x[j])**2)) 
                                  for i in range(2*n)]          
                                    for j in range(2 * n)])

# create the Laplacian matrix
D = diag(1 / sqrt( M.sum(axis = 0) ))
L = identity(2 * n) - dot(D, dot(M, D))

# compute eigenvectors of L
S, V = sl.eig(L)
# As L is symmetric the imaginary parts
# in the eigenvalues are only due to negligible numerical errors S=S.real
V=V.real

对应于最大特征值的特征向量给出了分组(例如,通过在 0 处设置阈值),并且可以用以下公式表示:

largest=abs(S).argmax()
plot(V[:,largest])

下图(图 14.2 )显示了简单两类数据集的光谱聚类结果:

Spectral clustering

图 14.2:显示了简单的两类聚类的结果

对于更难的数据集和更多的类,通常取 k 最大特征值对应的 k 特征向量,然后用其他方法对数据进行聚类,但使用特征向量代替原始数据点。一个常见的选择是k-意味着聚类算法,这是下一个例子的主题:

特征向量用作 k 的输入-意味着聚类,如下所示:

import scipy.linalg as sl
import scipy.cluster.vq as sc
# simple 4 class data
x = random.rand(1000,2)
ndx = ((x[:,0] < 0.4) | (x[:,0] > 0.6)) & 
                     ((x[:,1] < 0.4) | (x[:,1] > 0.6))
x = x[ndx]
n = x.shape[0]

# pairwise distance matrix
M = array([[ sqrt(sum((x[i]-x[j])**2)) for i in range(n) ]
                                       for j in range(n)])

# create the Laplacian matrix
D = diag(1 / sqrt( M.sum(axis=0) ))
L = identity(n) - dot(D, dot(M, D))

# compute eigenvectors of L
_,_,V = sl.svd(L)

k = 4
# take k first eigenvectors
eigv = V[:k,:].T

# k-means
centroids,dist = sc.kmeans(eigv,k)
clust_id = sc.vq(eigv,centroids)[0]

注意,我们在这里使用奇异值分解sl.svd计算特征向量。由于 L 是对称的,结果就像我们使用sl.eig一样,但是特征向量已经按照特征值的顺序排序了。我们还使用了一次性变量。svd返回一个包含三个数组的列表,左右奇异向量UV,奇异值S,如下所示:

U, S, V = sl.svd(L)

由于这里不需要US,所以在解svd的返回值时可以扔掉:

_, _, V = sl.svd(L)

结果可通过以下方式绘制:

for i in range(k):
    ndx = where(clust_id == i)[0]
    plot(x[ndx, 0], x[ndx, 1],'o')
axis('equal')

下图显示了简单多类数据集的光谱聚类结果:

Spectral clustering

图 14.3:简单四类数据集的光谱聚类示例。

求解初值问题

在这一节中,我们将考虑对给定初始值数值求解一个常微分方程组的数学任务:

( t ) = f ( t,y ) ( t 0】

这个问题的解决方案是一个函数 y 。一种数值方法旨在计算好的近似值,yIT5】∑y(tI)在离散点,即通信点 t i ,在感兴趣的区间内[ t 0 ,t e 。我们在一个类中收集描述问题的数据,如下所示:

class IV_Problem:
    """
    Initial value problem (IVP) class
    """
    def __init__(self, rhs, y0, interval, name='IVP'):
        """
        rhs 'right hand side' function of the ordinary differential
                                                   equation f(t,y)
        y0 array with initial values
        interval start and end value of the interval of independent
        variables often initial and end time
        name descriptive name of the problem
        """
        self.rhs = rhs
        self.y0 = y0
        self.t0, self.tend = interval
        self.name = name

微分方程:

Solving initial value problems

描述一个数学钟摆; y 1 描述其相对于垂直轴的角度, g 为引力常数, l 为其长度。初始角度为π/2,初始角速度为零。

钟摆问题成为问题类的一个实例,如下所示:

def rhs(t,y):
    g = 9.81
    l = 1.
    yprime = array([y[1], g / l * sin(y[0])])
    return yprime

pendulum = IV_Problem(rhs, array([pi / 2, 0.]), [0., 10.] ,
                                            'mathem. pendulum')

对于手头的问题可能会有不同的看法,从而导致不同的类设计。例如,人们可能希望将独立变量的间隔视为解决过程的一部分,而不是问题定义的一部分。考虑初始值时也是如此。正如我们在这里所做的,它们可能被认为是数学问题的一部分,而其他作者可能希望允许初始值的变化,将它们作为求解过程的一部分。

解决方案流程被建模为另一个类:

class IVPsolver:
    """
    IVP solver class for explicit one-step discretization methods
    with constant step size
    """
    def __init__(self, problem, discretization, stepsize):
        self.problem = problem
        self.discretization = discretization
        self.stepsize = stepsize
    def one_stepper(self):
        yield self.problem.t0, self.problem.y0
        ys = self.problem.y0
        ts = self.problem.t0
        while ts <= self.problem.tend:
            ts, ys = self.discretization(self.problem.rhs, ts, ys,
                                                self.stepsize)
            yield ts, ys
    def solve(self):
        return list(self.one_stepper())

我们继续首先定义两种离散化方案:

  • 显式欧拉法:
      def expliciteuler(rhs, ts, ys, h):
          return ts + h, ys + h * rhs(ts, ys)
  • 经典龙格-库塔四阶段法( RK4 ):
      def rungekutta4(rhs, ts, ys, h):
          k1 = h * rhs(ts, ys)
          k2 = h * rhs(ts + h/2., ys + k1/2.) 
          k3 = h * rhs(ts + h/2., ys + k2/2.)
          k4 = h * rhs(ts + h, ys +  k3)
          return ts + h, ys + (k1 + 2*k2 + 2*k3 + k4)/6.

有了这些,我们可以创建实例来获得摆式 ODE 的相应离散化版本:

pendulum_Euler = IVPsolver(pendulum, expliciteuler, 0.001) 
pendulum_RK4 = IVPsolver(pendulum, rungekutta4, 0.001)

我们可以求解两个离散模型,并绘制解和角度差:

sol_Euler = pendulum_Euler.solve()
sol_RK4 = pendulum_RK4.solve()
tEuler, yEuler = zip(*sol_Euler)
tRK4, yRK4 = zip(*sol_RK4)
subplot(1,2,1), plot(tEuler,yEuler),\
       title('Pendulum result with Explicit Euler'),\
       xlabel('Time'), ylabel('Angle and angular velocity')
subplot(1,2,2), plot(tRK4,abs(array(yRK4)-array(yEuler))),\
       title('Difference between both methods'),\
       xlabel('Time'), ylabel('Angle and angular velocity')

Solving initial value problems

图 14.4:用显式欧拉方法模拟摆,并与更精确的龙格-库塔 4 方法的结果进行比较

讨论替代的班级设计是值得的。什么应该放在单独的班级里,什么应该捆绑到同一个班级里?

  • 我们把数学问题和数值方法严格分开了。初始值应该放在哪里?他们应该是问题的一部分还是解决者的一部分?还是应该将它们作为求解器实例的求解方法的输入参数?人们甚至可以设计程序,使其允许多种可能性。使用其中一种替代品的决定取决于该程序的未来用途。通过将初始值作为求解方法的输入参数,可以简化参数识别中对各种初始值的循环。另一方面,用相同的初始值模拟不同的模型变量会促使将初始值与问题联系起来。
  • 为了简单起见,我们只介绍了具有恒定给定步长的解算器。IVPsolver类的设计是否适合自适应方法的未来扩展,在这种扩展中给出的是公差而不是步长?
  • 我们之前建议使用发电机结构作为步进机构。适应性方法需要不时地拒绝步骤。这种需求是否与IVPsolver.onestepper中步进机构的设计相冲突?
  • 我们鼓励您检查用于求解初始值的两个 SciPy 工具的设计,即scipy.integrate.odescipy.integrate.odeint

总结

我们在这本书里解释的大部分内容被捆绑到本章的三个较长的例子中。它们模仿代码开发并给出原型,鼓励你改变和面对自己的想法。

您看到科学计算中的代码可以有自己的味道,因为它与数学定义的算法有很强的关系,并且保持代码和公式之间的关系可见通常是明智的。正如您所看到的,Python 在这方面有技巧。

练习

Ex。1 →实现一种方法__add__,通过将两个给定的多项式 pq 相加,构造一个新的多项式 p+q 。在单项式中,多项式只需将系数相加即可,而在牛顿式中,系数取决于插值点的横坐标 x i 。在将两个多项式的系数相加之前,多项式 q 必须获得新的插值点,其横坐标 x ip 的横坐标一致,并且必须为此提供方法__changepoints__。它应该改变插值点并返回一组新的系数。

Ex。2 →编写转换方法,将一个多项式从牛顿形式转换为单项式形式,反之亦然。

Ex。3 →编写一个名为add_point的方法,该方法将一个多项式 q 和一个元组 (x,y) 作为参数,并返回一个新的插值self.points(x,y) 的多项式。

Ex。4 →编写一个名为LagrangePolynomial的类,以拉格朗日形式实现多项式,并尽可能从多项式基类继承。

Ex。5 →为多项式类编写测试。

十五、符号计算——SymPy

在本章中,我们将简要介绍如何使用 Python 进行符号计算。市场上有强大的执行符号计算的软件,例如,Maple TM 或 Mathematica TM 。但有时,用你习惯的语言或框架进行符号计算可能是有利的。在本书的这个阶段,我们假设这种语言是 Python,所以我们在 Python 中寻找一种工具 SymPy 模块。

如果可能的话,对 SymPy 的完整描述可以填满整本书,这不是本章的目的。相反,我们将通过一些指导性的例子来指出进入这个工具的途径,让这个工具的潜力成为 NumPy 和 SciPy 的补充。

什么是符号计算?

到目前为止,我们在这本书里做的所有计算都是所谓的数值计算。这些是主要对浮点数的一系列操作。数值计算的本质是结果是精确解的近似值。

符号计算通过将代数或微积分中教授的公式或符号转换成其他公式来对公式或符号进行操作。这些转换的最后一步可能需要插入数字并执行数值计算。

我们通过计算这个定积分来说明区别:

What are symbolic computations?

象征性地,这个表达式可以通过考虑被积函数的原始函数来转换:

What are symbolic computations?

我们现在通过插入积分界得到定积分的公式:

What are symbolic computations?

这被称为积分的封闭形式表达式。很少有数学问题的解可以用封闭形式的表达式给出。它是积分的精确值,没有任何近似值。另外,将实数表示为浮点数不会引入误差,否则会引入舍入误差。

逼近和舍入在最后时刻发挥作用,这时需要对这个表达式进行评估。平方根和反正切只能用数值方法近似计算。这样的评估给出了一定精度(通常未知)的最终结果:

What are symbolic computations?

另一方面,数值计算将通过某种近似方法(例如辛普森法则)直接近似定积分,并给出数值结果,通常带有误差估计。在 Python 中,这是通过以下命令完成的:

from scipy.integrate import quad
quad(lambda x : 1/(x**2+x+1),a=0, b=4)   

它们返回值 0.9896614396122965 和误差范围的估计值1.173566342283496 10-08

下图显示了数值近似和符号近似的比较:

What are symbolic computations?

图 15.1:符号和数字求积

在 SymPy 中阐述一个例子

首先,让我们详细说明前面的例子,其中解释了步骤。

首先,我们必须导入模块:

from sympy import *
init_printing()

第二个命令确保公式尽可能以图形方式呈现。然后,我们生成一个符号并定义被积函数:

x = symbols('x')
f = Lambda(x, 1/(x**2 + x + 1))

x现在是类型为Symbol的 Python 对象,f是一个 SymPy Lambda函数(注意命令以大写字母开头)。

现在我们从积分的符号计算开始:

integrate(f(x),x)    

根据你的工作环境,结果以不同的方式呈现;参见下面的截图(图 15.2 ),它代表了 SymPy 公式在不同环境下的两种不同结果:

Elaborating an example in SymPy

图 15.2:一个公式在两个不同环境中的 SymPy 表示的两个截图。

我们可以通过微分来检查结果是否正确。为此,我们为基元函数指定一个名称,并根据 x 进行区分:

pf = Lambda(x, integrate(f(x),x))
diff(pf(x),x)    

获得的结果如下:

Elaborating an example in SymPy

这可以通过使用以下命令来简化:

simplify(diff(pf(x),x))    

Elaborating an example in SymPy

我们期待的结果。

定积分通过使用以下命令获得:

pf(4) - pf(0)     

simplify简化后给出如下输出:

Elaborating an example in SymPy

为了获得一个数值,我们最终将这个表达式求值为一个浮点数:

(pf(4)-pf(0)).evalf() # returns 0.9896614396123

症状的基本要素

这里我们介绍一下 SymPy 的基本元素。您会发现熟悉 Python 中的类和数据类型是有益的。

符号——所有公式的基础

在 SymPy 中构建公式的基本构造元素是符号。正如我们在介绍性示例中看到的,符号是由命令symbols创建的。这个 SymPy 命令从给定的字符串生成符号对象:

x, y, mass, torque = symbols('x y mass torque')

它实际上是以下命令的简短形式:

symbol_list=[symbols(l) for l in 'x y mass torque'.split()]

随后是解包步骤以获得变量:

 x, y, mass, torque = symbol_list

命令的参数定义了符号的字符串表示形式。符号的变量名通常选择为与其字符串表示相同,但这不是语言所要求的:

row_index=symbols('i',integer=True)
print(row_index**2)  # returns i**2

这里,我们还定义了假设符号是整数。

整套符号可以用非常简洁的方式定义:

integervariables = symbols('i:l', integer=True)
dimensions = symbols('m:n', integer=True)
realvariables = symbols('x:z', real=True)

类似地,索引变量的符号可以通过以下方式定义:

A = symbols('A1:3(1:4)')

这给出了一组符号,

Symbols - the basis of all formulas

索引范围的规则是我们在本书前面处理切片时看到的规则(更多详细信息,请参考第 3 章容器类型)。

数字

Python 直接对数字进行运算,不可避免地会引入舍入误差。这些会阻碍所有的符号计算。当我们sympify数字:

1/3  # returns 0.3333333333333333
sympify(1)/sympify(3)  # returns '1/3'

sympify命令将整数转换为类型为sympy.core.numbers.Integer的对象。

不用把 1/3 写成两个整数的运算,也可以用Rational(1,3)直接表示为有理数。

功能

SymPy 区分定义函数和未定义函数。术语未定义函数(可能有点误导)指的是定义良好的 Python 对象,用于没有特殊属性的一般函数。

具有特殊属性的函数的一个例子是本章介绍性示例中使用的atanLambda函数。

注意同一数学函数不同实现的不同名称:sympy.atanscipy.arctan

未定义的函数

通过给symbols命令一个额外的类参数来创建一个未定义函数的符号:

f, g = symbols('f g', cls=Function)

使用Function构造函数也可以实现同样的效果:

f = Function('f')
g = Function('g')

对于未定义的函数,我们可以评估微积分的一般规则。

例如,让我们计算以下表达式:

Undefined functions

这在 Python 中通过使用以下命令象征性地计算出来:

x = symbols('x')
f, g = symbols('f g', cls=Function)
diff(f(x*g(x)),x)

执行时,前面的代码返回以下内容作为输出:

Undefined functions

此示例显示了如何应用产品规则和链规则。

我们甚至可以使用未定义的函数作为几个变量中的函数,例如:

x = symbols('x:3')
f(*x)

它返回以下输出:

Undefined functions

注意使用星型运算符来解包一个元组,形成带有参数的f;参见第 7 章、功能部分匿名功能

利用列表理解,我们可以构造出 f 的所有偏导数的列表:

 [diff(f(*x),xx) for xx in x]

这会返回一个包含Undefined functions(T2 的的梯度)元素的列表:

Undefined functions

也可以使用Function对象的diff方法重写命令:

[f(*x).diff(xx) for xx in x]

另一种方法是泰勒级数展开:

x = symbols('x')
f(x).series(x,0,n=4)

这将返回泰勒公式,以及由朗道符号表示的剩余项:

Undefined functions

初等函数

SymPy 中初等函数的例子是三角函数及其逆函数。以下示例显示了简化如何作用于包含初等函数的表达式:

x = symbols('x')
simplify(cos(x)**2 + sin(x)**2)  # returns 1

下面是使用初等函数的另一个例子:

atan(x).diff(x) - 1./(x**2+1)  # returns 0

如果您同时使用 SciPy 和 SymPy,我们强烈建议您在不同的名称空间中使用它们:

import scipy as sp
import sympy as sym
# working with numbers
x=3
y=sp.sin(x)
# working with symbols
x=sym.symbols('x')
y=sym.sin(x)   

λ-函数

在第 7 章、函数匿名函数一节中,我们看到了如何在 Python 中定义所谓的匿名函数。SymPy 中的对应物由Lambda命令完成。注意区别;lambda是关键词,Lambda是建造师。

命令Lambda采用两个参数,函数自变量的符号和一个 SymPy 表达式来计算函数。

下面是一个将空气阻力(也称为阻力)定义为速度函数的例子:

C,rho,A,v=symbols('C rho A v')
# C drag coefficient, A coss-sectional area, rho density
# v speed
f_drag = Lambda(v,-Rational(1,2)*C*rho*A*v**2)

f_drag显示为一个表达式:

Lambda - functions

这个函数可以通过提供一个参数以通常的方式进行计算:

x = symbols('x')
f_drag(2)
f_drag(x/3)

这将产生给定的表达式:

Lambda - functions

也可以通过提供几个参数在几个变量中创建函数,例如:

t=Lambda((x,y),sin(x) + cos(2*y))

对该函数的调用可以通过两种方式完成,或者直接提供几个参数:

t(pi,pi/2)  # returns -1

或者通过解包元组或列表:

p=(pi,pi/2)
t(*p)   # returns -1

SymPy 中的矩阵对象甚至可以定义向量值函数:

F=Lambda((x,y),Matrix([sin(x) + cos(2*y), sin(x)*cos(y)]))

这使我们能够计算雅可比:

F(x,y).jacobian((x,y))

它给出以下表达式作为输出:

Lambda - functions

在变量较多的情况下,使用更紧凑的形式来定义函数是很方便的:

x=symbols('x:2')
F=Lambda(x,Matrix([sin(x[0]) + cos(2*x[1]),sin(x[0])*cos(x[1])]))  
F(*x).jacobian(x)

符号线性代数

符号线性代数由我们将首先介绍的 SymPy 的matrix数据类型支持。然后,我们将介绍一些线性代数方法,作为这个领域中符号计算的广泛可能性的例子:

符号矩阵

当我们讨论向量值函数时,我们简要地遇到了matrix数据类型。在这里,我们看到了它最简单的形式,它将列表转换成矩阵。举个例子,让我们构造一个旋转矩阵:

phi=symbols('phi')
rotation=Matrix([[cos(phi), -sin(phi)],
                 [sin(phi), cos(phi)]])

当使用 SymPy 矩阵时,我们必须注意到运算符*执行矩阵乘法,而不是像 NumPy 数组那样充当元素乘法。

通过使用矩阵乘法和矩阵转置,可以检查上述定义的旋转矩阵的正交性:

simplify(rotation.T*rotation -eye(2))  # returns a 2 x 2 zero matrix

前面的例子展示了矩阵是如何转置的,以及单位矩阵是如何创建的。或者,我们可以检查它的逆是否是它的转置,这可以通过以下方式完成:

simplify(rotation.T - rotation.inv())

建立矩阵的另一种方法是提供符号列表和形状:

M = Matrix(3,3, symbols('M:3(:3)'))

这将创建以下矩阵:

Symbolic matrices

创建矩阵的第三种方法是通过给定的函数生成其条目。语法是:

Matrix(number of rows,number of colums, function)

我们通过考虑托普利兹矩阵是具有常对角线的矩阵来举例说明上述矩阵。给定一个 2n-1 数据向量 a ,其元素定义为

Symbolic matrices

在 SymPy 中,矩阵可以通过直接使用这个定义来定义:

def toeplitz(n):
    a = symbols('a:'+str(2*n))
    f = lambda i,j: a[i-j+n-1]
    return Matrix(n,n,f)

执行前面的代码给出toeplitz(5):

Symbolic matrices

人们清楚地看到想要的结构;沿着次对角线和超对角线的所有元素都是相同的。我们可以根据第三章容器类型列表一节中介绍的 Python 语法,通过索引和切片来访问矩阵元素:

a=symbols('a')
M[0,2]=0  # changes one element
M[1,:]=Matrix(1,3,[1,2,3]) # changes an entire row

SymPy 中线性代数方法的例子

线性代数的基本任务是求解线性方程组:

Examples for Linear Algebra Methods in SymPy

让我们为一个 3 × 3 矩阵象征性地这样做:

A = Matrix(3,3,symbols('A1:4(1:4)'))
b = Matrix(3,1,symbols('b1:4'))
x = A.LUsolve(b)

这个相对小的问题的输出已经仅仅是可读的了,这可以在下面的表达式中看到:

Examples for Linear Algebra Methods in SymPy

再次,使用simplify命令有助于我们检测取消项并收集常见因素:

simplify(x)

这将导致以下看起来更好的输出:

Examples for Linear Algebra Methods in SymPy

随着矩阵维数的增加,符号计算变得非常慢。对于大于 15 的维度,甚至可能出现内存问题。

上图(图 15.3 )说明了用符号和数值求解线性系统的 CPU 时间差异:

Examples for Linear Algebra Methods in SymPy

图 15.3:用数字和符号求解线性系统的中央处理器时间。

替代

让我们首先考虑一个简单的符号表达式:

x, a = symbols('x a')
b = x + a

如果我们设置x = 0会发生什么?我们观察到b没有变化。我们所做的是改变了 Python 变量x。它现在不再指符号对象,而是指整数对象 0 。由字符串'x'表示的符号保持不变,b也是如此。

相反,通过用数字、其他符号或表达式替换符号来改变表达式是通过一种特殊的替换方法来完成的,这可以在下面的代码中看到:

x, a = symbols('x a')
b = x + a
c = b.subs(x,0)   
d = c.subs(a,2*a)  
print(c, d)   # returns (a, 2a)

此方法采用一个或两个参数:

b.subs(x,0)
b.subs({x:0})  # a dictionary as argument

作为参数的字典允许我们一步完成几个替换:

b.subs({x:0, a:2*a})  # several substitutions in one

由于字典中的条目没有明确的顺序,人们永远不知道哪一个是第一个,因此需要确保对条目进行置换不会影响替换结果。因此,在 SymPy 中,首先在字典中进行替换,然后在表达式中进行替换。下面的例子说明了这一点:

x, a, y = symbols('x a y')
b = x + a
b.subs({a:a*y, x:2*x, y:a/y})
b.subs({y:a/y, a:a*y, x:2*x})

两个替换返回相同的结果,即,

Substitutions

定义多个替换的第三种方法是使用旧值/新值对列表:

 b.subs([(y,a/y), (a,a*y), (x,2*x)]) 

也可以用其他表达式替换整个表达式:

n, alpha = symbols('n alpha')
b = cos(n*alpha)
b.subs(cos(n*alpha), 2*cos(alpha)*cos((n-1)*alpha)-cos((n-2)*alpha))

为了说明矩阵元素的替换,我们再次取 5 × 5 托普利兹矩阵:

Substitutions

考虑替代M.subs(T[0,2],0)。它改变位置[0,2]处的符号对象,即符号 a 2 。它还发生在另外两个地方,这两个地方会自动受到这种替代的影响。

给定的表达式是结果矩阵:

Substitutions

或者,我们可以为这个符号创建一个变量,并在替换中使用它:

a2 = symbols('a2')
T.subs(a2,0)

作为一个更复杂的替代例子,我们描述了如何将托普利兹矩阵转化为三对角托普利兹矩阵这可以通过以下方式实现:首先,我们生成一个列表,列出我们想要替换的符号;然后我们使用zip命令生成一个配对列表。最后,我们通过给出如上所述的旧值/新值对的列表来替代:

symbs = [symbols('a'+str(i)) for i in range(19) if i < 3 or i > 5]
substitutions=list(zip(symbs,len(symbs)*[0]))
T.subs(substitutions)

这给出了以下矩阵结果:

Substitutions

评估符号表达式

在科学计算的环境中,通常需要首先进行符号操作,然后将符号结果转换为浮点数。

评估符号表达式的中心工具是evalf。它使用以下方法将符号表达式转换为浮点数:

pi.evalf()   # returns 3.14159265358979

结果对象的数据类型是Float(注意大写),这是一种允许任意位数(任意精度)的浮点数的 SymPy 数据类型。默认精度对应于 15 位数字,但是可以通过给evalf一个额外的正整数参数来改变,该参数根据数字的数量来指定所需的精度,

pi.evalf(30)   # returns  3.14159265358979323846264338328

使用任意精度的结果是数字可以任意小,也就是说,打破了经典浮点表示的限制;参考第二章变量和基本类型中的浮点数部分。

非常有趣的是,用类型为Float的输入评估一个 SymPy 函数会返回一个与输入精度相同的浮点值。我们在一个更详细的数值分析例子中演示了这一事实的使用。

例:牛顿法收敛阶的研究

如果存在正常数 C ,则迭代xnT3】的迭代方法被称为以顺序 qExample: A study on the convergence order of Newton's Method收敛

Example: A study on the convergence order of Newton's Method

牛顿的方法在一开始的时候有一个很好的初始先后顺序 q = 2,而对于某些问题,甚至 q = 3。牛顿法应用于问题 arctan( x ) = 0 时给出如下迭代方案:

Example: A study on the convergence order of Newton's Method

其立方收敛;那就是 q = 3。

这意味着从一次迭代到另一次迭代,正确数字的数量是三倍。用标准的 16 位浮点数据类型来演示三次收敛和数值确定常数 C 几乎是不可能的。

下面的代码将 SymPy 与高精度评估结合使用,将三次收敛研究推向了极致:

x = sp.Rational(1,2)
xns=[x]

for i in range(1,9):
    x = (x - sp.atan(x)*(1+x**2)).evalf(3000)
    xns.append(x)

结果如下图所示(图 15.4 ),该图显示从一次迭代到另一次迭代,正确数字的数量增加了两倍。

Example: A study on the convergence order of Newton's Method

图 15.4:应用于反正切(x)=0 的牛顿法的收敛性研究

这种极端的精度要求(3000 位数!)使我们能够评估前面序列的七个项,以下列方式演示三次收敛:

# Test for cubic convergence
print(array(abs(diff(xns[1:]))/abs(diff(xns[:-1]))**3,dtype=float64))

结果是七个术语的列表,让我们假设 C = 2/3:

[ 0.41041618, 0.65747717, 0.6666665,  0.66666667, 0.66666667, 0.66666667, 0.66666667]

将符号表达式转换为数值函数

正如我们所看到的,符号表达式的数值计算分三步进行,首先我们进行一些符号计算,然后我们用数字代替数值,并用evalf对浮点数进行计算。

符号计算的原因通常是想进行参数研究。这要求在给定的参数范围内修改参数。这要求符号表达式最终变成数值函数。

多项式系数的参数相关性研究

我们通过一个插值示例来演示符号/数值参数研究,以引入 SymPy 命令lambdify。让我们考虑插入数据x=【0, t ,1】和y=【0,1,-1】的任务。这里, t 是一个自由参数,我们将在区间[-0.4,1.4]内变化。二次插值多项式的系数取决于该参数:

A study on the parameter dependency of polynomial coefficients

使用 SymPy 和中描述的单项方法,我们得到了这些系数的封闭公式:

t=symbols('t')
x=[0,t,1]
# The Vandermonde Matrix
V = Matrix([[0, 0, 1], [t**2, t, 1], [1, 1,1]])
y = Matrix([0,1,-1])  # the data vector
a = simplify(V.LUsolve(y)) # the coefficients
# the leading coefficient as a function of the parameter
a2 = Lambda(t,a[0])

我们获得插值多项式的前导系数 a 2 的符号函数:

A study on the parameter dependency of polynomial coefficients

现在是时候将表达式转换成数字函数了,例如,进行绘图。这是通过功能lamdify完成的。这个函数有两个参数,独立变量和一个 SymPy 函数。

对于 Python 中的示例,我们可以编写:

leading_coefficient = lambdify(t,a2(t))

例如,现在可以通过以下命令绘制该函数:

t_list= linspace(-0.4,1.4,200)
ax=subplot(111)
lc_list = [leading_coefficient(t) for t in  t_list]
ax.plot(t_list, lc_list)
ax.axis([-.4,1.4,-15,10])

上图(图 15.5 )是该参数研究的结果,可以清楚地看到由于多个插值点而产生的奇点,(此处为 t = 0 或 t = 1):

A study on the parameter dependency of polynomial coefficients

图 15.5:多项式系数对插值点位置的依赖性。

总结

在这一章中,你被介绍到了符号计算的世界,你看到了 SymPy 的力量。通过指导示例,您学习了如何设置符号表达式,如何处理符号矩阵,并看到了如何进行简化。使用符号函数并将其转换为数值计算,最终建立了与科学计算和浮点结果的联系。当您使用其强大的结构和清晰的语法将其完全集成到 Python 中时,您体验到了 SymPy 的强大。

把这最后一章当作开胃菜,而不是完整的菜单。我们希望你对科学计算和数学领域未来迷人的编程挑战充满渴望。

十六、附录:参考

  1. 米(meter 的缩写))Abramowitz 和 I.A. Stegun ,带公式、图形和数学表格的数学函数手册,美国商务部,2002 年。ISBN: 9780486612720。
  2. Anaconda–连续体分析下载页面。网址:https://www.continuum.io/downloads
  3. Michael J. Cloud,Moore Ramon E,和 R. Baker Kearfott,区间分析导论,工业与应用数学学会(SIAM),2009。ISBN: 0-89871-669-1。
  4. Python 装饰器库。网址:http://wiki.python.org/moin/PythonDecoratorLibrary
  5. Z.白安得森,,,德梅尔,东加拉,克罗兹,格林鲍姆,汉马林,麦肯尼和索伦森, LAPACK 用户指南, SIAM,1999。ISBN: 9780898714470。
  6. 分数–有理数库。网址:http://docs.python.org/library/fractions.html
  7. 克劳斯元首,扬·埃里克·森然和奥利维尔·威尔第,《用 Python 计算》,皮尔森,2014 年。ISBN: 978-0-273-78643-6。
  8. 函数工具–可调用对象的高阶函数和操作。uRL:http://docs.python.org/library/functools.html
  9. Python 生成器技巧。网址:http://linuxgazette.net/100/pramode.html
  10. 约翰·霍普金斯大学在数学科学方面的研究。约翰·霍普金斯大学出版社,1996 年。ISBN: 9780801854149。
  11. 恩斯特·海勒和格哈德·万纳,《通过历史进行分析》,施普林格,1995 年。
  12. python vs haskell。URL:http://wiki . python . org/moin/pythovshaskell
  13. IEEE 754-2008 标准。网址:http://en.wikipedia.org/wiki/IEEE_754-2008
  14. 区间运算。网址:http://en.wikipedia.org/wiki/Interval_arithmetic
  15. IPython:交互计算。网址:http://ipython.org/
  16. H.P. Langtangen,计算科学的 Python 脚本(计算科学和工程中的文本),斯普林格,2008。ISBN:9783540739159。
  17. 《Python 科学编程入门》(计算科学与工程教材),斯普林格,2009 年。ISBN: 9783642024740。
  18. D.劳登,椭圆函数与应用,斯普林格,1989。ISBN: 9781441930903。
  19. 米(meter 的缩写))卢茨,学习 Python:强大的面向对象编程,奥赖利,2009 。ISBN: 9780596158064。
  20. NumPy 教程–曼德勃罗设定示例: 网址:
  21. matplotlibURL:http://matplot lib . SourceForge . net
  22. 标准:Python 中的记忆化递归斐波那契。网址:http://ujihisa . blogspot . se/2010/11/memoized-recursive-fibonaccin-python . html
  23. Matplotlib mplot3d 工具包。URL:http://matplotlib . SourceForge . net/mpl _ toolkits/mplot 3d
  24. 多变量非线性方程的迭代解,SIAM,2000。ISBN: 9780898714616。
  25. pdb–Python 调试器,文档:http://docs.python.org/library/pdb.html
  26. 费尔南多·佩雷斯和布莱恩·格兰杰。交互式科学计算系统。”In: Comput。Sci。英格。9.3(2007 年 5 月),第 21-29 页。网址:http://ipython.org
  27. 迈克尔·鲍威尔。"一种不用计算导数就能求出多变量函数最小值的有效方法."摘自:《计算机杂志》第 7 期(1964 年第 2 期),第 155-162 页。doi: doi:10.1093/comjnl/7.2.155
  28. 蒂莫西·索尔,数值分析,皮尔森,2006。
  29. 《数值计算基础》,约翰·威利,1997。ISBN: 9780471163633。
  30. 扬·埃里克·森马,《用 Python 编程计算机视觉》,奥莱利媒体,2012 年。网址:http://programmingcomputervision.com
  31. Python 文档–模拟数字类型。网址:http://docs . python . org/reference/data model . html #仿真数字类型
  32. 狮身人面像:Python 文档生成器。网址:http://sphinx.pocoo.org/
  33. J.数值分析导论。应用数学教科书,斯普林格,2002 年。ISBN: 9780387954523。
  34. Python 格式字符串语法。网址:http://docs . python . org/library/string . html #格式-字符串-语法
  35. 南 Tosi,Python 开发人员的 Matplotlib, Packt 出版,2009。ISBN: 9781847197900。
  36. 劳埃德·特雷费森和大卫·鲍尔,《数值线性代数》,暹罗:工业和应用数学学会,1997 年。ISBN: 0898713617。
  37. 可视化–可视化的面向对象方法。网址:http://code.google.com/p/visvis/
  38. 内置例外的完整列表可以在http://docs.python.org/library/exceptions.html找到
posted @ 2025-10-23 15:16  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报