什么是 NumPy?#
NumPy 是 Python 中科学计算的基础软件包。它是一个 Python 库,提供多维数组对象、各种派生对象(例如掩码数组和矩阵),以及用于快速执行数组运算的各种例程,包括数学运算、逻辑运算、形状操作、排序、选择、I/O、离散傅里叶变换、基本线性代数、基本统计运算、随机模拟等等。
NumPy 包的核心是 ndarray
对象。它封装了同构数据类型的 *n* 维数组,许多操作都在编译代码中执行以提高性能。NumPy 数组与标准 Python 序列之间存在一些重要区别。
NumPy 数组在创建时具有固定大小,不像 Python 列表(可以动态增长)。更改
ndarray
的大小将创建一个新数组并删除原始数组。NumPy 数组中的所有元素都必须是相同的数据类型,因此在内存中大小相同。例外情况:可以拥有(Python,包括 NumPy)对象的数组,从而允许不同大小元素的数组。
NumPy 数组有助于对大量数据执行高级数学和其他类型的运算。通常,与使用 Python 的内置序列相比,这些运算的执行效率更高,代码更少。
越来越多的基于 Python 的科学和数学软件包正在使用 NumPy 数组;尽管这些软件包通常支持 Python 序列输入,但它们会在处理之前将此类输入转换为 NumPy 数组,并且它们通常输出 NumPy 数组。换句话说,为了有效地使用当今许多(也许甚至是大多数)基于 Python 的科学/数学软件,仅仅知道如何使用 Python 的内置序列类型是不够的——还需要知道如何使用 NumPy 数组。
关于序列大小和速度的要点在科学计算中尤为重要。作为一个简单的例子,考虑将一维序列中的每个元素与另一个相同长度的序列中的相应元素相乘的情况。如果数据存储在两个 Python 列表 a
和 b
中,我们可以遍历每个元素
c = []
for i in range(len(a)):
c.append(a[i]*b[i])
这会产生正确的答案,但是如果 a
和 b
各包含数百万个数字,我们将为 Python 中循环的低效率付出代价。我们可以通过编写 C 代码来更快地完成相同的任务(为清晰起见,我们忽略变量声明和初始化、内存分配等)
for (i = 0; i < rows; i++) {
c[i] = a[i]*b[i];
}
这节省了所有解释 Python 代码和操作 Python 对象的开销,但以牺牲使用 Python 编码获得的好处为代价。此外,所需的编码工作会随着我们数据的维数而增加。例如,在二维数组的情况下,C 代码(如前所述)扩展为
for (i = 0; i < rows; i++) {
for (j = 0; j < columns; j++) {
c[i][j] = a[i][j]*b[i][j];
}
}
NumPy 为我们提供了两全其美的优势:当涉及 ndarray
时,逐元素运算为“默认模式”,但逐元素运算由预编译的 C 代码快速执行。在 NumPy 中
c = a * b
执行了前面示例的操作,速度接近 C,但具有我们期望的基于 Python 的代码简洁性。事实上,NumPy 的习惯用法甚至更简单!最后一个示例说明了 NumPy 的两个特性,它们是其强大功能的基础:矢量化和广播。
为什么 NumPy 速度很快?#
矢量化描述了代码中任何显式循环、索引等的缺失——当然,这些事情正在发生,只是在优化的预编译 C 代码中“幕后”进行。矢量化代码有很多优点,其中包括
矢量化代码更简洁,更容易阅读
代码行数越少,通常意味着错误越少
代码更接近标准数学符号(通常使正确编码数学结构更容易)
矢量化会导致更“Pythonic”的代码。如果没有矢量化,我们的代码将充斥着低效且难以阅读的
for
循环。
广播是用于描述运算的隐式逐元素行为的术语;一般来说,在 NumPy 中,所有运算,不仅是算术运算,而且逻辑运算、按位运算、函数运算等,都以这种隐式逐元素的方式进行,即它们进行广播。此外,在上面的示例中,a
和 b
可以是相同形状的多维数组,或者是一个标量和一个数组,甚至是两个形状不同的数组,前提是较小的数组可以“扩展”到较大数组的形状,使得结果广播是明确的。有关广播的详细“规则”,请参阅 广播。
还有谁在使用 NumPy?#
NumPy 完全支持面向对象的方法,再次从 ndarray
开始。例如,ndarray
是一个类,拥有许多方法和属性。它的许多方法都由最外层 NumPy 命名空间中的函数镜像,允许程序员使用他们喜欢的任何范例进行编码。这种灵活性使得 NumPy 数组方言和 NumPy ndarray
类成为 Python 中使用的多维数据交换的 *事实上的* 语言。