1.加减乘除幂
NumPy 数组之间的加减乘除和幂运算默认执行元素级 (element-wise) 操作。这意味着对应位置的元素会进行相应的数学运算,并返回一个新的 ndarray
。这些操作是 NumPy 向量化能力的核心体现。
加法 (+
): arr1 + arr2
,对应位置的元素相加。
减法 (-
): arr1 - arr2
,对应位置的元素相减。
乘法 (\
): arr1 * arr2
,对应位置的元素相乘。这被称为元素级乘法 (element-wise multiplication) 或 Hadamard 乘积。它与线性代数中的矩阵乘法(@
运算符或 np.dot()
)不同。
除法 (/
): arr1 / arr2
,对应位置的元素相除。结果的数据类型通常会提升为浮点数,以保留小数部分。
幂运算 (**
): arr1 ** arr2
,对应位置的 arr1
元素作为底数,arr2
元素作为指数进行幂运算。
(1)形状兼容性 (Shape Compatibility) 与广播 (Broadcasting)
进行元素级运算的前提是数组的形状兼容。最简单的情况是两个数组的形状完全相同。如果形状不同,NumPy 会尝试应用其广播 (Broadcasting) 机制。广播是一种在不同形状的数组之间执行算术运算的强大功能,它会虚拟地扩展较小数组的维度,使其形状与较大数组兼容,而无需实际复制数据。
广播规则简述:详细参考博客numpy的广播机制
NumPy 的高性能得益于其底层使用 C 或 Fortran 等编译型语言实现。当执行
arr1 + arr2
这样的操作时:
- 类型检查和广播规则应用: NumPy 会首先检查两个数组的
dtype
和形状,并根据广播规则确定最终的输出形状和dtype
。- 循环优化: 实际的元素级运算是在底层 C 代码中高效完成的,而不是在 Python 解释器中进行循环。这意味着 NumPy 避免了 Python 循环的开销(如每次迭代时的类型检查、对象创建等)。
- SIMD (Single Instruction, Multiple Data) 指令: 现代 CPU 支持 SIMD 指令集(如 SSE, AVX)。NumPy 的底层实现能够利用这些指令,一次性对多个数据点执行相同的操作,进一步加速计算。
- 内存连续性:
ndarray
的数据在内存中是连续存储的,这使得 CPU 能够高效地访问数据,利用缓存,并为 SIMD 操作提供理想的条件。因此,NumPy 的元素级运算远比 Python 列表的循环操作快得多,是进行大规模数值计算的首选。
2.逻辑运算
NumPy 数组之间的逻辑运算(比较运算)也是元素级的,它们会比较对应位置的元素,并返回一个相同形状的布尔类型 (bool
) 数组,其中包含 True
或 False
。
- 小于 (
<
):arr1 < arr2
,对应位置的arr1
元素是否小于arr2
元素。 - 小于等于 (
<=
):arr1 <= arr2
。 - 大于 (
>
):arr1 > arr2
。 - 大于等于 (
>=
):arr1 >= arr2
。 - 等于 (
==
):arr1 == arr2
,对应位置的元素是否相等。 - 不等于 (
!=
):arr1 != arr2
。
布尔数组在 NumPy 中非常强大,它们常用于布尔索引 (Boolean Indexing) 或布尔掩码 (Boolean Masking),以从数组中选择满足特定条件的元素。
- 语法:
arr[boolean_array]
- 原理: 当使用布尔数组作为索引时,NumPy 会遍历布尔数组,只选择对应位置为
True
的元素。这会返回一个一维数组,其中包含所有满足条件的元素。
(1)数组与标量比较
1
2
3
arr1 =np.array([1, 2, 3, 4, 5])
arr1 < 5 # array([ True, True, True, True, False])
arr1 == 5 # array([False, False, False, False, True])
(2)数组与数组的比较
1
2
3
4
arr1 =np.array([1, 2, 3, 4, 5])
arr2 =np.array([1, 0, 3, 4, 5])
arr1 > arr2 # array([False, True, False, False, False])
arr1 == arr2 # array([ True, False, True, True, True])
大于等于同理
(3)布尔索引
1
2
3
4
5
6
7
8
9
10
11
data_array = np.array([10, 25, 5, 40, 100])
# 筛选出大于20的元素(掩码)
data_array_bigger_20_mask = data_array > 20 # array([False, True, False, True, True]) --->也是掩码
# 筛选出大于20的元素
data_array_bigger_20 = data_array[data_array_bigger_20_mask] # array([ 25, 40, 100])
# 筛选偶数(掩码)
data_array_even_mask = data_array % 2 == 0 # array([ True, False, False, True, True])
# 筛选偶数
data_array_even = data_array[data_array_even_mask] # array([ 10, 40, 100])
(4)与或非
1
2
3
4
data_array = np.array([10, 25, 5, 100, 53, 13])
# 筛选出大于10 小于30的奇数
mask_ = (data_array > 10) & (data_array < 30) & (data_array % 2 == 1) # array([False, True, False, False, False, True])
filtered = data_array[mask_] # array([25, 13])
- 与
&
- 或
|
- 非
~
3.数组与标量的计算
当一个 NumPy 数组与一个标量(单个数值)进行算术或逻辑运算时,NumPy 会自动将该标量值“传播”到数组的每一个元素,然后执行元素级运算。这是一种特殊的广播形式,非常高效。
数组与标量之间的运算是 NumPy 中最常见且最直观的向量化操作之一。它极大地简化了代码,避免了显式循环。
- 算术运算:
arr + scalar
:数组的每个元素都加上标量。arr - scalar
:数组的每个元素都减去标量。arr * scalar
:数组的每个元素都乘以标量。arr / scalar
:数组的每个元素都除以标量。arr ** scalar
:数组的每个元素都进行标量次幂运算。scalar / arr
:标量除以数组的每个元素。
- 逻辑运算:
arr < scalar
:数组的每个元素是否小于标量。arr == scalar
:数组的每个元素是否等于标量。- 等等。
数组与标量的计算是广播机制的一个最简单但又非常强大的应用。
- 标量扩展: 在内部,NumPy 会将标量视为一个与数组形状相同的“虚拟”数组,其中所有元素都等于该标量值。这个过程是逻辑上的,并没有实际创建新的内存副本。
- 元素级操作: 然后,就像两个数组之间的元素级运算一样,NumPy 的底层 C/Fortran 代码会高效地遍历原始数组的元素,并将每个元素与这个“虚拟”标量数组中对应位置的值进行运算。
- 性能优势: 这种机制避免了显式的 Python 循环,并且能够利用底层优化(如 SIMD 指令),从而实现极高的计算效率。它比手动循环遍历数组并进行操作快几个数量级。
这种“标量广播”是 NumPy 能够实现简洁且高性能代码的关键特性之一。
4.复合赋值运算符
*=
、+=
、-=
、/=
等复合赋值运算符在 NumPy 中执行原地 (in-place) 操作。这意味着它们会直接修改现有数组的内容,而不是创建一个新的数组并将其赋值给原变量。这通常比非原地操作更高效,尤其是在处理大型数组时,因为它避免了额外的内存分配和数据复制。
在 Python 中,a = a + b
和 a += b
之间存在一个细微但重要的区别。对于不可变类型(如整数、字符串、元组),两者效果相同,都会创建新对象。但对于可变类型(如列表),+=
通常是原地操作。在 NumPy 中,这种区别更加显著。
- 非原地操作 (Out-of-place Operations):
- 示例:
arr_new = arr1 + arr2
或arr_new = arr * 5
- 行为: 这些操作会创建一个全新的
ndarray
来存储结果,并将结果返回。原始数组arr1
或arr
保持不变。 - 内存消耗: 需要额外的内存来存储新的结果数组。
- 示例:
- 原地操作 (In-place Operations / Compound Assignment):
- 示例:
arr1 += 5
,arr1 *= arr2
,arr1 /= 2
- 行为: 这些操作会直接修改
arr1
数组的底层数据,将运算结果存储回arr1
所在的内存位置。它们不返回新的数组(虽然表达式本身会返回对修改后数组的引用)。 - 内存消耗: 不会分配新的内存用于存储结果数组,因此在处理大型数组时可以显著节省内存,并提高性能。
- 示例:
数据类型转换的注意事项:
- 整数除法 (
/=
) 的陷阱:- 当对整数类型的
ndarray
执行arr /= scalar
或arr1 /= arr2
时,NumPy 可能会尝试将结果转换为浮点数。 - 如果原始数组的
dtype
是整数类型(如int32
,int64
),而除法的结果是浮点数,NumPy 无法原地将整数类型数组转换为浮点数类型。这会导致TypeError
或DeprecationWarning
(取决于 NumPy 版本和具体操作)。 - 解决方案: 在执行除法前,将数组的数据类型显式转换为浮点数类型(例如使用
arr.astype(np.float32)
或arr.astype(np.float64)
),或者确保数组一开始就是浮点数类型。
- 当对整数类型的
原地操作的性能优势和内存效率来源于其对底层内存的直接操作。
- 内存地址不变: 当执行
arr += 5
时,NumPy 不会为arr += 5
的结果分配新的内存块。相反,它会直接访问arr
数组所指向的内存区域。- 直接修改数据: 底层的 C/Fortran 例程会遍历
arr
的每个元素,执行加 5 的操作,并将结果直接写回到该元素原来的内存位置。- 避免复制开销: 这种方式避免了创建新数组所需的内存分配(可能导致碎片化)和数据复制的开销。对于包含数百万甚至数十亿元素的数组,这种优化至关重要。
dtype
转换限制: 原地操作要求结果的数据类型能够被原始数组的dtype
容纳。例如,一个int32
数组可以原地执行+=
操作,只要结果仍在int32
的范围内。但如果int32
数组执行除法,结果需要浮点数精度,而int32
无法存储浮点数,因此无法原地完成,必须创建新数组或先进行类型转换。NumPy 会在编译时或运行时检查这种类型兼容性。理解原地操作对于编写高效和内存友好的 NumPy 代码至关重要。