基础知识

ndarray 对象介绍#

NumPy 的主要对象是同构的多维数组。它是一个同类型元素(通常是数字)构成的表,由非负整数元组索引。NumPy 中的维度被称为(axes)。

例如,3D 空间中的点坐标 [1, 2, 1] 具有一个轴。该轴有 3 个元素,因此我们说它的长度为 3。在如下示例中,数组有 2 个轴。第一轴的长度为 2,第二轴的长度为 3。

[[ 1., 0., 0.],
 [ 0., 1., 2.]]

NumPy 的数组类被称作 ndarray,它具有别名 array。请注意,numpy.array 与 Python 标准库中的 array.array 类并不相同,后者只处理一维数组并提供较少的功能。

以下列出了 ndarray 对象的一些重要属性:

  • ndarray.ndim:数组的轴(维度)的个数。
  • ndarray.shape:数组的维度。这是一个整数元组,表示每个维度中数组的大小。对于有 n 行和 m 列的矩阵,shape(n,m)。因此,shape 元组的长度就是轴的个数 ndim
  • ndarray.size:数组元素的总数。这等于 shape 中各元素的乘积。
  • ndarray.dtype:一个描述数组中元素类型的对象。可以使用标准的 Python 类型创建或指定 dtype。另外 NumPy 也提供它自己的类型,如 numpy.int32numpy.int16numpy.float64
  • ndarray.itemsize:数组中每个元素占用的字节数。例如,元素为 float64 类型的数组的 itemsize 为 8(=64/8),而 complex32 类型的数组的 itemsize 为 4(=32/8)。该值与 ndarray.dtype.itemsize 相等。
  • ndarray.data:包含实际数组元素的缓冲区。由于可以通过索引机制访问数组中的元素,因此通常不需要使用此属性。

一个例子:

import numpy as np

# arange() 类似于 Python 内建的 range(), 但返回 ndarray 对象;
# reshape() 将 ndarray 对象变为特定维度的对象,并返回该 ndarray 对象
a = np.arange(15).reshape(3, 5)

print(a)
# [[ 0  1  2  3  4]
#  [ 5  6  7  8  9]
#  [10 11 12 13 14]]

print(a.shape)  # (3, 5)
print(a.ndim)  # 2
print(a.dtype.name)  # int64
print(a.itemsize)  # 4
print(a.size)  # 15
print(type(a))  # <class 'numpy.ndarray'>

b = np.array([6, 7, 8])
print(b)  # [6 7 8]
print(type(b))  # <class 'numpy.ndarray'>

创建数组#

有多种创建数组的方法,请查看创建数组函数文档了解详细信息。

① 使用 array 函数从常规的 Python 列表或元组创建数组

numpy.array(object, dtype=None, *, copy=True, 
    order='K', subok=False, ndmin=0, like=None)
import numpy as np

a = np.array([2, 3, 4])
print(a)  # [2 3 4]
print(a.dtype)  # int32
b = np.array([1.2, 3.5, 5.1])
print(b.dtype)  # float64

所得数组元素的类型是从序列元素的类型自动推导得出的。

使用 array 函数的一个常见错误是在调用时为其提供了多个参数,而不是提供单个序列参数:

import numpy as np

a = np.array(1, 2, 3, 4)  # 错误
# TypeError: array() takes from 1 to 2 positional arguments but 4 were given

a = np.array([1, 2, 3, 4])  # 正确
a = np.array((1, 2, 3, 4))  # 正确

array 函数可以将序列的序列转换为二维数组,将序列的序列的序列转换为三维数值,依次类推;还可以在 array 的参数中显式指定元素类型:

import numpy as np

b = np.array([(1.5, 2, 3), (4, 5, 6)])
print(b)
# [[1.5 2.  3. ]
#  [4.  5.  6. ]]

c = np.array([[1, 2], [3, 4]], dtype=complex)
print(c)
# [[1.+0.j 2.+0.j]
#  [3.+0.j 4.+0.j]]

② 用特定函数创建具有特定尺寸和初始元素值的数组

  • zeros:通过给定形状和类型创建数组,并用 0 填充元素值。

    numpy.zeros(shape, dtype=float, order='C', *, like=None)
    
  • ones:通过给定形状和类型创建数组,并用 1 填充元素值。

    numpy.ones(shape, dtype=None, order='C', *, like=None)
    
  • empty:通过给定形状和类型创建数组,同时不初始化元素值。

    numpy.empty(shape, dtype=float, order='C', *, like=None)
    
  • full:创建一个具有给定形状和类型的数组,并用给定的 fill_value 值填充各元素。

    numpy.full(shape, fill_value, dtype=None, order='C', *, like=None)
    
import numpy as np

print(np.zeros((3, 4)))  # 不指定 dtype 值时,默认类型是 float64
# [[0. 0. 0. 0.]
#  [0. 0. 0. 0.]
#  [0. 0. 0. 0.]]
print(np.ones((2, 3, 4), dtype=np.int16))  # 显式指定 dtype 值
# [[[1 1 1 1]
#   [1 1 1 1]
#   [1 1 1 1]]

#  [[1 1 1 1]
#   [1 1 1 1]
#   [1 1 1 1]]]
print(np.empty((2, 3)))
# [[1.39069238e-309 1.39069238e-309 1.39069238e-309]
#  [1.39069238e-309 1.39069238e-309 1.39069238e-309]]

③ 使用 arangelinspace 函数创建数组

arange 函数与 Python 内建的 range 函数类似,但返回 ndarray 对象。

numpy.arange([start, ]stop, [step, ]dtype=None, *, like=None)
import numpy as np

print(np.arange(10, 30, 5))  # [10 15 20 25]
print(np.arange(0, 2, 0.3))  # [0.  0.3 0.6 0.9 1.2 1.5 1.8]

arange 与浮点参数一起使用时,由于有限的浮点精度,通常不可能预测所获得的元素的数量。出于这个原因,通常最好使用 linspace 函数来显式指定我们想要的元素数量,而不是像 arange 函数那样指定步长:

numpy.linspace(start, stop, num=50, endpoint=True,
    retstep=False, dtype=None, axis=0)
import numpy as np

print(np.linspace(0, 2, 9))  # [0.   0.25 0.5  0.75 1.   1.25 1.5  1.75 2.  ]
print(np.linspace(0, 2 * np.pi, 30))
# [0.         0.21666156 0.43332312 0.64998469 0.86664625 1.08330781
#  1.29996937 1.51663094 1.7332925  1.94995406 2.16661562 2.38327719
#  2.59993875 2.81660031 3.03326187 3.24992343 3.466585   3.68324656
#  3.89990812 4.11656968 4.33323125 4.54989281 4.76655437 4.98321593
#  5.1998775  5.41653906 5.63320062 5.84986218 6.06652374 6.28318531]

还有更多的创建数组的函数。该页面把创建数组的函数分为多个分类:从形状或值创建、从已有数据创建、创建记录数组 numpy.rec、创建字符数组 numpy.char、数值范围、构建矩阵、矩阵类。

注意该页面许多函数具有 _like 后缀,如 zeros_likeones_likeempty_likefull_like,这些函数的功能和不带 _like 的函数类似,但可不必为其提供 shapedtype 参数,而可通过为其提供一个原型数组 a,从而创建与该原型具有相同形状和类型的数组。

实际创建数组时,应根据具体场景,从该页面选择使用合适的函数。

基本操作#

根据矢量化和广播规则,对数组的算术运算符会逐个应用到每个元素。下面是创建一个新数组并填充结果的示例:

import numpy as np

a = np.array([20, 30, 40, 50])
b = np.arange(4)
c = a - b
print(c)  # [20 29 38 47]
print(b**2)  # [0 1 4 9]
print(10 * np.sin(a))  # [ 9.12945251 -9.88031624  7.4511316  -2.62374854]
print(a < 35)  # [ True  True False False]

与许多矩阵运算语言不同,乘法运算符 * 在 NumPy 数组中是逐个元素进行。矩阵乘积可以使用 @ 运算符(对 Python >= 3.5)或 dot 函数或方法执行:

import numpy as np
A = np.array( [[1,1], [0,1]] )
B = np.array( [[2,0], [3,4]] )
print(A * B)
# [[2 0]
#  [0 4]]

print(A @ B)
print(A.dot(B))
print(np.dot(A, B))
# 结果均为:
# [[5 4]
#  [3 4]]

某些操作(例如 +=*=)会更直接更改被操作的矩阵数组而不会创建新矩阵数组。

import numpy as np

a = np.ones((2,3), dtype=int)
b = np.random.random((2,3))
a *= 3
print(a)
# [[3 3 3]
#  [3 3 3]]
b += a
print(b)
# [[3.42596777 3.14670933 3.10927589]
#  [3.64844792 3.381072   3.46206654]]
a += b  # b 的元素是浮点类型,无法被自动转换为 a 的整数类型
# numpy.core._exceptions._UFuncOutputCastingError: Cannot cast ufunc 'add' output from dtype('float64') to dtype('int32') with casting rule 'same_kind'

当对不同类型的数组进行操作时,会遵守向上转换的规则,即结果数组的类型将是参与运算的类型中更一般和更通用的类型。

import numpy as np

a = np.ones(3, dtype=np.int32)
b = np.linspace(0, np.pi, 3)
print(b.dtype.name)  # float64

c = a + b
print(c, c.dtype.name)  # [1.         2.57079633 4.14159265] float64

d = np.exp(c*1j)
print(d, d.dtype.name)  # [ 0.54030231+0.84147098j -0.84147098+0.54030231j -0.54030231-0.84147098j] complex128

许多一元运算,如计算数组中所有元素的和,都是作为 ndarray 类的方法实现的。

import numpy as np

rng = np.random.default_rng()  # 创建一个伪随机数生成器
a = rng.random((2, 3))
print(a)
# [[0.54981685 0.85835019 0.50779762]
#  [0.54727067 0.92390732 0.83417839]]

print(a.sum(), a.max(), a.min())
# 4.2213210430646395 0.9239073221612277 0.5077976241685102

默认情况下,适用于数字列表的运算也适用于任何形状的数组。不过,通过指定 axis 参数,您可以沿数组的指定轴进行运算:

import numpy as np

b = np.arange(12).reshape(3, 4)
print(b)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(b.sum(axis=0))  # 每列之和
# [12 15 18 21]

print(b.min(axis=1))  # 每行的最小值
# [0 4 8]

print(b.cumsum(axis=1))  # 沿每行的累加和
# [[ 0  1  3  6]
#  [ 4  9 15 22]
#  [ 8 17 27 38]]

通用函数#

NumPy 提供了一些我们原来已熟悉的数学函数,如 sincosexp。在 NumPy 中,这些被称为“通用函数”(ufunc),这些函数在数组上按元素逐个进行运算,产生一个数组作为输出。

import numpy as np

a = np.arange(3)
b = np.array([2., -1., 4.])
print(a)  # [0 1 2]

print(np.exp(a))  # [1.         2.71828183 7.3890561 ]
print(np.sqrt(a))  # [0.         1.         1.41421356]
print(np.add(a, b))  # [2. 0. 6.]

其他常见的通用函数包括:

all, any, apply_along_axis, argmax, argmin, argsort, average, bincount, ceil, clip, conj, corrcoef, cov, cross, cumprod, cumsum, diff, dot, floor, inner, invert, lexsort, max, maximum, mean, median, min, minimum, nonzero, outer, prod, re, round, sort, std, sum, trace, transpose, var, vdot, vectorize, where

索引、切分和迭代#

可以像对待 Python 序列一样对一维数组进行索引、切分和迭代。

import numpy as np

a = np.arange(10)**3
print(a)  # [  0   1   8  27  64 125 216 343 512 729]
print(a[2:5])  # [ 8 27 64]
# 等价于 a[0:6:2] = 1000,从开始到 6,将每第二个元素值设置为 1000
a[:6:2] = 1000
print(a)  # [1000    1 1000   27 1000  125  216  343  512  729]
print(a[::-1])  # a 的逆序数组
# [ 729  512  343  216  125 1000   27 1000    1 1000]
for e in a:
    print(e**(1 / 3.))
# 9.999999999999998
# 1.0
# 9.999999999999998
# 3.0
# 9.999999999999998
# 4.999999999999999
# 5.999999999999999
# 6.999999999999999
# 7.999999999999999
# 8.999999999999998

多维数组的每个轴都可以有一个索引,这些索引以逗号分隔的元组给出(注意这与 Python 的多维列表或元组的方法不同)。

import numpy as np

def f(x, y):
    return 10 * x + y

# fromfunction 通过针对每个元素执行一次函数而构建数组
b = np.fromfunction(f, (5, 4), dtype=int)
print(b)
# [[ 0  1  2  3]
#  [10 11 12 13]
#  [20 21 22 23]
#  [30 31 32 33]
#  [40 41 42 43]]

print(b[2, 3])  # 23

# 以下两行均打印 a 的所有列的第二行
print(b[0:5, 1])
print(b[:, 1])
# [ 1 11 21 31 41]

print(b[1:3, :])  # a 的第 2、3 行的所有列
# [[10 11 12 13]
#  [20 21 22 23]]

当提供的索引数少于轴的数量时,缺失的索引被认为是完整的切片:

print(b[-1])  # 最后一行,等价于 b[-1, :]
# [40 41 42 43]

b[i] 方括号中的表达式 i 被视为后面紧跟着足够多的 :,用于表示剩余轴。NumPy 也允许将省略的轴表示为三个点(不是省略号),如 b[i, ...]三个点...)代表生成完整的索引元组所需的冒号。

例如,假设 x 是一个具有 5 个轴的数组,则:

  • x[1, 2, ...] 等价于 x[1, 2, :, :, :]
  • x[..., 3] 等价于 x[:, :, :, :, 3]
  • x[4, ..., 5, :] 等价于 x[4, :, :, 5, :]

一个更复杂的示例:

import numpy as np

c = np.array([[[  0,  1,  2],  # 一个三维数组
        [ 10, 12, 13]],
        [[100, 101, 102],
        [110, 112, 113]]])

print(c.shape)  # (2, 2, 3)

print(c[1, ...])  # c[1, ...] 等价于 c[1, :, :] 或 c[1]
# [[100 101 102]
#  [110 112 113]]

print(c[..., 2])  # c[..., 2] 等价于 c[:, :, 2]
# [[  2  13]
#  [102 113]]

对多维数组进行迭代操作是针对第一个轴进行的:

import numpy as np
b = np.array([[ 0,  1,  2,  3],
      [10, 11, 12, 13],
      [20, 21, 22, 23],
      [30, 31, 32, 33],
      [40, 41, 42, 43]])
for row in b:
    print(row)
# [0 1 2 3]
# [10 11 12 13]
# [20 21 22 23]
# [30 31 32 33]
# [40 41 42 43]

如果想要对数组中的每个元素执行操作,可以使用 flat 属性,该属性是数组的所有元素的迭代器:

import numpy as np
b = np.array([[ 0,  1,  2,  3],
        [10, 11, 12, 13],
        [20, 21, 22, 23],
        [30, 31, 32, 33],
        [40, 41, 42, 43]])
for element in b.flat:
    print(element)
# 0
# 1
# 2
# 3
# 10
# 11
# 12
# 13
# 20
# 21
# 22
# 23
# 30
# 31
# 32
# 33
# 40
# 41
# 42
# 43
NumPy 介绍形状操作