什么是 NumPy?#

NumPy 包的核心是 ndarray 对象。它封装了同质数据类型的 \(n\) 维数组,并且许多操作都是通过编译代码来执行以提高性能。NumPy 数组与标准的 Python 序列之间有几个重要的区别:

  • NumPy 数组在创建时具有固定大小,这与可以动态增长的 Python list 不同。改变 ndarray 的大小将创建新的数组并删除原始数组。

  • NumPy 数组中的元素必须具有相同的数据类型,因此在内存中它们的大小相同。例外情况是:可以有包含 Python(包括 NumPy)对象的数组,从而允许数组包含不同大小的元素。

  • NumPy 数组便于对大量数据进行高级数学运算和其他类型的操作。通常,这些操作比使用 Python 内置序列执行得更快,并且代码更少。

越来越多的基于 Python 的科学和数学包正在使用 NumPy 数组;尽管这些包通常支持 Python 序列输入,但它们在处理之前会将这些输入转换为 NumPy 数组,并且它们通常输出 NumPy 数组。换句话说,为了有效地使用当今许多(甚至可能是大多数)基于 Python 的科学/数学软件,仅仅知道如何使用 Python 的内置序列类型是不够的——还需要知道如何使用 NumPy 数组。

关于序列大小和速度的要点在科学计算中尤为重要。作为一个简单的例子,考虑将一个一维序列中的每个元素与另一个相同长度的序列中的对应元素相乘的情况。如果数据存储在两个 Python 列表 ab 中,可以遍历每个元素:

c = []
for i in range(len(a)):
    c.append(a[i]*b[i])

这会得到正确的答案,但如果 ab 各自包含数百万个数字,将为 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 的两个特性,它们是其强大功能的基础:向量化(vectorization)和广播(broadcasting)。

为什么 NumPy 快?#

向量化(Vectorization)描述了代码中没有显式的循环、索引等操作——当然,这些操作仍然在进行,只是“幕后”通过优化过的预编译 C 代码执行。向量化代码有许多优点,其中包括:

  • 向量化代码更简洁且更易读

  • 更少的代码行通常意味着更少的错误

  • 代码更接近标准的数学表示法(通常使得编写正确的数学结构更容易)

  • 向量化使代码更“Pythonic”。如果没有向量化,代码将充斥着低效且难以阅读的 for 循环。

广播(Broadcasting)是用来描述操作中隐式逐元素行为的术语;一般来说,在 NumPy 中,所有操作(不仅仅是算术操作,还包括逻辑操作、位操作、函数操作等)都以这种隐式的逐元素方式进行,即它们会广播。此外,在上面的示例中,ab 可以是具有相同形状的多维数组,或者一个是标量,一个是数组,甚至可以是两个形状不同的数组,只要较小的数组能够通过某种方式扩展成较大数组的形状,从而使广播结果明确无误。关于广播的具体“规则”,请参阅广播

还有谁使用 NumPy?#

NumPy 完全支持面向对象的方法,再次从 ndarray 开始。例如,ndarray 是一个类,拥有众多方法和属性。它的许多方法在外层 NumPy 命名空间中都有对应的函数,允许程序员根据个人偏好选择编程范式。这种灵活性使得 NumPy 数组方言和 NumPy ndarray 类成为 Python 中多维数据交换的实际标准语言。