什么是 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 中使用的多维数据交换的 *事实上的* 语言。