原文:
annas-archive.org/md5/58a42b6d23877a7910fec539d6659c22
译者:飞龙
前言
Python 在科学计算领域具有巨大的潜力。本书更新版《Python 科学计算》增加了关于图形用户界面、高效数据处理和并行计算的新章节,帮助你使用 Python 高效地进行数学和科学计算。
本书将帮助你探索新的 Python 语法特性,并利用科学计算原理创建不同的模型。本书将 Python 与数学应用相结合,展示了如何在计算中应用 Python 概念,并通过涉及 Python 3.8 的示例来说明。你将使用 pandas 进行基本的数据分析,以理解现代科学计算的需求,并涵盖数据模块的改进和内置特性。你还将探索像 NumPy 和 SciPy 这样的数值计算模块,它们可以快速访问高效的数值算法。通过学习使用绘图模块 Matplotlib,你将能够在报告和出版物中展示你的计算结果。特别章节介绍了 SymPy,这是一种用于桥接符号计算和数值计算的工具。本书还介绍了用于消息传递并行编程的 Python 包装器 mpi4py。
在本书结束时,你将对任务自动化有一个扎实的理解,并且能够在科学计算中实现和测试数学算法。
第一章:本书适合的人群
本书适用于具有数学背景的学生、设计现代编程课程的大学教师、数据科学家、研究人员、开发者以及任何希望在 Python 中进行科学计算的人。本书源自于 13 年的 Python 教学经验,涵盖了本科科学与工程项目中的课程、行业内专门的内部课程以及针对高中教师的专业化课程。典型的读者需要在数学、大数据处理、机器学习和仿真等领域使用 Python。因此,具备向量和矩阵的基本知识,以及收敛性和迭代过程等概念将是有益的。
本书所涵盖的内容
第一章,入门,介绍了 Python 的主要语言元素,而不深入细节。这里我们将对所有内容进行简要浏览。对于那些想要直接开始的人来说,这是一个很好的起点;对于那些想要复习函数等基础构造的读者来说,它是一个快速参考。
第二章,变量和基本类型,介绍了 Python 中最重要和最基础的类型。浮动类型是科学计算中最重要的数据类型,另外还有特殊的数字 nan 和 inf。布尔类型、整数、复合数据类型和字符串是本书中将会使用的其他基本数据类型。
第三章,容器类型,解释了如何使用容器类型,主要是列表。字典和元组也会被解释,包括索引和遍历容器对象。偶尔,我们还可以使用集合作为一种特殊的容器类型。
第四章,线性代数 - 数组,涵盖了线性代数中最重要的对象——向量和矩阵。本书选择了 NumPy 数组作为描述矩阵甚至更高阶张量的核心工具。数组具有许多高级特性,并且允许通用函数逐元素地作用于矩阵或向量。本书重点讨论了数组索引、切片以及点积,作为大多数计算任务中的基本操作。通过一些线性代数示例,展示了如何使用 SciPy 的linalg
子模块。
第五章,高级数组概念,解释了一些数组的高级概念。详细解释了数组副本和视图之间的区别,因为视图使得使用数组的程序非常快速,但常常是难以调试的错误源。演示了如何使用布尔数组编写高效、紧凑且易于阅读的代码。最后,通过与函数上的操作进行比较,解释了数组广播的技术——这是 NumPy 数组的独特特性。
第六章,绘图,展示了如何制作图表,主要是经典的x/y图表,也包括 3D 图表和直方图。科学计算需要良好的工具来可视化结果。Python 的matplotlib
模块被引入,从其pyplot
子模块中的方便绘图命令开始。通过创建图形对象(如坐标轴),可以对图表进行微调和修改。我们将展示如何更改这些对象的属性,并如何进行标注。
第七章,函数,讨论了函数,函数是编程中的基本构建块,紧密关联于一些基本的数学概念。函数定义和函数调用被解释为设置函数参数的不同方式。匿名的 lambda 函数被引入,并在全书的多个示例中使用。
第八章,类,将对象定义为类的实例,我们为其提供方法和属性。在数学中,类属性通常相互依赖,这需要特殊的编程技术来处理 setter 和 getter 函数。可以为特殊的数学数据类型定义基本的数学操作,如加法。继承和抽象是反映面向对象编程的数学概念。我们通过使用一个简单的求解常微分方程的类来演示继承的使用。
第九章,迭代,介绍了使用循环和迭代器进行迭代。本书中没有一章不涉及循环和迭代,但在这一章,我们将讨论迭代器的原理,并创建自己的生成器对象。在本章中,你将学习为什么生成器可能会被耗尽,以及如何编写无限循环。Python 的itertools
模块是本章的有用伴侣。
第十章,序列和数据框 – 使用 pandas,简要介绍了 pandas。本章将教你如何在 Python 中处理各种时间序列,DataFrames 的概念,以及如何访问和可视化数据。本章还将讲解 NumPy 数组概念如何扩展到 pandas DataFrames。
第十一章,通过图形用户界面进行通信,展示了 GUI 编程的基本原理。
Matplotlib。解释了事件、滑块移动或鼠标点击的作用,以及它们与所谓的回调函数的交互,附带了一些示例。
第十二章,错误和异常处理,讲解了错误和异常以及如何发现和修复它们。错误或异常是中断程序单元执行的事件。本章将展示遇到这种情况时应该怎么办,也就是说,如何处理异常。你将学习如何定义自己的异常类,并提供有价值的信息来捕获这些异常。错误处理不仅仅是打印错误信息。
第十三章,命名空间、作用域和模块,讲解了 Python 模块。什么是局部变量和全局变量?一个变量何时对程序单元可见,何时不可见?这一章讨论了这个问题。变量可以通过参数列表传递给函数,也可以通过利用其作用域隐式传递。当应该应用这种技术时,何时又不应该应用?本章尝试回答这个核心问题。
第十四章,输入和输出,讲解了处理数据文件的一些选项。数据文件用于存储和提供特定问题的数据,通常是大规模的测量数据。本章描述了如何使用不同的格式访问和修改这些数据。
第十五章,测试,专注于科学编程中的测试。关键工具是unittest
,它允许自动化测试和参数化测试。通过考虑数值数学中的经典二分法算法,我们举例说明了设计有意义的测试的不同步骤,这些步骤还间接提供了代码使用的文档。仔细的测试提供了测试协议,在调试复杂代码时,这些协议可以提供帮助,尤其是当代码由许多不同的程序员编写时。
第十六章,符号计算 – SymPy,完全专注于符号计算。科学计算主要是针对不精确数据和近似结果的数值计算。这与符号计算的形式化操作形成对比,符号计算旨在通过封闭式表达式寻找精确解。在本章中,我们介绍了 Python 中的这一技术,它常用于推导和验证理论上的数学模型和数值结果。我们专注于符号表达式的高精度浮点运算。
第十七章,与操作系统的交互,展示了 Python 脚本与系统命令的交互。本章基于 Linux 系统,如 Ubuntu,仅作为概念和可能性的展示。它将科学计算任务放在应用上下文中,其中不同的软件通常需要结合使用,甚至硬件组件也可能参与其中。
第十八章,Python 并行计算,介绍了并行计算和mpi4py
模块。本章展示了如何在不同处理器上并行执行相同脚本的副本。本章中提供的命令是由mpi4py
Python 模块提供的,这是一个 Python 包装器,用于实现 C 语言中的 MPI 标准。通过学习本章内容,你将能够独立编写并行编程的脚本,并且会发现我们这里只描述了最基础的命令和概念。
第十九章,综合实例,展示了一些全面的、较长的示例,并简要介绍了它们的理论背景和完整实现。这些示例使用了本书迄今为止展示的所有构造,并将它们置于一个更大、更复杂的背景中。读者可以在此基础上进行扩展。
为了充分利用这本书
本书面向初学者或具有一定编程经验的读者。你可以从第一页读到最后一页,也可以选择自己感兴趣的部分。对 Python 的先验知识不是必须的。
本书涉及的软件/硬件 | 操作系统要求 |
---|---|
Python 3.8 | Windows/Linux/macOS |
你需要安装 Ubuntu(或其他 Linux 操作系统)系统,才能进行第十七章,与操作系统交互。
如果你使用的是本书的数字版,我们建议你亲自输入代码,或者通过 GitHub 仓库访问代码(链接在下一部分提供)。这样可以帮助你避免因复制粘贴代码而产生的潜在错误。
下载示例代码文件
你可以从 GitHub 上下载本书的示例代码文件,地址是:github.com/PacktPublishing/Scientific-Computing-with-Python-Second-Edition
。如果代码有更新,GitHub 仓库中的现有代码会随时更新。
我们还有来自我们丰富书籍和视频目录的其他代码包,欢迎访问**github.com/PacktPublishing/
**。快去看看吧!
下载彩色图片
我们还提供了一份 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。你可以在这里下载: static.packt-cdn.com/downloads/9781838822323_ColorImages.pdf
。
使用的约定
本书中使用了一些文本约定。
CodeInText
:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入以及 Twitter 用户名。例子:“for
语句有两个重要的关键字:break
和else
。”
代码块设置如下:
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])
获取联系
我们始终欢迎读者的反馈。
一般反馈:如果你对本书的任何方面有疑问,请在邮件主题中注明书名,并通过customercare@packtpub.com
联系我们。
勘误:虽然我们已经尽力确保内容的准确性,但错误难免发生。如果你发现本书中有错误,感谢你向我们报告。请访问www.packtpub.com/support/errata,选择你的书籍,点击勘误提交表单链接,并填写详细信息。
盗版:如果你在互联网上发现任何形式的非法复制品,感谢你提供相关位置地址或网站名称。请通过copyright@packt.com
与我们联系,并提供相关材料的链接。
如果你有兴趣成为一名作者:如果你在某个领域拥有专业知识,并且有兴趣编写或贡献书籍内容,请访问authors.packtpub.com。
评论
请留下评论。阅读并使用本书后,为什么不在您购买该书的网站上留下评论呢?潜在的读者可以看到并参考您的公正意见来做出购买决策,我们在 Packt 也能了解您对我们产品的看法,而我们的作者也能看到您对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问 packt.com。
开始使用
在本章中,我们将简要概述 Python 的主要语法元素。本章旨在引导刚开始学习编程的读者。每个主题都以 如何做 的方式呈现,并将在本书后续部分以更深入的概念性方式解释,并结合许多应用和扩展。
对于那些已经熟悉其他编程语言的读者,本章将介绍 Python 方式的经典语言构造。这将为他们提供快速入门 Python 编程的机会。
无论哪种类型的读者,都可以将本章作为参考指南,在阅读本书时随时查看。不过,在开始之前,我们需要确保一切准备就绪,确保你已安装正确版本的 Python,并配备好用于科学计算和工具的主要模块,例如一个好的编辑器和 Shell,这些工具有助于代码开发和测试。
在本章中,我们将介绍以下主题:
-
安装和配置说明
-
程序和程序流程
-
Python 中的基本数据类型
-
使用循环重复语句
-
条件语句
-
使用函数封装代码
-
理解脚本和模块
-
Python 解释器
即使你的计算机上已经安装了 Python,仍然建议阅读以下部分。你可能需要对环境进行调整,以符合本书中的工作环境。
第二章:1.1 安装和配置说明
在深入研究本书的主题之前,你应该已经在计算机上安装了所有相关工具。我们提供一些建议,并推荐你可能希望使用的工具。我们只描述公共领域和免费的工具。
1.1.1 安装
目前有两个主要版本的 Python:2.x 分支和新的 3.x 分支。两个分支之间存在语言不兼容性,你需要知道该使用哪个版本。本书基于 3.x 分支,考虑到语言已发布到 3.7 版本。
本书中,你需要安装以下内容:
-
解释器:Python 3.7(或更高版本)
-
用于科学计算的模块:SciPy 与 NumPy
-
用于数学结果图形表示的模块:matplotlib
-
Shell:IPython
-
与 Python 相关的编辑器:最好使用 Spyder(见 图 1.1)。
这些工具的安装通过所谓的发行包来简化。我们建议你使用 Anaconda。
1.1.2 Anaconda
即使你的计算机上已经预安装了 Python,我们仍然建议你创建个人的 Python 环境,这样你可以在不冒险影响计算机功能所依赖的软件的情况下进行工作。通过使用虚拟环境(例如 Anaconda),你可以自由地更改语言版本并安装软件包,而不会产生意外的副作用。
如果最糟糕的情况发生,并且你完全弄乱了,只需删除 Anaconda 目录并重新开始。运行 Anaconda 安装程序将安装 Python、Python 开发环境和编辑器(Spyder)、shell(IPython)以及最重要的数值计算包:SciPy、NumPy 和 matplotlib。
你可以通过在 Anaconda 创建的虚拟环境中使用conda install
安装额外的包(另见官方文档*)。
1.1.3 Spyder
Spyder 的默认屏幕包括左侧的编辑器窗口,右下角的控制台窗口,它提供对 IPython shell 的访问,以及右上角的帮助窗口,如下图所示:
图 1.1:Spyder 的默认屏幕
1.1.4 配置
大多数 Python 代码会保存在文件中。我们建议你在所有 Python 文件中使用以下头部:
from numpy import *
from matplotlib.pyplot import *
通过这个,你可以确保本书中用于科学计算的所有基本数据类型和函数都已经导入。没有这个步骤,本书中的大多数示例都会抛出错误。
Spyder 会提供语法警告和语法错误指示。警告由黄色三角形标记;参见图 1.2。
语法警告表示语句是正确的,但由于某些原因,不建议使用它。前述语句from
就会引发这样的警告。我们将在本书后面讨论这种警告的原因。在这种特定情况下,我们忽略此警告。
图 1.2:Spyder 中的警告三角形
许多编辑器,如 Spyder,提供为你的文件创建模板的功能。查找此功能并将前述头部放入模板中。
1.1.5 Python shell
Python shell 很好,但对于交互式脚本编写来说并不最优。因此,我们建议使用 IPython [25]。
IPython 可以通过不同的方式启动:
-
在终端 shell 中运行以下命令:
ipython
-
直接点击名为 Jupyter QT Console 的图标:
- 在使用 Spyder 时,应该使用 IPython 控制台(参见图 1.1)。
1.1.6 执行脚本
你经常需要执行文件的内容。根据文件在计算机上的位置,执行文件内容之前,必须导航到正确的位置:
-
在 IPython 中使用命令
cd
,以便切换到文件所在的目录。 -
要执行名为
myfile.py
的文件内容,只需在 IPython shell 中运行以下命令:
run myfile
1.1.7 获取帮助
以下是一些使用 IPython 的提示:
-
要获取有关某个对象的帮助,只需在对象的名称后面键入
?
,然后按下回车键。 -
使用箭头键来重复上次执行的命令。
-
你可以使用Tab键进行补全(即你输入一个变量或方法的首字母,IPython 会展示一个包含所有可能补全项的菜单)。
-
使用Ctrl+D退出。
-
使用 IPython 的魔法函数。你可以通过在命令提示符下输入
%%magic
来查看函数列表和说明。
你可以在 IPython 的在线文档中了解更多信息。
1.1.8 Jupyter – Python notebook
Jupyter notebook 是一个非常棒的工具,用于展示你的工作。学生可能希望用它来制作和记录作业和练习,而老师则可以用它来准备讲座,甚至是幻灯片和网页。
如果你通过 Anaconda 安装了 Python,你已经具备了 Jupyter 所需的一切。你可以通过在终端窗口中运行以下命令来启动 notebook:
jupyter notebook
一个浏览器窗口将会打开,你可以通过网页浏览器与 Python 进行交互。
1.2 程序与程序流程
程序是一个按自上而下顺序执行的语句序列。这个线性执行顺序有一些重要的例外:
-
可能会有条件执行替代语句组(代码块),我们称之为分支。
-
有些代码块会被重复执行,这叫做循环(见图 1.3)。
-
有一些函数调用,它们是指向另一个代码片段的引用,该代码片段在主程序流程恢复之前被执行。函数调用打断了线性执行,并暂停程序单元的执行,同时将控制权传递给另一个单元——一个函数。当该函数执行完成后,控制权会返回给调用单元。
图 1.3:程序流程
Python 使用特殊的语法来标记语句块:一个关键字,一个冒号,以及一个缩进的语句序列,这些语句属于该代码块(见图 1.4)。
图 1.4:代码块命令
1.2.1 注释
如果程序中的一行包含符号 #
,那么该行后面的内容会被视为注释:
# This is a comment of the following statement
a = 3 # ... which might get a further comment here
1.2.2 行连接
行末的反斜杠 \
表示下一行是续行,即显式行连接。如果一行结束时所有括号没有闭合,下一行将自动被识别为续行,即隐式行连接。
1.3 Python 中的基本数据类型
让我们来看看你在 Python 中会遇到的基本数据类型。
1.3.1 数字
一个数字可以是整数、实数或复数。常见的运算如下:
-
加法和减法,
+
和-
-
乘法和除法,
*
和/
-
幂运算,
**
这里是一个示例:
2 ** (2 + 2) # 16
1j ** 2 # -1
1\. + 3.0j
符号 j
表示复数的虚部。它是一个语法元素,不应与变量的乘法混淆。
1.3.2 字符串
字符串是由字符组成的序列,用单引号或双引号括起来:
'valid string'
"string with double quotes"
"you shouldn't forget comments"
'these are double quotes: ".." '
你还可以使用三引号来表示多行字符串:
"""This is
a long,
long string"""
1.3.3 变量
变量是对对象的引用。一个对象可以有多个引用。你使用赋值运算符=
将值赋给一个变量:
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)
1.3.4 列表
列表是非常有用的结构,是 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
len(['a',[1,2]]) # returns 2
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]
- 正如你所预期的,使用整数乘以列表会将该列表与自身连接多次:
n*L
等同于进行n次添加操作:
L = [1, 2]
3 * L # [1, 2, 1, 2, 1, 2]
1.3.6 布尔表达式
布尔表达式是一个值为True
或False
的表达式。一些常见的返回条件表达式的运算符如下:
-
相等:
==
-
不等于:
!=
-
严格小于、小于或等于:
<
、<=
-
严格大于、大于或等于:
>
、>=
你可以使用or
和and
将不同的布尔值组合在一起。关键字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
。运算符and
和or
的优先级最低。优先级高的运算符会在优先级低的运算符之前被计算。
1.4 使用循环重复语句
循环用于重复执行一系列语句,同时在每次迭代时改变一个变量的值。这个变量被称为索引变量。它依次被赋值为列表的元素:
L = [1, 2, 10]
for s in L:
print(s * 2) # output: 2 4 20
for
循环中需要重复的部分必须正确缩进:
my_list = [...] # define a list
for elt in my_list:
... #do_something
... #something_else
print("loop finished") # outside the for block
1.4.1 重复任务
for
循环的一个典型用法是重复执行某个任务固定次数:
n = 30
for iteration in range(n):
... # a statement here gets executed n times
1.4.2 break 和 else
for
语句有两个重要的关键字:break
和else
。关键字break
可以在迭代列表未结束时退出for
循环:
x_values=[0.5, 0.7, 1.2]
threshold = 0.75
for x in x_values:
if x > threshold:
break
print(x)
最后的else
检查for
循环是否通过break
关键字被中断。如果没有被中断,那么else
后面的代码块会被执行:
x_values=[0.5, 0.7]
threshold = 0.75
for x in x_values:
if x > threshold:
break
else:
print("all the x are below the threshold")
1.5 条件语句
本节内容介绍如何使用条件语句进行分支、跳出或以其他方式控制代码。
条件语句定义了一个代码块,如果条件为真则执行。一个可选的以关键字else
开始的代码块将在条件未满足时执行(见图 1.4)。我们通过打印来演示这一点,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/a3b7b1bd-67b2-41b9-a204-e4289a6af25a.png,即https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/17c7560e-295e-4113-a19b-3d1e2d4a9d3a.png的绝对值:
Python 等效代码如下:
x = ...
if x >= 0:
print(x)
else:
print(-x)
任何对象都可以测试其布尔值,用于if
或while
语句。如何获得布尔值的规则在第 2.3.2 节中有说明,布尔转换*。*
1.6 使用函数封装代码
函数对于将相似的代码片段聚集在一起非常有用。考虑以下的数学函数:
Python 等效代码如下:
def f(x):
return 2*x + 1
在图 1.5中,函数块的各个元素已被解释:
-
关键字
def
告诉 Python 我们正在定义一个函数。 -
f
是函数的名称。 -
x
是函数的参数或输入。 -
return
后面的部分被称为函数的输出。
图 1.5:函数的结构
一旦函数定义完成,就可以使用以下代码调用它:
f(2) # 5
f(1) # 3
1.7 理解脚本和模块
一组语句保存在一个文件中(通常该文件具有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
1.7.1 简单模块 - 收集函数
通常,你会在脚本中收集函数。这将创建一个具有额外 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 术语中,你可以说这些函数被放入了实际的命名空间中。
1.7.2 使用模块和命名空间
另外,也可以通过命令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
导入命令import
和from
。仅将函数导入到相应的命名空间中。导入后更改函数对当前的 Python 会话没有影响。
1.8 Python 解释器
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 中最重要和最基本的类型。什么是类型?它是由数据内容、其表示以及所有可能的操作组成的集合。在本书的后续部分,当我们在第八章:类中介绍类的概念时,我们将更精确地定义这一概念。
在本章中,我们将涵盖以下主题:
-
变量
-
数字类型
-
布尔值
-
字符串
第三章:2.1 变量
变量是 Python 对象的引用。它们通过赋值创建,例如:
a = 1
diameter = 3.
height = 5.
cylinder = [diameter, height] # reference to a list
变量的名称可以由大写字母、小写字母、下划线_
和数字组成。变量名不能以数字开头。请注意,变量名是区分大小写的。良好的变量命名是文档化工作的重要部分,因此我们建议使用具有描述性的变量名。
Python 有 33 个保留关键字,不能作为变量名使用(见表 2.1)。如果尝试将这些关键字作为变量名,将会引发语法错误:
表 2.1:保留的 Python 关键字
与其他编程语言不同,Python 中的变量不需要声明类型。类型是自动推导的:
x = 3 # integer (int)
y = 'sunny' # string (str)
你可以通过多重赋值语句创建多个变量:
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
2.2 数字类型
在某些情况下,你将不得不处理数字,因此我们首先考虑 Python 中不同的数字类型形式。在数学中,我们区分自然数(ℕ)、整数(ℤ)、有理数(ℚ)、实数(ℝ)和复数(ℂ)。这些是无限集合。不同集合之间的运算有所不同,有时甚至没有定义。例如,通常在 ℤ 中进行除法操作可能不会得到一个整数——在 ℤ 中没有定义。
在 Python 中,像许多其他计算机语言一样,我们有数字类型:
-
数字类型
int
,它至少在理论上是整个 ℤ -
数字类型
float
,它是 ℝ 的一个有限子集 -
数字类型
complex
,这是 ℂ 的一个有限子集
有限集合有最小值和最大值,并且两个数字之间有最小间隔;有关更多细节,请参见第 2.2.2 节,浮点数。
2.2.1 整数
最简单的数字类型是整数类型int
。
整数
语句k = 3
将变量k
赋值为一个整数。
对整数应用+
、-
或*
等运算符会返回一个整数。除法运算符//
返回一个整数,而/
返回一个float
:
6 // 2 # 3 an integer value
7 // 2 # 3
7 / 2 # 3.5 a float value
Python 中的整数集合是无限的;没有最大的整数。这里的限制是计算机的内存,而不是语言给出的固定值。
如果在前面的示例中,除法运算符(/
)返回 3,则说明你没有安装正确的 Python 版本。
2.2.2 浮动点数
如果你在 Python 中执行语句a = 3.0
,你创建了一个浮动点数(Python 类型:float
)。这些数字形成有理数的有限子集,ℚ。
另外,常量也可以用指数表示法给出,如 a = 30.0e-1
或简写为 a = 30.e-1
。符号 e
将指数与尾数分开,该表达式在数学表示中读作 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/34c6a326-b6a8-4985-af30-2f6a912355f8.png。浮动点数 这个名称指的是这些数字的内部表示,并反映了在考虑广泛范围内的数字时,小数点的浮动位置。
对两个浮动点数,或一个整数与一个浮动点数应用基本的数学运算,如 +
、-
、*
和 /
,将返回一个浮动点数。
浮动点数之间的运算很少会返回与有理数运算中预期的精确结果:
0.4 - 0.3 # returns 0.10000000000000003
这个事实在比较浮动点数时非常重要:
0.4 - 0.3 == 0.1 # returns False
这背后的原因可以通过查看浮动点数的内部表示来显现;另请参见第 15.2.6 节,浮动点比较。
浮动点表示
一个浮动点数由三个量表示:符号、尾数和指数:
其中* ![]* 和 ![]。
*https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/f6b98d33-ab3e-45ae-8896-4ebd7f803c80.png被称为尾数长度。条件![]使得表示是唯一的,并在二进制情况下(![])节省了一个位。
在典型的英特尔处理器上,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/5d26973f-5573-4563-879e-1274e2f8d403.png位用于尾数,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/ed7fbf30-ec56-4388-a342-d80506b81d79.png位用于指数https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/766ba752-823f-4c34-b7de-73e7492d0707.png。因此,指数的上限https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/b5e73e68-6101-4590-bb39-5a99dab93cea.png是![]。
对于此数据,最小的可表示正数是
^(![)],并且最大的 ^(![)].
请注意,浮点数在 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/ca6cbce7-a4ce-46d0-99bb-dca5f1794c47.png 中并非等间隔分布。特别地,零附近有一个间隙(另见 [29])。在 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/3eb1eae7-94e1-480b-841a-2fed7ac10df9.png 和第一个正数之间的距离是 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/67eab6d8-e466-4605-ba04-a14143e434da.png,而第一个和第二个之间的距离则较小,缩小了一个因子 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/8473ff72-9b9e-487c-879a-2dbe99db75fb.png。这种由标准化 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/a9bba62c-f778-4468-ac75-a5daa0022e28.png 引起的效应在 图 2.1 中得到了可视化:
图 2.1:零处的浮点间隙。这里是 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/d13b67f1-5714-4ab2-9464-57890ea60892.png
这个间隙被等距填充,使用的是 非规范化 浮点数,并将此类结果四舍五入为这些数。非规范化浮点数具有最小的指数,并且不遵循标准化惯例 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/a9bba62c-f778-4468-ac75-a5daa0022e28.png。
无限和非数字
总共有 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/de248920-9ba1-48a4-9ff0-67d55e999013.png 个浮点数。有时,数值算法计算的浮点数超出了这个范围。
这会导致数字溢出或下溢。在 NumPy 中,溢出结果会被赋值为特殊的浮点数 inf
:
exp(1000.) # inf
a = inf
3 - a # -inf
3 + a # inf
使用 inf
可能会导致数学上未定义的结果。Python 会通过将结果赋值给另一个特殊的浮点数 nan
来表示这一点。nan
代表 非数字,即数学运算的未定义结果。为证明这一点,我们继续前面的例子:
a + a # inf
a - a # nan
a / a # nan
对于与 nan
和 inf
的操作,有一些特殊规则。例如,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
检查 nan
和 inf
的一种方法是使用 isnan
和 isinf
函数。通常,当变量的值为 nan
或 inf
时,您希望直接作出反应。可以通过使用 NumPy 命令 seterr
来实现这一点。以下命令
seterr(all = 'raise')
如果一个计算返回其中的某个值,则会引发 FloatingPointError
错误。
下溢 - 机器精度
下溢发生在操作结果是一个落入零附近间隙的有理数时;见 图 2.1。
机器精度,或称舍入单位,是使得 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/6d100c01-69a0-4326-a8ed-b2b7ecbb38ef.png 的最大数字,这样 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/47f7d0b1-c2dd-4fa9-8bff-145c7015cb61.png。
请注意,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/a7c0bf81-47d4-4b9e-ac42-dd4d5f4dab79.png 在当今大多数计算机上是这样的。您当前运行代码的机器上适用的值可以使用以下命令访问:
import sys
sys.float_info.epsilon # 2.220446049250313e-16
变量sys.float_info
包含关于浮动点类型在你机器上内部表示的更多信息。
函数float
将其他类型转换为浮动点数(如果可能)。这个函数在将适当的字符串转换为数字时特别有用:
a = float('1.356')
NumPy 中的其他浮动点类型:
NumPy 还提供了其他浮动点类型,这些类型在其他编程语言中被称为双精度和单精度数字,分别是float64
和float32
:
a = pi # returns 3.141592653589793
a1 = float64(a) # returns 3.141592653589793
a2 = float32(a) # returns 3.1415927
a - a1 # returns 0.0
a - a2 # returns -8.7422780126189537e-08
倒数第二行演示了a
和a1
在精度上没有差别。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
2.2.3 复数:
复数是实数的扩展,广泛应用于许多科学和工程领域。
数学中的复数:
复数由两个浮动点数组成,一个是该数的实部 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/083c68b7-6261-47c5-ba34-1159100bf27c.png,另一个是它的虚部 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/fb28559a-4054-4030-83de-a51af952b28f.png。在数学中,复数写作 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/f0d38862-1d90-4fbb-866c-6452f22f82fb.png,其中 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/76735070-c450-4546-9eb6-5ec65378509d.png由 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/7dfea4b2-f817-4a4d-8cd5-df416093471f.png定义,称为虚数单位。共轭复数对 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/9a4dbac2-6452-4e15-8c32-e4dfbf9f777f.png是 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/f3c4e8c1-1e17-44c0-8d58-e939be4cdc44.png。
如果实部 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/226b452e-93e2-4f55-95b9-a56845d21fc0.png为零,则该数字称为虚数。
j 表示法:
在 Python 中,虚数通过在浮动点数后添加字母j
来表示,例如,z = 5.2j
。复数是由一个实数和一个虚数组成的,例如,z = 3.5 + 5.2j
。
虽然在数学中,虚部表示为实数b与虚数单位 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/6c2f614f-c3d2-4a4b-873e-7afdc5a20e1c.png的乘积,但在 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)
实部和虚部:
你可以使用real
和imag
属性访问复数 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/66674c49-3c36-41a0-835b-5a9128c904cb.png的实部和虚部。这些属性是只读的;换句话说,它们不能被改变:
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
有趣的是,real
和imag
属性以及共轭方法对复数数组同样适用;参见第 4.3.1 节,数组属性。我们通过计算第 N 次单位根来展示这一点,这些单位根是https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/58eb7ad5-3e96-4cc9-a2b5-b94a5d1f70d6.png的https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/a2c3cdd6-df4f-4a77-914c-d73959af422d.png解:
from matplotlib.pyplot import *
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
结果图显示了 10 个单位根。在图 2.2中,它通过标题和坐标轴标签进行补充,并与单位圆一起显示。(有关如何绘制图表的更多细节,请参见第六章:绘图。)
图 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)
2.3 布尔值
布尔值是一种数据类型,得名于乔治·布尔(1815-1864)。布尔变量只能取两个值,True
或False
。这种类型的主要用途是在逻辑表达式中。以下是一些示例:
a = True
b = 30 > 45 # b gets the value False
布尔表达式常常与if
语句结合使用:
x= 5
if x > 0:
print("positive")
else:
print("nonpositive")
2.3.1 布尔运算符
布尔操作通过关键字and
、or
和not
来执行:
True and False # False
False or True # True
(30 > 45) or (27 < 30) # True
not True # False
not (3 > 4) # True
运算符遵循一些优先级规则(参见第 1.3.5 节,布尔表达式),这些规则使得第三行和最后一行的括号变得不必要。然而,无论如何,使用括号是一种良好的实践,可以提高代码的可读性。
请注意,and
运算符在以下布尔表达式中是隐式链式连接的:
a < b < c # same as: a < b and b < c
a < b <= c # same as: a < b and b <= c (less or equal)
a == b == c # same as: a == b and b == c
2.3.2 布尔类型转换
大多数 Python 对象都可以转换为布尔值;这称为布尔类型转换。内置函数bool
执行这种转换。需要注意的是,大多数对象都会转换为True
,除了0
、空元组、空列表、空字符串或空数组,这些都转换为False
。
表 2.2:布尔值的类型转换规则
除非数组不含元素或仅包含一个元素,否则不可能将数组转换为布尔值;这一点在第 5.2.1 节,布尔数组中有进一步的解释。前面的表格(参见表 2.2:布尔值类型转换规则)总结了布尔类型转换的规则。
我们通过一些使用示例来演示这一点:
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: # the modulo operator
print("n is odd")
else:
print("n is even")
请注意,我们使用了%
进行取模运算,它返回整数除法后的余数。在这种情况下,它返回0
或1
作为除以 2 后的余数。
在这个最后的例子中,值0
或1
会被转换为bool
;也请参见第 2.3.4 节,布尔值和整数。
布尔运算符or
、and
和not
也会隐式地将其一些参数转换为布尔值。
2.3.3 and
和or
的返回值
请注意,运算符and
和or
并不一定会产生布尔值。这可以通过以下等式来解释:*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
时,变量x
甚至不需要被定义!False and x
同样适用。
请注意,与数学逻辑中的对应运算符不同,这些运算符在 Python 中不再是交换律的。事实上,以下表达式并不等价:
1 or 'a' # produces 1
'a' or 1 # produces 'a'
2.3.4 布尔值和整数
实际上,布尔值和整数是相同的。唯一的区别在于0
和1
的字符串表示形式,在布尔值中,它们分别是False
和True
。这使得可以构造如下:
def print_ispositive(x):
possibilities = ['nonpositive or zero', 'positive']
return f"x is {possibilities[x>0]}"
这个例子中的最后一行使用了字符串格式化,具体解释见第 2.4.3 节,字符串格式化。
我们指出,对于已经熟悉子类概念的读者,bool
类型是int
类型的子类(请参见第八章:类)。实际上,所有四个查询——isinstance(True, bool)
、isinstance(False, bool)
、isinstance(True, int)
和isinstance(False, int)
都返回值True
(请参见第 3.7 节,检查变量的类型)。
即使是像True+13
这样很少使用的语句也是正确的。
2.4 字符串
string
类型是用于文本的类型:
name = 'Johan Carlsson'
child = "Åsa is Johan Carlsson's daughter"
book = """Aunt Julia
and the Scriptwriter"""
字符串可以由单引号或双引号括起来。如果字符串包含多行,则必须用三个双引号"""
或三个单引号'''
括起来。
字符串可以通过简单的索引或切片来索引(请参见第三章:容器类型,了解关于切片的详细说明):
book[-1] # returns 'r'
book[-12:] # returns 'Scriptwriter'
字符串是不可变的;也就是说,项不能被更改。它们与元组共享这个特性。命令**book[1] = 'a'
**返回:
TypeError: 'str' object does not support item assignment
2.4.1 转义序列和原始字符串
字符串'\n'
用于插入换行符,'\t'
用于在字符串中插入水平制表符(TAB)以对齐多行:
print('Temperature\t20\tC\nPressure\t5\tPa')
这些字符串是转义序列的例子。转义序列总是以反斜杠\
开始。多行字符串会自动包含转义序列:
a="""
A multi-line
example"""
a # returns '\nA multi-line \nexample'
一个特殊的转义序列是"\\"
,它表示文本中的反斜杠符号:
latexfontsize="\\tiny"
print(latexfontsize) # prints \tiny
同样的结果可以通过使用原始字符串来实现:
latexfs=r"\tiny" # returns "\tiny"
latexfontsize == latexfs # returns True
请注意,在原始字符串中,反斜杠保持在字符串中并用于转义某些特殊字符:
print(r"\"") # returns \"
print(r"\\") # returns \
print(r"\") # returns an error (why?)
原始字符串是一种方便的工具,用于以可读的方式构建字符串。结果是相同的:
r"\"" == '\\"'
r"She: \"I am my dad's girl\"" == 'She: \\"I am my dad\'s girl\\"'
2.4.2 字符串操作和字符串方法
多个字符串的相加会导致它们的连接:
last_name = 'Carlsson'
first_name = 'Johanna'
full_name = first_name + ' ' + last_name
# returns 'Johanna Carlsson'
因此,整数的乘法是重复加法:
game = 2 * 'Yo' # returns 'YoYo'
对浮点数或复数的乘法未定义,并会导致TypeError
。
当字符串进行比较时,采用字典顺序,大写形式排在相同字母的小写形式之前:
'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'
- 在字符串中搜索:该方法返回字符串中给定搜索子字符串开始的第一个索引位置:
birthday = '20101210'
birthday.find('10') # returns 2
如果搜索字符串未找到,方法的返回值是-1
。
- 字符串格式化:该方法将变量的值或表达式的结果插入字符串中。它非常重要,以至于我们将以下小节专门讨论它。
2.4.3 字符串格式化
字符串格式化是将值插入给定字符串并确定其显示方式的过程。这可以通过多种方式实现。我们首先描述相关的字符串方法format
,以及更现代的替代方法——所谓的f-string。
下面是一个关于使用 format 方法的例子:
course_code = "NUMA01"
print("Course code: {}".format(course_code)) # Course code: NUMA01
这里是使用f-string的变体例子:
course_code = "NUMA01"
print(f"Course code: {course_code}") # Course code: NUMA01
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
类似地,格式说明符也可以在 f-string 中使用:
quantity = 33.45
print(f"{quantity:1.1f}") # 33.5
格式说明符允许指定四舍五入精度(表示中小数点后的位数)。此外,还可以设置表示数字的符号总数,包括前导空格。
在这个例子中,获取其值的对象名称作为参数传递给format
方法。第一个{}
对会被第一个参数替换,后续的{}
对会被后续的参数替换。或者,使用键值对语法也可能很方便:
print("{name} {value:.1f}".format(name="quantity",value=quantity))
# prints "quantity 33.5"
这里处理了两个值——一个没有格式说明符的字符串name
,和一个浮点数value
,它以固定点格式打印,保留小数点后一位。(详细内容请参考完整的字符串格式化文档。)
字符串中的大括号
有时候,一个字符串可能包含一对大括号,但不应被视为format
方法的占位符。在这种情况下,使用双大括号:
r"we {} in LaTeX \begin{{equation}}".format('like')
这将返回以下字符串:'we like in LaTeX \\begin{equation}'
。
2.5 小结
在本章中,你了解了 Python 中的基本数据类型,并看到了相应的语法元素。我们将主要处理整数、浮点数和复数等数值类型。
布尔值在设置条件时是必需的,且通过使用字符串,我们常常传达结果和消息。
2.6 练习
例 2: 根据德摩根公式,以下公式成立:
选择数字n和x并在 Python 中验证公式。
例 3: 复数。以同样的方式验证欧拉公式:
例 4: 假设我们正试图检查一个发散序列的收敛性(这里,序列由递归关系定义:https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/5bcda964-c7e2-47e7-8cf9-2bae91d8b1b5.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0b4ceaa6-cea3-485e-aa01-10fc06d8fbee.png):
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')
-
由于序列不收敛,代码应打印
No convergence
消息。执行它来看看会发生什么。 -
如果你替换掉这一行会发生什么?
if not abs(u-uold) > 1.e-8:
使用
if abs(u-uold) < 1.e-8:
它应该给出完全相同的结果,不是吗?再次运行代码查看会发生什么。
-
如果你将
u=1.0
替换为u=1
(没有小数点),会发生什么?运行代码来验证你的预测。 -
解释这个代码的意外行为。
例 5: 一个蕴含式 C = (A ⇒ B) 是一个布尔表达式,定义如下:
-
C当A为
False
或A和B都为True
时是True
-
C在其他情况下是
False
编写一个 Python 函数implication(A, B)
。
例 6: 这个练习是用来训练布尔运算的。两个二进制数字(位)通过一个称为半加器的逻辑装置相加。它生成一个进位位(下一个更高位的数字)和根据下表定义的和,半加器电路:
p | q | sum | carry |
---|---|---|---|
1 | 1 | 0 | 1 |
1 | 0 | 1 | 0 |
0 | 1 | 1 | 0 |
0 | 0 | 0 | 0 |
半加器操作的定义:
图 2.3:半加法器电路
全加法器由两个半加法器组成,它可以对两个二进制位和一个额外的进位位进行求和(另请参见下图):
图 2.4:全加法器电路
编写一个实现半加法器的函数,并编写另一个实现全加法器的函数。测试这些函数。
容器类型
容器类型用于将对象组合在一起。不同容器类型之间的主要区别在于如何访问单个元素以及如何定义操作。在本章中,我们讨论了诸如列表、元组、字典和集合等容器类型以及相关的概念,如索引技巧。更专业的容器,如 pandas DataFrame,将在第四章:线性代数–数组、第五章:高级数组概念,以及第十章:序列和数据框中介绍。
特别地,我们将涵盖以下主题:
-
列表
-
数组
-
元组
-
字典
-
集合
第四章:3.1 列表
在本节中,我们介绍列表——Python 中最常用的容器数据类型。使用列表,我们可以将多个甚至完全不同的 Python 对象放在一起。
列表,顾名思义,是由任何类型的对象组成的列表:
L = ['a', 20.0, 5]
M = [3,['a', -3.0, 5]]
本例中的第一个列表包含一个字符串、一个浮动数和一个整数对象。第二个列表M
包含另一个列表作为它的第二个元素。
每个对象通过分配给每个元素一个索引来进行枚举。列表中的第一个元素获得索引 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
3.1.1 切片
就像从一条面包中切下一片,列表也可以被切分成切片。将列表在i
和j
之间切割会创建一个新列表,其中包含从索引i
开始、在j
之前结束的元素。
对于切片,必须给出一个索引范围。L[i:j]
意味着通过从L[i]
开始到L[j-1]
为止,创建一个新列表。换句话说,新列表是通过从L
中删除前i
个元素并取下一个j-i
个元素来得到的。
这里,L[i:]
表示移除第一个元素,L[:i]
表示仅取前i
个元素:
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:-j]
表示移除前i个元素和最后j个元素。
这里是一个例子:
L = ['C', 'l', 'o', 'u', 'd', 's']
L[-2:] # ['d', 's']
L[:-2] # ['C', 'l', 'o', 'u']
在范围中省略一个索引对应于ℝ中的半开区间。半开区间(∞, a)表示取所有严格小于a的数;这类似于语法L[:j]
;更多示例请参见图 3.1:
图 3.1:一些典型的切片情况
请注意,越界切片时,你永远不会遇到索引错误。可能你会得到空列表:
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]))
结果是3
,0
,1
,3
,而你期望的结果是0
,0
,1
,3
。
让我们总结一下切片的使用:
-
L[i:]
表示取所有元素,除了前i个。 -
L[:i]
表示取前i个元素。 -
L[-i:]
表示取列表的最后i个元素。 -
L[:-i]
表示取所有元素,除了最后i个。
步长
在计算切片时,你也可以指定步长,即从一个索引到另一个索引的步长。默认步长为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]
另外,你也可以使用方法reverse
,该方法在第 3.1.4 节:列表方法中有详细说明。
3.1.2 修改列表
列表的典型操作是插入和删除元素以及列表连接。使用切片表示法,列表的插入和删除变得非常直观;删除就是用空列表[]
替换列表的一部分:
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]
列表上没有算术运算,比如逐元素求和或除法。对于这些操作,我们使用数组;请参见第 3.2 节:数组概念简览。
3.1.3 属于列表
你可以使用关键字in
和not in
来确定一个元素是否属于列表,这类似于数学中的https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/240418bd-550d-4d6d-a5f3-501370f5490d.png和https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/c905a7b8-fd14-4009-9804-7be9fe23cad1.png:
L = ['a', 1, 'b', 2]
'a' in L # True
3 in L # False
4 not in L # True
3.1.4 列表方法
一些有用的list
类型方法汇集在以下表 3.1中:
命令 | 操作 |
---|---|
list.append(x) | 将x 添加到列表的末尾。 |
list.extend(L) | 将列表L 的元素添加到列表末尾。 |
list.insert(i,x) | 将x 插入到位置i 。 |
list.remove(x) | 移除列表中第一个值为x 的元素。 |
list.sort() | 对列表项进行排序。 |
list.reverse() | 反转列表中的元素。 |
list.pop() | 移除列表中的最后一个元素。 |
表 3.1:列表数据类型的原地方法
这些方法是原地操作,即它们直接修改列表。
其他方法,如表 3.2中给出的那些,不会修改列表,而是返回一些信息或创建一个新列表对象:
命令 | 操作 |
---|---|
list.count(x) | 计算x 在列表中出现的次数。 |
list.copy() | 创建列表的副本。 |
表 3.2:返回新对象的列表数据类型方法
原地操作
大多数生成列表的方法都是原地操作。这些操作直接改变 Python 对象,而不会创建相同类型的新对象。通过查看以下示例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 代码。但原地操作会返回None
,并改变列表。因此,例如,像使用newL
作为(排序后的)列表那样,
print(newL[0])
会导致错误:
TypeError: 'NoneType' object is not subscriptable
在这里,我们展示了原地列表操作:
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
3.1.5 合并列表 – zip
一个特别有用的列表函数是zip
。它可以将两个给定的列表通过配对原列表中的元素合并成一个新列表。结果是一个元组列表(请参见第 3.3 节:元组):
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.3 节:可迭代对象。
3.1.6 列表推导式
构建列表的便捷方法是使用列表推导式,可能还带有条件。列表推导式的语法如下:
[<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]
这在处理数组时尤其重要;详见第 3.2 节:快速了解数组概念。
列表推导式与集合的数学表示法紧密相关。比较![]和L2 = [2*x for x in L]
。不过一个很大的区别是,列表是有序的,而集合不是;详见第 3.5 节:集合。
在完成对列表的理解后,我们将继续下一节,学习数组的相关内容。
3.2 快速了解数组的概念
NumPy 包提供了数组,它们是用于操作数学中的向量、矩阵或甚至更高阶张量的容器结构。在本节中,我们指出了数组与列表之间的相似性。但数组值得更广泛的介绍,这将在第四章:线性代数——数组,以及第五章:高级数组概念中详细讲解。
数组是通过函数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
-
数组只存储相同数值类型的元素(通常是
float
或complex
,也可以是int
)。 -
运算符
+
、*
、/
和-
都是逐元素操作。函数dot
以及在 Python 版本≥3.5 中,使用中缀运算符@
来进行标量积和相应的矩阵操作。 -
与列表不同,数组没有
append
方法。尽管如此,有一些特殊方法可以通过堆叠较小的数组来构造数组;参见第 4.7 节:堆叠。一个相关的点是,数组不像列表那样具有弹性;你不能使用切片来改变它们的长度。 -
向量切片是视图,即它们可以用来修改原始数组;参见第 5.1 节:数组视图与副本。
在本节中,我们快速了解了容器类型array
。它在科学计算中非常重要,以至于我们将专门用两章内容来详细讲解,它还将涉及更多的方面;参见第四章:线性代数——数组,以及第五章:高级数组概念。
3.3 元组
元组是不可变的列表。不可变意味着它不能被修改。元组是由逗号分隔的对象序列(没有括号的列表)。为了增加可读性,通常将元组括在一对圆括号中:
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
省略圆括号可能会产生副作用;请看下面的示例:
1, 2 == 3, 4 # returns (1, False, 4)
(1, 2) == (3, 4) # returns False
逗号表示该对象是一个元组:
singleton = 1, # note the comma
len(singleton) # 1
singleton = (1,) # this creates the same tuple
元组在一组值需要一起使用时很有用;例如,它们用于从函数返回多个值。参见第 7.3 节:返回值。
3.3.1 打包与解包变量
你可以通过解包列表或元组来一次性赋值多个变量:
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
3.4 字典
列表、元组和数组是有序的对象集合。单独的对象根据它们在列表中的位置被插入、访问和处理。另一方面,字典是无序的键值对集合。你通过键来访问字典数据。
3.4.1 创建和修改字典
例如,我们可以创建一个包含机械学中刚体数据的字典,如下所示:
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.2 节:参数和实参)。
字典中的键可以是字符串、函数、包含不可变元素的元组以及类等。键不能是列表或数组。
dict
命令从包含键/值对的列表中生成字典:
truck_wheel = dict([('name','wheel'),('mass',5.7),
('Ix',20.0), ('Iy',1.), ('Iz',17.),
('center of mass',[0.,0.,0.])])
zip
函数在此情况下可能会很有用;见第 3.15 节:合并列表–zip。
3.4.2 遍历字典
遍历字典的方式主要有三种:
- 通过键:
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.values():
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),...
请参阅第 14.4 节:架子,了解用于文件访问的特殊字典对象。
3.5 集合
本节介绍的最后一个容器对象由数据类型set
定义。
集合是与数学集合共享属性和操作的容器。数学集合是由不同对象组成的集合。就像数学中一样,在 Python 中,集合的元素也被列在一对大括号内。
这里有一些数学集合表达式:
这里是它们的 Python 对应项:
A = {1,2,3,4}
B = {5}
C = A.union(B) # returns{1,2,3,4,5}
D = A.intersection(C) # returns {1,2,3,4}
E = C.difference(A) # returns {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 中的集合可以包含所有类型的不可变对象,即数字对象、字符串和布尔值。
存在union
和intersection
方法,分别对应数学中的并集和交集操作:
A={1,2,3,4}
A.union({5})
A.intersection({2,4,6}) # returns {2, 4}
此外,集合可以使用issubset
和issuperset
方法进行比较:
{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.6 容器转换
我们在以下表 3.3中总结了迄今为止介绍的容器类型的最重要属性。(数组将在第四章:线性代数–数组中单独讨论):
类型 | 访问方式 | 顺序 | 重复值 | 可变性 |
---|---|---|---|---|
列表 | 通过索引 | 是 | 是 | 是 |
元组 | 通过索引 | 是 | 是 | 否 |
字典 | 通过键 | 否 | 是 | 是 |
集合 | 否 | 否 | 否 | 是 |
表 3.3: 容器类型
正如你在前面的表格中看到的,访问容器元素的方式是有区别的,集合和字典是无序的。
由于各种容器类型的属性不同,我们经常将一种类型转换为另一种类型(见 表 3.4):
容器类型 | 语法 |
---|---|
列表 → 元组 | tuple([1, 2, 3]) |
元组 → 列表 | list((1, 2, 3)) |
列表,元组 → 集合 | set([1, 2]), set((1, )) |
集合 → 列表 | list({1, 2 ,3}) |
字典 → 列表 | {'a':4}.values() |
列表 → 字典 | - |
表 3.4: 容器类型转换规则
在本节中,我们了解了如何转换容器类型。在第二章:变量与基本类型,我们了解了如何转换更基础的数据类型,例如数字。所以,现在是时候考虑如何实际检查一个变量的数据类型,这将是下一节的主题。
3.7 检查变量类型
查看变量类型的直接方式是使用命令 type
:
label = 'local error'
type(label) # returns str
x = [1, 2] # list
type(x) # returns list
然而,如果你想检查一个变量是否属于某种类型,应该使用 isinstance
(而不是使用 type
比较类型):
isinstance(x, list) # True
使用 isinstance
的理由在阅读过第 8.5 节:子类化与继承后变得更加明显。简而言之,不同的类型往往与某个基础类型共享一些共同的属性。经典的例子是 bool
类型,它是通过从更通用的 int
类型继承得到的。在这种情况下,我们看到如何更通用地使用 isinstance
命令:
test = True
isinstance(test, bool) # True
isinstance(test, int) # True
type(test) == int # False
type(test) == bool # True
因此,为了确保变量 test
可以像整数一样使用——尽管具体类型可能无关紧要——你应该检查它是否是 int
的实例:
if isinstance(test, int): print("The variable is an integer")
Python 不是一种强类型语言。这意味着对象的识别依据其功能,而不是其类型。例如,如果你有一个字符串操作函数,该函数通过使用 len
方法作用于一个对象,那么你的函数可能对任何实现了 len
方法的对象都有效。
到目前为止,我们已经遇到了不同的数据类型:float
,int
,bool
,complex
,list
,tuple
,module
,function
,str
,dict
和 array
。
3.8 总结
在这一章中,你学习了如何使用容器类型,主要是列表。了解如何填充这些容器以及如何访问和管理其中的内容非常重要。我们看到,访问方式可以是通过位置或通过关键字。
我们将在下一章的数组部分再次遇到切片这一重要概念。这些是专门为数学运算设计的容器。
3.9 练习
例 1: 执行以下语句:
L = [1, 2]
L3 = 3*L
-
L3
的内容是什么? -
尝试预测以下命令的结果:
L3[0]
L3[-1]
L3[10]
- 以下命令的作用是什么?
L4 = [k**2 for k in L3]
- 将
L3
和L4
连接成一个新的列表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: 考虑递归公式:
-
创建一个列表
u
。将其前面三个元素分别存储为三个值 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/921ed357-eca6-4286-ace3-ea396515a5c5.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/232fc9b2-0542-4858-843d-d4e57c224f47.png。这些值表示给定公式中的起始值 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/e5832afc-9afc-46ae-b065-53b9711fd5b9.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0683b5d5-4b9d-4362-8a66-35245b0bc4a7.png。根据递归公式构建完整的列表。 -
构建第二个列表
td
,将值存储为 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/4256cd7e-e93c-49c4-9bc6-1404ee654be4.png,其中 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/2c943450-f803-4e9c-b44d-66af7f791edd.png。绘制td
与u
的图(见 第 6.1 节:绘制图表)。再绘制第二张图,展示差异,即 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/13138cdc-1cc2-4ce8-9f80-d82f7a7c630c.png,其中 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/325d4dc1-fe3d-4d4c-907c-f8aa912e0b37.png 表示td
向量中的值。设置轴标签和标题。
递归是一个多步公式,用于求解具有初始值 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/75f4fafd-e7c9-405b-9cf0-a5263202aa3d.png 的微分方程 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/bddef92b-5e38-4de4-a136-f9b278e2e06a.png。
![] 近似于 ![]。
Ex. 6: 假设 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/2f193357-daa9-41f6-a179-e4971501a234.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/b785da32-e046-4761-8745-9258b57b0bf8.png 是集合。集合 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/59f36140-6831-4e59-a86c-031dc8f9cb56.png 被称为这两个集合的对称差。编写一个函数来执行此操作。将你的结果与以下命令的结果进行比较:
A.symmetric_difference(B).
Ex. 7: 在 Python 中验证空集合是任何集合的子集这一说法。
Ex. 8: 研究集合的其他操作。你可以使用 IPython
的命令自动补全功能找到这些操作的完整列表。特别是,研究 update
和 intersection_update
方法。intersection
和 intersection_update
有什么区别?
线性代数 - 数组
线性代数是计算数学中的一个基本组成部分。线性代数的对象是向量和矩阵。NumPy 包包含了处理这些对象所需的所有工具。
第一个任务是构建矩阵和向量,或通过切片修改现有的矩阵和向量。另一个主要任务是点积运算,它包含了大多数线性代数运算(标量积、矩阵-向量积和矩阵-矩阵积)。最后,提供了多种方法来解决线性问题。
本章节将涵盖以下主题:
-
数组类型概述
-
数学预备知识
-
数组类型
-
访问数组元素
-
构造数组的函数
-
访问和更改形状
-
堆叠
-
对数组进行操作的函数
-
SciPy 中的线性代数方法
第五章:4.1 数组类型概述
对于急于了解的读者,以下是如何使用数组的简要介绍。不过需要注意的是,数组的行为一开始可能会让人感到惊讶,因此我们建议在阅读完本介绍部分后继续阅读。
再次提醒,本章节的呈现方式假设你已经导入了 NumPy 模块,正如本书其他地方所假设的那样:
from numpy import *
通过导入 NumPy,我们可以访问数据类型ndarray
,将在接下来的章节中进行描述。
4.1.1 向量和矩阵
创建向量就像使用array
函数将一个列表转换为数组一样简单:
v = array([1.,2.,3.])
对象v
现在是一个向量,表现得很像线性代数中的向量。我们已经在第 3.2 节中强调了它与 Python 中的列表对象的区别:快速了解数组的概念。
这里是一些对向量进行基本线性代数运算的示例:
# 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 numpy.linalg import norm
norm(v1) # 3.7416573867739413
# scalar product
dot(v1, v2) # 5.0
v1 @ v2 # 5.0 ; 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])
本主题将在第 4.8 节中讲解:对数组进行操作的函数。
矩阵的创建与向量类似,只是它是从列表的列表中创建的:
M = array([[1.,2],[0.,1]])
请注意,向量并不是列矩阵或行矩阵。一个https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/dd804db6-ce44-4feb-b8b6-359bc63442df.png向量、一个https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/75c70cc7-6946-4e16-af4b-c6ba929f7142.png,以及一个https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/6f7b05e6-6f92-4cef-831d-790496c52a3b.png矩阵是三种不同的对象,即使它们包含相同的数据。
要创建一个行矩阵,包含与向量v = array([1., 2., 1.])
相同的数据,我们应用reshape
方法:
R = v.reshape((1,3))
shape(R) # (1,3): this is a row matrix
对应的列矩阵通过reshape
以相应的方式获得:
C = v.reshape((3, 1))
shape(C) # (3,1): this is a column matrix
在学习了如何创建数组并看到基本的数组操作后,我们现在将学习如何通过索引和切片来访问数组元素和子数组。
4.1.2 索引和切片
索引和切片与列表中的对应操作类似。主要区别在于,当数组是矩阵时,可能会有多个索引或切片。该主题将在第 4.4.1 节:基本数组切片中深入讨论;在这里,我们仅提供一些索引和切片的示例:
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!
由于数组是所有计算线性代数任务的基本数据类型,本文本节将展示一些示例、点积及线性方程组的解法。
4.1.3 线性代数操作
执行大多数常见线性代数操作的关键运算符是 Python 函数dot
。它用于矩阵-向量乘法(有关详细信息,请参阅第 4.2.4 节:点积操作):
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
求解线性系统
如果![]是矩阵,![]是向量,你可以求解线性方程组
使用线性代数子模块numpy.linalg
中的solve
函数:
from numpy.linalg import solve
x = solve(A, b)
例如,求解
执行以下 Python 语句:
from numpy.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
。可以选择设置容差值。有关与线性方程组相关的更多方法,请参阅第 4.9 节:SciPy 中的线性代数方法。
现在,你已经看到了在 Python 中使用数组的第一种基本方式。在接下来的章节中,我们将向你展示更多细节及其基本原理。
4.2 数学基础
为了理解数组在 NumPy 中的工作原理,了解通过索引访问张量(矩阵和向量)元素与通过提供参数评估数学函数之间的数学关系非常有用。我们还将在本节中介绍点积作为归约算子的推广。
4.2.1 将数组视为函数
数组可以从多个不同的角度进行考虑。如果你希望从数学角度理解这一概念,可能会通过将数组类比为多个变量的函数来获益。这个视角将在后续讲解广播概念时再次提到,第 5.5 节:广播。
例如,选择给定向量中的一个分量,在https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0827edf6-8019-496b-87da-a611639c6ab4.png中可能被视为从https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/49fb25fc-995c-4a1c-b7db-fb0ca393e72e.png到https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/cab4aa7c-b61f-459e-afa8-71cd5baa66f1.png的函数,其中我们定义集合:
在这里,集合 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/9ff103aa-3603-4365-9d45-818b85a2c38d.png 有 n 个元素。Python 函数 range
生成 ![]。
另一方面,选择一个给定矩阵的元素是一个有两个参数的函数,其值域为 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/2dd26863-2ac9-4c48-83ef-25700ef810e6.png。从一个 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/07d6e38b-b49b-4337-8925-160b771356ab.png 矩阵中选择特定元素,因此可以看作是一个从 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/5398c6ee-de44-43c1-b7fb-7c30dbfe6117.png 到 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/f0d6533e-221c-4d66-8138-a00f24e830e0.png 的函数。
4.2.2 操作是逐元素的
NumPy 数组本质上被当作数学函数来处理。特别是对于操作来说是这样。考虑两个定义在同一域并且取实值的函数,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/4cdad02e-e6f4-4c17-95c7-bfd5cf329282.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/6acb3ba5-69f9-4970-a5f4-90f7281ac936.png。这两个函数的乘积 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/8bd32329-08fb-4861-b05a-64b90be30405.png 被定义为逐点乘积,即:
请注意,这种构造对于两个函数之间的任何操作都是可能的。对于一个在两个标量之间定义的任意操作,我们这里用 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/17a32615-8ff1-488f-b7df-8fc015e28be2.png 表示,可以将 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/4e03099d-ba8c-47a9-865e-a675a56e4e02.png 定义如下:
这一看似无害的言论让我们理解了 NumPy 对操作的立场;所有操作在数组中都是逐元素的。例如,两个矩阵之间的乘积,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/498ba54b-8eb4-40c7-9b03-4ff15b67a00d.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/2aa90bb5-7dbc-4b46-9e63-053f3c934bcf.png,就像函数一样,其定义如下:
4.2.3 形状和维度数
这里有一个明确的区分:
-
标量:没有参数的函数
-
向量:一个具有一个参数的函数
-
矩阵:具有两个参数的函数
-
高阶张量:具有两个以上参数的函数
以下内容中,维度数是一个函数的参数个数。形状本质上对应于一个函数的定义域。
例如,一个大小为 n 的向量是一个从集合 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/3e022549-3df7-4296-89f7-f07c4a254201.png 到 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/67c574fe-7309-413d-bc56-5dbfee30661e.png 的函数。因此,它的定义域是 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/914d84e0-9c6a-4895-9e11-c42f794a770a.png。它的形状定义为单例 (n,)。类似地,大小为 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/61fcaba3-7269-4d3d-b042-734057d63e0b.png 的矩阵是一个定义在 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/cbf9a605-eaf9-45d2-843e-ee7498361ef6.png 上的函数。相应的形状就是一对 (m, n)。数组的形状由函数 numpy.shape
获取,维度数由函数 numpy.ndim
获取;请参见 第 4.6 节:访问和更改形状。
4.2.4 点积操作
将数组视为函数,虽然非常强大,但完全忽略了我们熟悉的线性代数结构,即矩阵-向量和矩阵-矩阵操作。幸运的是,这些线性代数操作可以都写成类似的统一形式:
向量-向量操作:
矩阵-向量操作:
矩阵-矩阵操作:
向量-矩阵操作:
本质的数学概念是“约简”(reduction)。对于矩阵-向量操作,约简由以下公式给出:
通常情况下,在两个张量之间定义的约简操作,分别是 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/1f0c151e-39a4-4e33-b6cf-23789680aa6e.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/8016ef96-8baa-48ac-80e0-56016e765985.png,它们的维度分别是 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/58f0e0a7-a065-40a0-82b0-d8c685294696.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/cfd803fd-209a-4fa1-82cc-b2fa4237e1ac.png,可以定义为:
显然,张量的形状必须与该操作兼容才能产生
这要求对于矩阵-矩阵乘法也很熟悉。乘法 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/e5b3c2c8-d223-4b63-9afe-67c3946130b2.png
矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/40c5c7dc-0cc9-4c09-b130-5ed416713e2c.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/2ed6b962-1034-42c4-bcd7-9f6df445ef53.png 之间的操作,只有当 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/3f6cc52f-5279-45b4-ab25-c779292f6c32.png 的列数等于 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/4fc01d29-fe44-4681-8146-b4b1da231ed4.png 的行数时才有意义。
约简操作的另一个结果是它生成了一个具有 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/907b8b22-fae5-42f9-bff7-b0381719d8fa.png 维度的新张量。在表 4.1中,我们收集了涉及矩阵和向量的约简操作输出:
表 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
。从现在起,我们将坚持使用更方便的中缀符号;如果需要其他形式,您可以修改示例。然而,我们需要注意的是,dot
会在其参数类型为其他可以转换为数组的类型(如列表或浮点数)时执行类型转换。而中缀运算符@
则不具有这个特性。
乘法运算符*
始终是逐元素的。它与点积操作无关。即使*https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/04ede9d1-0823-4b24-a5e7-fc247edcc0fa.png*是矩阵,且https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/8d8c60e1-cd74-45d6-9ec6-1798e72a0077.png*是向量,A*v
仍然是合法的操作。这将在第 5.5 节中解释:广播。
在本节中,我们介绍了在数学中使用数组与矩阵和向量的结合,并解释了相关的操作。特别地,我们解释了科学计算中最核心的操作——点积。接下来,我们将转向数组数据类型ndarray
及其更一般的方法。
4.3 数组类型
用于操作向量、矩阵以及更一般张量的对象在 NumPy 中被称为 ndarray,简称数组。在本节中,我们将探讨它们的基本属性、如何创建它们以及如何访问其信息。
4.3.1 数组属性
数组本质上由三个属性来表征,这些属性在表 4.2中进行了描述。
名称 | 描述 |
---|---|
shape | 该属性描述数据应如何解释,例如作为向量、矩阵或更高阶张量,并给出相应的维度。可以通过shape 属性访问该值。 |
dtype | 该属性给出基础数据的类型(如浮点数、复数、整数等)。 |
strides | 此属性指定数据应该如何读取。例如,一个矩阵可以按照列顺序(FORTRAN 约定)或行顺序(C 约定)连续存储在内存中。该属性是一个元组,包含到达下一行和下一列时需要跳过的字节数。它甚至允许对内存中的数据进行更灵活的解释,这也使得数组视图成为可能。 |
表 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
属性中。
4.3.2 从列表创建数组
创建数组的一般方法是使用 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 # int64
V = array([1., 2]) # [1., 2] mix float/integer
V.dtype # float64
V = array([1\. + 0j, 2.]) # mix float/complex
V.dtype # complex128
NumPy 会默默地将浮点数转换为整数,这可能会导致意外的结果:
a = array([1, 2, 3])
a[0] = 0.5
a # now: array([0, 2, 3])
同样,常见的、通常是意外的数组类型转换发生在从 complex
到 float
之间。
数组和 Python 括号
如我们在 第 1.2.2 节:行连接 中注意到,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
到目前为止,你已经看到数组和列表在定义和使用上的许多区别。相比之下,访问数组元素似乎与访问列表元素非常相似。但尤其是多个索引的使用以及切片操作结果的对象,需要我们更加详细地研究这些问题。
4.4 访问数组条目
数组条目通过索引访问。与向量系数不同,访问矩阵系数需要两个索引。这些索引放在一对括号中。这使得数组语法与列表的列表有所区别。在后者中,需要两对括号来访问元素。
M = array([[1., 2.],[3., 4.]])
M[0, 0] # first row, first column: 1.0
M[-1, 0] # last row, first column: 3.0
现在我们更详细地来看一下双重索引和切片的使用。
4.4.1 基本数组切片
切片与列表的切片类似(另见 第 3.1.1 节:切片),不过它们现在可能存在多个维度:
-
M[i,:]
是由行 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/60a8e43a-37e5-406e-bad5-90981a04b955.png 填充的向量,来自 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/c0e17bfb-4fea-4289-9f4f-582181cc5699.png. -
M[:,j]
是由列填充的向量 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/47def328-ef21-466c-8046-1574defb7ec6.png 来自 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/450c6166-8464-42a9-b6c1-acce375705c0.png. -
M[2:4,:]
是对行的2:4
切片。 -
M[2:4,1:4]
是行和列的切片。
矩阵切片的结果见 图 4.1:
图 4.1:矩阵切片的结果
如果省略索引或切片,NumPy 会假定你只是在取行。M[3]
是一个向量,它是对 M 的第三行的视图,而 M[1:3]
是一个矩阵,它是对 M 的第二行和第三行的视图:
修改切片的元素会影响整个数组(另见 第 5.1 节:数组视图和副本):
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:
访问 | ndim | 类型 |
---|---|---|
索引, 索引 | 0 | 标量 |
切片, 索引 | 1 | 向量 |
索引, 切片 | 1 | 向量 |
切片, 切片 | 2 | 矩阵 |
表 4.3:一般切片规则
数组 M
的切片操作结果(形状为 (4, 4))见 表 4.4:
访问 | 形状 | 维度 | 类型 |
---|---|---|---|
M[:2, 1:-1] | (2,2) | 2 | 矩阵 |
M[1,:] | (4,) | 1 | 向量 |
M[1,1] | () | 0 | 标量 |
M[1:2,:] | (1,4) | 2 | 矩阵 |
M[1:2, 1:2] | (1,1) | 2 | 矩阵 |
表 4.4:形状为 (4,4) 的数组 M 的切片操作结果
4.4.2 使用切片更改数组
你可以使用切片或直接访问来更改数组。以下示例仅更改一个元素,位于 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/b44e53a3-95ce-43a3-a6b4-3616878d891c.png 矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/c54c7a2c-59a0-4e88-bb60-28e29b2a1852.png 中:
M[1, 2] = 2.0 # scalar
同样,我们也可以更改矩阵中的一整行:
M[2, :] = [1., 2., 3.] # vector
同样,我们也可以替换整个子矩阵:
M[1:3, :] = array([[1., 2., 3.],[-1.,-2., -3.]])
列矩阵和向量之间是有区别的。以下使用列矩阵的赋值不会报错:
M[1:4, 1:2] = array([[1.],[0.],[-1.0]])
而使用向量赋值时会返回 ValueError
:
M[1:4, 1:2] = array([1., 0., -1.0]) # error
一般的切片规则如 表 4.3 所示。前面的矩阵和向量必须具有适当的大小,以适应矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/b75a4632-4ef1-495c-891e-1184dfa2bc58.png。你还可以使用广播规则(参见 第 5.5 节:广播)来确定替换数组的允许大小。如果替换数组的形状不正确,将引发 ValueError
异常。
我们已经看到如何通过切片从其他数组中构造数组。在下一节中,我们将考虑一些直接创建和初始化数组的特殊 NumPy 函数。
4.5 构造数组的函数
设置数组的常见方法是通过列表。但也有一些方便的方法用于生成特殊数组,这些方法在 表 4.5 中给出:
方法 | 形状 | 生成的结果 |
---|---|---|
zeros((n,m)) | (n,m) | 填充了 0 的矩阵 |
ones((n,m)) | (n,m) | 填充了 1 的矩阵 |
full((n,m),q) | (n,m) | 填充了 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/d4907979-e755-4827-ae0b-318783930a2d.png 的矩阵 |
diag(v,k) | (n,n) | 来自向量的(下、上)对角矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/9cb240a7-8fe3-4f7a-862a-2057841ca009.png |
random.rand(n,m) | (n,m) | 填充了均匀分布的随机数(在 (0,1) 之间)的矩阵 |
arange(n) | (n,) | 前 n 个整数 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/cacd22ff-75a8-40a9-ad1d-b8e06d0c8dd9.png |
linspace(a,b,n) | (n,) | 向量,包含在 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/76197639-ef19-4b09-89f0-b6702c4638f1.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/a68eb689-1673-4372-a195-a071d550b333.png 之间均匀分布的 n 个点 |
表 4.5:创建数组的命令
这些命令可能会接受额外的参数。特别地,命令 zeros
、ones
、full
和 arange
接受 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.]])
4.6 访问和改变形状
维度数是区分向量和矩阵的标志。形状 是区分不同大小的向量或矩阵的标志。在这一节中,我们将研究如何获取和改变数组的形状。
4.6.1 shape
函数
矩阵的形状是其维度的元组。一个 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/116a1156-6428-4645-83b5-91a371d0ac37.png 矩阵的形状是元组 (n, m)
。可以通过 shape
函数获得:
M = identity(3)
shape(M) # (3, 3)
或者,通过其属性来简单获取
M.shape # (3, 3)
然而,使用 shape
作为函数而不是属性的优点在于,函数也可以用于标量和列表。这在代码需要同时处理标量和数组时非常有用:
shape(1.) # ()
shape([1,2]) # (2,)
shape([[1,2]]) # (1,2)
对于一个向量,形状是一个包含该向量长度的单一元素:
v = array([1., 2., 1., 4.])
shape(v) # (4,) <- singleton (1-tuple)
4.6.2 维度数
数组的维度可以通过 ndim
函数或数组的 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
4.6.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
reshape
对由 arange(6)
定义的数组的各种影响如 图 4.2 所示:
图 4.2:reshape
对数组的各种影响
reshape
不会创建一个新数组。它只是为现有数组提供一个新的视图。在前面的示例中,修改 M
的一个元素会自动导致 v
中相应元素的变化。当这种行为不可接受时,你需要复制数据,如在第 5.1 节中解释的那样:数组视图与副本。
如果你尝试重新塑形一个数组,而其形状不能与原始形状相乘,则会抛出错误:
ValueError: total size of new array must be unchanged.
有时候,指定一个 shape
参数并让 Python 自动计算出另一个参数,使得它们的乘积等于原始形状是很方便的。这可以通过将自由的 shape
参数设置为 -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
转置
重新塑形的一种特殊形式是 转置。它仅交换矩阵的两个形状元素。矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/01c5013f-d771-4398-8b4a-09a7127faf37.png 的转置是矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/1e85d89b-68ed-4ce3-afcb-def575a94660.png,使得
这将通过以下方式解决:
A = ...
shape(A) # (3,4)
B = A.T # A transpose
shape(B) # (4,3)
transpose
不会复制:转置与重新塑形非常相似,尤其是它也不复制数据,而是仅返回同一数组的视图:
A= array([[ 1., 2.],[ 3., 4.]])
B=A.T
A[1,1]=5\.
B[1,1] # 5.0
转置一个向量没有意义,因为向量是一个一维的张量,也就是一个单变量的函数——索引。然而,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
4.7 堆叠
从一对(匹配的)子矩阵构建矩阵的通用方法是concatenate
。它的语法是:
concatenate((a1, a2, ...), axis = 0)
当指定axis=0
时,这个命令会将子矩阵垂直堆叠(一个在另一个之上)。使用axis=1
参数时,它们会水平堆叠,这个操作会根据更高维度的数组进行泛化。这个函数通过多个方便的函数来调用,如下所示:
-
hstack
:用于水平堆叠数组 -
vstack
:用于垂直堆叠数组 -
columnstack
:用于将向量堆叠成列
4.7.1 向量堆叠
你可以使用vstack
和column_stack
按行或按列堆叠向量,如图 4.3所示:
图 4.3:vstack 和 column_stack 的区别
注意,hstack
将会产生v1
和v2
的拼接。
让我们以辛普森排列为向量堆叠的例子:我们有一个大小为https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/95e7f926-6570-4979-aff3-cfd2456400df.png的向量。我们想对具有偶数个分量的向量执行辛普森变换,即将向量的前半部分与后半部分交换,并且改变符号:
这个操作在 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]])
4.8 对数组的函数
数组上有不同类型的函数。有些是逐元素作用的,它们返回一个形状相同的数组,这些被称为通用函数。其他数组函数返回形状不同的数组。在本节中,我们将接触这两种类型的函数,并学习如何将标量函数转换为通用函数。
4.8.1 通用函数
通用函数是对数组逐元素作用的函数。因此,它们的输出数组与输入数组具有相同的形状。这些函数允许我们一次性计算标量函数在整个数组上的结果。
内建通用函数
一个典型的例子是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([2, 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
函数应用于一个向量 [a, b]
时,应该返回 [heaviside(*a*), heaviside(*b*)]
。遗憾的是,这并不奏效,因为该函数总是返回一个标量,无论输入参数的大小如何。此外,使用数组输入时,if
语句会引发异常,具体细节可参见 第 5.2.1 节:布尔数组。
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])
图 4.4 显示了结果图:
图 4.4:Heaviside 函数
函数 vectorize
提供了一种方便的方式,可以快速地将一个函数转换,使其逐元素作用于列表和数组。
vectorize
也可以作为装饰器使用:
@vectorize
def heaviside(x):
if x >= 0:
return 1\.
else:
return 0\.
# and a call of this results in:
heaviside(array([-1, 2])) # array([0, 1])
装饰器将在 第 7.8 节 中介绍:作为装饰器的函数。
4.8.2 数组函数
有一些作用于数组的函数,并不是逐元素作用的。这些函数的例子包括 max
、min
和 sum
。这些函数可以作用于整个矩阵、按行作用或按列作用。当没有提供参数时,它们会作用于整个矩阵。
假设:
对该矩阵应用的 sum
函数返回一个标量:
sum(A) # 36
该命令有一个可选参数 axis
。它允许我们选择沿哪个轴执行操作。例如,如果轴是 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/34738052-9fa9-47b9-82ff-83a13e4cff45.png,意味着应该沿第一个轴计算和。沿轴 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/b3c35afb-1602-49a5-af3a-b4f4a92bb398.png 对形状为 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/925c52a4-d0cd-400d-a844-1f5ff7e56e72.png 的数组求和,将得到一个长度为 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0ae89473-603a-4d7f-a6e1-002309711489.png 的向量。
假设我们计算 A
沿轴 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/fb5a95be-3141-40c3-bb6c-4fa00dbd0586.png 的和:
sum(A, axis=0) # array([ 6, 8, 10, 12])
这相当于计算列上的和:
结果是一个向量:
现在假设我们计算沿轴 1 的和:
A.sum(axis=1) # array([10, 26])
这相当于计算行上的和:
结果是一个向量:
在本节中,我们已经介绍了作用于数组的函数,接下来我们将转向解决基础科学计算任务的函数。我们通过考虑一些线性代数中的标准任务来举例说明。
4.9 SciPy 中的线性代数方法
SciPy 提供了一系列数值线性代数方法,这些方法在其模块 scipy.linalg
中。许多这些方法是 Python 包装的 LAPACK
程序,LAPACK 是一组广泛认可的 FORTRAN 子程序,用于解决线性方程组和特征值问题,详见 [5]。线性代数方法是科学计算中任何方法的核心,SciPy 使用包装器而非纯 Python 代码使得这些核心方法极其快速。我们在这里详细展示了如何通过 Scipy 解决两个线性代数问题,旨在让你对该模块有所了解。
你之前接触过一些来自 numpy.linalg
模块的线性代数函数。NumPy 和 SciPy 两个包是兼容的,但 Scipy 更侧重于科学计算方法,并且功能更加全面,而 NumPy 更侧重于数组数据类型,仅提供了一些便捷的线性代数方法。
4.9.1 使用 LU 解多个线性方程组
设 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/4196e6f5-3ee7-4824-9a06-1e2a0023f791.png 是一个 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/6e3bf0cd-1219-43c9-8e6e-80888770ba90.png 矩阵,且 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/919aa858-0c1e-4349-929f-6cab888543b5.png 是一系列 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/aec0a0b8-00cd-4635-9c1c-0686cbc24cc0.png 向量。我们考虑求解问题,找到 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/143b692e-7046-4f9d-b164-fe5cd9effa12.png 向量 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/67cb9bbc-afe6-4faa-849b-1d68041b2802.png,使得:
我们假设向量 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/85efadf5-99b6-44b4-a962-833b8b3d8355.png 不是同时已知的。特别地,通常情况下,必须先解决 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/871fa756-0080-460c-92c0-fd64f972467f.png^(th) 问题,然后才能获得 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0282095b-b6ca-4e70-8a8a-dce30fc06d5f.png,例如在简化的牛顿迭代法中,详见 [24]。
https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/85bac675-3f92-4601-af52-ca94f39eef12.png 因式分解是一种组织经典高斯消元法的方法,能够将计算分为两步进行:
-
矩阵的因式分解步骤 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/00bdf963-c2e4-44f6-b55f-d79be2f3f4d9.png,目的是将矩阵转换为三角形形式
-
一种相对廉价的向后和向前消元步骤,作用于 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/efd18f9b-201c-4d88-a885-7fe6cc05eec3.png 的实例,并且受益于更耗时的因式分解步骤
该方法还利用了这样一个事实:如果 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/cecba16b-7cc5-4ca7-a597-a6c22ccac977.png 是一个置换矩阵,使得 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/99dfac8f-563a-4c50-98cd-e977f9ac85b5.png 是原矩阵的行经过置换后的矩阵,那么两个系统 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/b6467921-e799-4a7c-87d1-df158307b3d2.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/2d88dcdc-57aa-4e10-9cdb-494fbec51d25.png 具有相同的解。
https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/4578b253-5ead-4eef-9412-b4e3ea7439f6.png 因式分解找到一个置换矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/6c293ce8-4881-4f96-a225-cc7dfa04a769.png,一个下三角矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/44e8c59b-3b02-4ad4-9363-2b8a92b506fe.png,以及一个上三角矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/98ffc1b2-a1c7-45ba-9c92-71043ecb2d05.png,使得:
https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/caaf71e0-07ba-4fc0-80e0-cb79f1286d6c.png 或等价地 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/beaa3fdf-6087-4498-8883-f8d9797efc06.png。
这种因式分解总是存在的。此外,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/3d8a0cf3-0fba-48ef-992d-4654fb86a225.png 可以以 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/6bc9dce9-f3fd-46c4-a547-553c7e51e6a7.png 的方式确定。因此,来自 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/35f23021-e8c1-4326-b7e9-7909b1e2f02f.png 的核心数据必须存储为 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/9f65052f-5fc6-451c-acc0-509bb256677c.png,同时 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/9e90e974-9f89-492b-a3e8-3e8376d73586.png 被保留。于是,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/2b0e1170-2606-4e74-88b1-2c0681f6076c.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/8d7fe4fb-c2fb-4d10-9f76-b4ffb42ae6c3.png 可以存储在一个 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/46e6348b-3f0b-442e-8b32-6c724c4ea4f5.png 数组中,而关于置换矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0bdf51ae-0e08-4759-8a69-fe3479b39ccb.png 的信息只需要一个 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/d0f432d4-6869-4b90-9b12-31e8f12db89b.png 整数向量——即主元向量。
在 SciPy 中,有两种方法可以计算 LU 因式分解。标准的方法是 scipy.linalg.lu
,它返回三个矩阵 L
、U
和 P
。另一种方法是 lu_factor
。我们在这里描述的是这种方法,因为它将方便地与 lu_solve
结合使用:
import scipy.linalg as sl
[LU,piv] = sl.lu_factor(A)
在这里,矩阵 A
被分解,并返回一个包含 L
和 U
信息的数组,同时返回主元向量。有了这些信息,通过根据主元向量中的信息对向量 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/153577e0-6458-4c62-92a0-8cf350510081.png 进行行交换,再通过使用 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/96b762f1-848b-444d-a841-7d0d2766a23c.png 的回代替换,最后使用 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/db5a6831-edd9-4a37-bb7e-8284f8a66aef.png 的前代替换,即可求解系统。这在 Python 中被打包为 lu_solve
方法。下面的代码片段展示了如何在执行 LU 因式分解并将其结果存储在元组 (LU, piv) 中后,解决系统 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/a8a9ac9f-b68e-45f0-a555-b8e489316cd5.png:
import scipy.linalg as sl
xi = sl.lu_solve((LU, piv), bi)
4.9.2 使用 SVD 求解最小二乘问题
一个线性方程组 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/81eb24c9-baf8-4329-a16e-e721ec9262e1.png,其中 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/92a1bee6-65ee-4def-b0a6-8ed17278ef02.png 为 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/1d73bf3f-58a2-414e-9d7d-1afb6720205a.png 矩阵,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0fd80a0a-c81b-47b2-8e56-e2d1858c1e2a.png,称为过度确定的线性系统。通常,它没有经典解,你需要寻找一个向量 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/6d0da559-17ca-4da7-bf6f-c8fc0c776aae.png,满足以下性质:
这里,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/cbba8bf7-fee6-4d90-bdda-74de6bd48875.png 表示欧几里得向量范数 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/1b83e74f-85d8-4b81-9f8c-03be5257c74e.png。
这个问题被称为最小二乘问题。解决此问题的稳定方法是基于对 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/b6070459-10bf-46cd-8a81-79da90e4677d.png, 进行分解,其中 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/b2199c3b-3baf-4d99-b440-5a7f8128e122.png 是一个 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/9f894183-c602-4e45-acd2-1a5b1dfa8e57.png 正交矩阵,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/321ee887-ee37-47ad-88e9-f433f47452eb.png 是一个 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/ec15b505-3dc6-454e-8ac0-432c1126e3c5.png 正交矩阵,而 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/9b49437c-733d-43d8-a853-e62eeeb21ea9.png 是一个具有属性 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/408d1afa-77dd-400a-9f84-938372b59921.png 的矩阵,对于所有的 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/a93a7e22-86c4-451e-b37c-70929ccdb519.png。这种分解被称为 奇异值分解(SVD)。
我们写作:
使用对角线 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/482f5dde-34d4-431c-9b78-7922c29f02c6.png 矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/214f9d14-1fe5-4917-bf5f-903e54fc58fe.png。如果我们假设 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/00bda032-6d12-4c96-bf4f-35e863ba8b90.png 是满秩的,那么 ![] 是可逆的,并且可以证明:
成立。
如果我们将 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/871828cc-22c0-46f1-837c-68393d365c37.png 分割,其中 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/5ef1ec2f-a76a-4523-bfcb-990bf31f162f.png 是一个 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0a7fbf6f-dae4-4e00-8074-6c9453a683e9.png 子矩阵,那么前面的方程可以简化为:
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
指示是计算完整矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/9e72bfce-f08d-4f8a-a8b1-1ffef56ddffa.png 还是仅计算其子矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/51d29c20-bb37-4615-b3eb-5f131e0ed12b.png。由于你经常使用 svd
只计算奇异值,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0ff12784-516f-4184-9447-e2016fa22484.png,在我们的情况下,我们需要明确要求计算 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0be678a3-b8e0-4f25-88c3-38878a4d6a97.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/e209beca-7321-416e-a764-1645933a0536.png,这可以通过使用关键字 compute_uv
来实现。
SciPy 函数 scipy.linalg.lstsq
通过内部使用 SVD 来直接求解最小二乘问题。
4.9.3 更多方法
到目前为止的例子中,你遇到了一些用于线性代数计算任务的方法,例如 solve
。执行命令 import scipy.linalg as sl
后,可以使用更多的方法。最常用的方法列在 表 4.6 中:
方法 | 描述 |
---|---|
sl.det | 矩阵的行列式 |
sl.eig | 矩阵的特征值和特征向量 |
sl.inv | 矩阵的逆 |
sl.pinv | 矩阵伪逆 |
sl.norm | 矩阵或向量的范数 |
sl.svd | 奇异值分解 |
sl.lu | LU 分解 |
sl.qr | QR 分解 |
sl.cholesky | Cholesky 分解 |
sl.solve | 一般或对称线性系统的解:Ax = b |
sl.solve.banded | 带状矩阵的解法 |
sl.lstsq | 最小二乘解 |
表 4.6:scipy.linalg 模块的线性代数函数
首先执行 import scipy.linalg as sl
。
4.10 总结
在本章中,我们处理了线性代数中最重要的对象——向量和矩阵。为此,我们学习了如何定义数组,并掌握了重要的数组方法。一个较小的部分展示了如何使用 scipy.linalg
中的模块来解决线性代数中的核心任务。
在接下来的一章中,我们将考虑数组的更高级和特殊方面。
4.11 练习
Ex. 1: 考虑一个 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/94b86ce2-e6b2-475b-9f14-76c5e36c9e19.png 矩阵:
-
使用函数
array
在 Python 中构造此矩阵。 -
使用函数
arange
和适当的reshape
构造相同的矩阵。 -
表达式
M[2,:]
的结果是什么?类似表达式M[2:]
的结果是什么?
Ex. 2: 给定一个向量 x,用 Python 构造如下矩阵:
这里,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/6b2583e7-6896-4d67-964d-fd049538ba7f.png 是向量 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/18d85501-1ef0-49b3-8a5b-9e48a139c592.png 的分量(从零开始编号)。给定向量 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/219de5ca-b350-487d-aa10-0ad36e6b5f28.png,用 Python 解决线性方程组 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/ebed84dd-d1d0-4f9e-a484-605bfc06c9bb.png。让 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/03a227be-9acb-460e-9c61-af0cd3bd9232.png 的分量用 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/7e32640f-ea55-4d20-95c8-cb2b8f653841.png 表示。编写一个函数 poly
,其输入为 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/2b36f4f3-1189-45e2-bb5a-a0425f127e30.png 和 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/782f3b31-642b-4427-8c2a-4ed1be231cd2.png,计算多项式:
绘制该多项式,并在同一图中将点 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/2f31ef49-1465-45fe-8d17-22e1a9ebb089.png 表示为小星号。使用以下向量测试你的代码:
Ex. 3: 矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/bcb237c4-8b52-4760-b9f7-a53906c9378f.png 在 Ex. 2 中称为范德蒙矩阵。可以直接使用命令 vander
在 Python 中设置它。用 Python 命令 polyval
评估由系数向量定义的多项式。使用这些命令重复 Ex. 2。
Ex. 4: 设 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/ee99a147-92c3-476d-a39a-a74ad9c0985c.png 是一个一维数组。构造另一个数组 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/290f6f75-cb1d-4a2f-8ff2-0a8e1a7921d0.png,其值为 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/6b23c9cd-6a6c-45c1-a2a7-fd6205634028.png。在统计学中,这个数组被称为 移动平均值。在逼近理论中,它扮演了三次样条函数的 Greville 点的角色。尝试在你的脚本中避免使用for
循环。
Ex. 5:
-
从矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/7178b185-4f57-411e-ab36-1878955e902b.png (见Ex. 2)构造一个矩阵 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/2df3ed42-0a6a-41a5-a792-3ea9d7c739c0.png,删除其中 https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0c7572b8-ae60-4056-bc23-b7755d8fd266.png 的第一列。
-
计算https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/9f1f2ce4-d0bc-4b76-90e6-a007bf3de393.png,并使用Ex. 2中的y。
-
使用https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/c3627f5c-ffb0-472c-adcd-349774cbc08b.png和
polyval
绘制由https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/6914b439-8e00-43a4-9d05-cf710057f952.png定义的多项式。再次在同一图中绘制点https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/fa578596-8b6d-4e5d-8d45-3444e2a19f77.png。
例 6: 例 5 描述了最小二乘法。重复该练习,但改用 SciPy 的scipy.linalg.lstsq
方法。
例 7: 设https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/9c8c6479-a5f2-468d-89ff-ebba2e920bf6.png是一个向量,以其坐标形式写成https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/dfb4ade8-dd92-4337-8bbb-69ad3e591092.png矩阵https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/d5202a4a-76e0-49b2-aa6a-a3700a41331e.png。构造投影矩阵:
https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/a37ac33c-9332-43af-88a3-44340ef9916e.png和https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/23ac98e4-77db-423f-8ce7-456009c627ba.png
实验表明,https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/83a4e7b1-68fc-49e9-b334-210006ce7cf4.png是矩阵https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/92b532e7-074a-4860-aa1c-7f8c7173159e.png和https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/0bc7f0fb-df10-4658-b18d-928f7a13cc76.png的特征向量。相应的特征值是多少?
例 8: 在数值线性代数中,*https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/44464ed2-4f61-4367-8619-39608a28d1e7.png*矩阵https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/5f13f90a-e7a8-4775-a9e7-b9d2228e5ca4.png具有以下性质
被用作极端增长因子的例子,在执行 LU 分解时,见[36, p. 165]。
在 Python 中设置这个矩阵,对于不同的https://github.com/OpenDocCN/freelearn-ds-pt3-zh/raw/master/docs/sci-comp-py-2e/img/a92b5fed-a950-43f8-ac67-7f39e1606d55.png值,使用命令scipy.linalg.lu
计算其 LU 分解,并通过实验得出关于增长因子的陈述。