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 数组。
序列大小和速度的问题在科学计算中尤为重要。举个简单的例子,考虑将一个 1D 序列中的每个元素与另一个相同长度序列中的对应元素相乘的情况。如果数据存储在两个 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 中编码所带来的好处。此外,所需的编码工作量随着我们数据的维度增加而增加。例如,对于 2D 数组,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 代码进行。矢量化代码有很多优点,其中包括:
矢量化代码更简洁易读
代码行数越少,通常意味着 bug 越少
代码更接近标准的数学表示法(通常更容易正确地编码数学结构)
矢量化产生了更“Pythonic”的代码。如果没有矢量化,我们的代码将充斥着效率低下且难以阅读的
for循环。
广播是指操作的隐式逐元素行为;总的来说,在 NumPy 中,所有操作,不仅是算术运算,还有逻辑运算、位运算、函数运算等,都以这种隐式的逐元素方式进行,即它们会广播。此外,在上面的示例中,a 和 b 可以是相同形状的多维数组,或者是一个标量和一个数组,甚至可以是形状不同的两个数组,前提是较小的数组可以“扩展”到较大数组的形状,并且由此产生的广播是明确无误的。有关广播的详细“规则”,请参阅 广播。
还有谁在使用 NumPy?#
NumPy 完全支持面向对象的方法,同样以 ndarray 开始。例如,ndarray 是一个类,拥有许多方法和属性。它的许多方法在最外层的 NumPy 命名空间中有相应的函数,允许程序员选择他们偏好的编程范式。这种灵活性使得 NumPy 数组方言和 NumPy ndarray 类成为 Python 中用于多维数据交换的事实上的标准语言。