Python Numpy 的 View 与 Copy 使用详解与优化技巧
Python Numpy 的 View 与 Copy 使用详解与优化技巧
文章目录
- Python Numpy 的 View 与 Copy 使用详解与优化技巧
- 一 NumPy Array 和 Python List
- NumPy 与 List 的综合对比
- 二 NumPy 的 View 和 Copy
- 划重点
- 1 copy 和 view
- 2 操作 view 比 copy 快
- 3 “光速” 展平矩阵
- 三 优化一 数据选择提速
- 1 view 的方式
- 2 copy 的方式
- 四 优化二 替代 copy 操作
- 1 使用 `np.take()` 替代索引方式进行数据选择
- 2 使用 `np.compress()` 替代 `mask` 选择数据
- 五 优化三 利用 `out` 参数提升性能
- 划重点
- 六 完整代码示例
- 七 源码地址
本文深入探讨了 Python Numpy 中的
View
和
Copy
概念,并详细对比了它们的特性及应用场景。通过多个代码示例,展示了在不同情况下如何选择视图或副本来操作数据,以及它们对内存与计算性能的影响。此外,文章还提供了诸如使用
np.take()
、
np.compress()
以及
out
参数等优化技巧,帮助开发者提升数据选择与运算效率。这些技巧在处理大规模数据时尤其有效,能够显著提高 Numpy 的运算性能。
导入依赖库
import numpy as np
import timeit
from functools import partial
一 NumPy Array 和 Python List
图文来自 Why Python is Slow: Looking Under the Hood
NumPy 与 List 的综合对比
特性 | NumPy 数组 | Python 原生列表 |
---|---|---|
内存管理 | 数组在内存中连续存储,访问速度快 | 存储分散,访问速度相对较慢 |
数据类型 | 支持固定数据类型(如 int32 、float64 ),高效使用内存 | 支持多种数据类型,可能导致额外开销 |
运算性能 | 支持向量化运算,无需显式循环,计算速度快 | 通常需循环,效率较低 |
广播机制 | 支持不同形状数组之间的运算 | 不支持广播,运算需形状一致 |
功能丰富性 | 提供大量数学和统计函数,适合科学计算 | 功能较为简单,主要用于基本数据存储 |
维度支持 | 支持多维数组(ndarray),处理复杂数据结构 | 支持嵌套列表,操作不够高效 |
NumPy 数组在性能、功能和内存管理上优于 Python 原生列表,特别适合数值计算和大规模数据处理。
详细对比见 :Python NumPy 与 List 的性能对决:为何 NumPy 更胜一筹
二 NumPy 的 View 和 Copy
图文来自 SettingwithCopyWarning: How to Fix This Warning in Pandas
视图(View)和副本(Copy)特性对比
特性 | 视图 (View) | 副本 (Copy) |
---|---|---|
定义 | 视图是原数组的一个窗口,与原数组共享数据,只有结构被修改。 | 副本是原数组的一个完全独立的复制,拥有独立的内存。 |
创建方法 | 通过基本切片(如 a[1:4] )、np.reshape 、np.transpose 等操作,这些操作不会复制数据而只修改视图。 | 通过 .copy() 方法、np.copy() 函数以及通过整数索引和布尔索引(如 a[idx] 或 a[mask] )创建副本。 |
优点 | - 不需要额外内存,节省内存使用和计算资源 - 快速访问和修改数据 | - 修改副本不会影响原数组 - 可以安全地修改数据,适合数据隔离和保密 |
缺点 | - 修改视图会影响原数组 - 在不清晰的情况下可能会导致数据被意外修改 | - 需要额外的内存和计算资源来创建和维护副本 - 操作速度可能比视图慢 |
划重点
- 基本切片创建视图(View)。
- 整数数组索引和布尔索引创建副本(Copy)。
1 copy 和 view
在副本 (copy) 上修改数据 不会影响 源数据,而在视图 (view) 上修改数据 会影响 “窗口”内的源数据。
a = np.arange(1, 7).reshape((3, 2))print(a)a_view = a[:2]a_copy = a[:2].copy()print(a_view)print(a_copy)a_copy[1, 1] = 0print("在 copy 上修改数据,不会影响源数据:\n", a)a_view[1, 1] = 0print("在 view 上修改数据,会影响'窗里'的源数据:\n", a)
2 操作 view 比 copy 快
在视图 (view) 的操作中,处理速度通常比副本 (copy) 更快。通过以下代码可以展示这个差异:
def get_run_time(func, *args):repeat = 3number = 200return min(timeit.Timer(partial(func, *args)).repeat(repeat=repeat, number=number)) / numbera = np.random.rand(1000, 1000)
b = np.random.rand(1000, 1000)def f1():global b# 这会产生新的 bb = 2 * bdef f2():global a# 这不会产生新的 aa *= 2 # 和 a[:] *= 2 一样print('%.6f - b = 2*b' % get_run_time(f1))
print('%.6f - a *= 2' % get_run_time(f2))
运行结果如下:
0.000771 - b = 2*b
0.000238 - a *= 2
可以看到,视图操作 (a *= 2
) 的速度比副本操作 (b = 2*b
) 快了三倍以上。这是因为视图修改数据时不需要创建新的数组,而副本操作则会生成新的对象,导致额外的内存开销和处理时间。
3 “光速” 展平矩阵
在将矩阵展平时,np.flatten()
和 np.ravel()
都能完成相似的功能,但它们的底层机制有所不同:
flatten()
总是执行 复制操作,即无论原始数组的存储方式如何,都会返回一个新的数组副本。ravel()
只在必要时进行 复制操作,如果可以,它会返回一个视图,因此速度通常更快。
我们可以通过以下代码来对比它们的运行速度:
def f11():print(a.flatten())def f22():print(b.ravel())# 总是 Copy
print('%.6f - flatten' % get_run_time(f11))
# 用 ravel(), 需要 copy 的时候才会被 copy
print('%.6f - ravel' % get_run_time(f22))
运行结果如下:
0.000656 - flatten
0.000000 - ravel
可以看出,ravel()
的速度远快于 flatten()
,甚至在这次运行中 ravel()
的执行时间接近 0。这是因为 ravel()
只在必要时才会进行内存复制,而 flatten()
总是创建数组的副本,导致更多的时间消耗。
三 优化一 数据选择提速
操作上优化
1 view 的方式
a = np.zeros([100, 100])print(a)# 切片的操作是 viewa_view1 = a[1:2, 3:6] # 切片 slicea_view2 = a[:10] # 同上a_view3 = a[::2] # 跳行a_view4 = a.ravel() # 上面提到了
2 copy 的方式
# # 花式索引访问的方式是 copya = np.zeros([2, 2])# a = np.random.rand(2, 2)print("a = ", a)a_copy2 = a[[True, True], [False, True]] # 用 maskprint("a_copy2 = ", a_copy2)a = np.zeros([100, 100])a_copy1 = a[[1, 4, 6], [2, 4, 6]] # 用 index 选a_copy3 = a[[1, 2], :] # 虽然 1,2 的确连在一起了, 但是他们确实是 copya_copy4 = a[a[1, :] != 0, :] # fancy indexinga_copy5 = a[np.isnan(a[:, 0]), :] # fancy indexing
四 优化二 替代 copy 操作
1 使用 np.take()
替代索引方式进行数据选择
当我们通过索引选择数据时,np.take()
是一种更高效的替代方法。与直接使用索引相比,它通常能加快处理速度。以下代码对比
a000 = np.random.rand(1000000, 10)
indices = np.random.randint(0, len(a000), size=10000)def f111():# 使用索引进行数据选择_ = a000[indices]def f222():# 使用 np.take() 进行数据选择_ = np.take(a000, indices, axis=0)print('%.6f - [indices]' % get_run_time(f111))
print('%.6f - take' % get_run_time(f222))
运行结果
0.000416 - [indices]
0.000186 - take
可以看到,np.take()
比索引方式快了约 50%,表现更为优越,尤其是在处理大规模数据时。
2 使用 np.compress()
替代 mask
选择数据
在使用掩码(mask)筛选数据时,np.compress()
可以更高效地完成此任务。以下代码两者的性能对比
a111 = np.random.rand(10000, 10)
mask = a111[:, 0] < 0.5def f1111():# 使用 mask 进行数据选择_ = a111[mask]def f2222():# 使用 np.compress() 进行数据选择_ = np.compress(mask, a111, axis=0)print('%.6f - [mask]' % get_run_time(f1111))
print('%.6f - compress' % get_run_time(f2222))
运行结果
0.000067 - [mask]
0.000023 - compress
结果表明,np.compress()
的速度明显快于直接使用 mask,几乎快了 3 倍。这是因为 np.compress()
在底层进行了优化,能够更高效地处理数据筛选。
五 优化三 利用 out
参数提升性能
在数组运算中,使用 out
参数可以显著提升性能。out
参数允许将运算结果直接存储在现有的数组中,避免了创建新的数组对象。以下代码对比了三种不同的操作方式的性能。
aa = np.zeros([1000, 1000])
bb = np.zeros_like(aa)
cc = np.zeros_like(aa)def f1a():# 不使用 out 参数,结果重新赋值给原数组aa[:] = np.add(aa, 1)def f2b():# 使用 out 参数,结果直接存储在原数组中np.add(bb, 1, out=bb)def f3c():# 结果赋值到一个新的数组_cc = np.add(cc, 1)print('%.6f - without out' % get_run_time(f1a))
print('%.6f - out' % get_run_time(f2b))
print('%.6f - new data' % get_run_time(f3c))
运行结果
0.000817 - without out
0.000229 - out
0.000668 - new data
划重点
使用 out
参数与不使用时的性能对比
操作方式 | 时间(秒) | 说明 |
---|---|---|
不使用 out 参数 | 0.000817 | 运算后重新赋值给原数组,涉及数组的重新分配和赋值 |
使用 out 参数 | 0.000229 | 运算结果直接存储在原数组中,性能最优 |
生成新数组 | 0.000668 | 结果赋值到新创建的数组,存在内存分配开销 |
使用 out
参数可以显著提升性能,特别是在需要频繁进行大规模数据运算时,通过避免不必要的内存分配和数据拷贝,能够使程序更高效。详情见官方文档
六 完整代码示例
# This is a sample Python script.# Press ⌃R to execute it or replace it with your code.
# Press Double ⇧ to search everywhere for classes, files, tool windows, actions, and settings.
import numpy as np
import timeit
from functools import partialdef get_run_time(func, *args):repeat = 3number = 200return min(timeit.Timer(partial(func, *args)).repeat(repeat=repeat, number=number)) / numbera = np.random.rand(1000, 1000)
b = np.random.rand(1000, 1000)def f1():global b# 这会产生新的 bb = 2 * bdef f2():global a# 这不会产生新的 aa *= 2 # 和 a[:] *= 2 一样def f11():a.flatten()def f22():b.ravel()a000 = np.random.rand(1000000, 10)
indices = np.random.randint(0, len(a000), size=10000)def f111():# fancy indexing_ = a000[indices]# print("a000[indices] = ", _)def f222():# take_ = np.take(a000, indices, axis=0)# print("np.take(a000, indices, axis=0) = ", _)a111 = np.random.rand(10000, 10)
mask = a111[:, 0] < 0.5def f1111():_ = a111[mask]def f2222():_ = np.compress(mask, a111, axis=0)a222 = np.zeros([10000, 10])def f11111(a222):a222 = a222 + 1def f22222(a222):a222 = np.add(a222, 1)aa = np.zeros([1000, 1000])
bb = np.zeros_like(aa)
cc = np.zeros_like(aa)def f1a():aa[:] = np.add(aa, 1) # 把计算结果赋值回原数据def f2b():np.add(bb, 1, out=bb) # 把计算结果赋值回原数据def f3c():_cc = np.add(cc, 1) # 把计算结果赋值到新数据def print_hi(name):# Use a breakpoint in the code line below to debug your script.print(f'Hi, {name}') # Press ⌘F8 to toggle the breakpoint.a = np.arange(1, 7).reshape((3, 2))a_view = a[:2]a_copy = a[:2].copy()# print(a_view, a_copy)a_copy[1, 1] = 0print("在 copy 上修改数据,不会影响源数据:\n", a)a_view[1, 1] = 0print("在 view 上修改数据,会影响'窗里'的源数据:\n", a)print('%.6f - b = 2*b' % get_run_time(f1))print('%.6f - a *= 2' % get_run_time(f2))print()# 总是 Copyprint('%.6f - flatten' % get_run_time(f11))# 用 ravel(), 需要 copy 的时候才会被 copyprint('%.6f - ravel' % get_run_time(f22))# 在选择数据上加速a = np.zeros([100, 100])# a = np.random.rand(10, 6)# print(a)# 切片的操作是 viewa_view1 = a[1:2, 3:6] # 切片 slicea_view2 = a[:10] # 同上a_view3 = a[::2] # 跳行a_view4 = a.ravel() # 上面提到了# print("a_view1 = ", a_view1)# print("a_view2 = ", a_view2)# print("a_view3 = ", a_view3)# print("a_view4 = ", a_view4)print()# 花式索引访问的方式是 copy# a = np.zeros([2, 2])a = np.random.rand(2, 2)# print("a = ", a)a_copy2 = a[[True, True], [False, True]] # 用 mask# print("a_copy2 = ", a_copy2)a = np.zeros([100, 100])a_copy1 = a[[1, 4, 6], [2, 4, 6]] # 用 index 选a_copy3 = a[[1, 2], :] # 虽然 1,2 的确连在一起了, 但是他们确实是 copya_copy4 = a[a[1, :] != 0, :] # fancy indexinga_copy5 = a[np.isnan(a[:, 0]), :] # fancy indexing# print("a_copy1 = ", a_copy1)# print("a_copy3 = ", a_copy3)# print("a_copy4 = ", a_copy4)# print("a_copy5 = ", a_copy5)print()# 优化copy的方法# 1. 使用 np.take(), 替代用 index 选数据的方法。# print("len(a000) = ", len(a000))# print("indices = ", indices)print('%.6f - [indices]' % get_run_time(f111))print('%.6f - take' % get_run_time(f222))print()# 2. 使用 np.compress(), 替代用 mask 选数据的方法。print('%.6f - [mask]' % get_run_time(f1111))print('%.6f - compress' % get_run_time(f2222))print()# 非常有用的 out 参数print('%.6f - a + 1' % get_run_time(f11111, a222))print('%.6f - np.add(a, 1)' % get_run_time(f22222, a222))a = np.zeros([2, ])a_copy = np.add(a, 1) # copy 发生在这里# print(a, a_copy)b = np.zeros([2, ])c = np.zeros_like(b) # copy 发生在这里np.add(b, 1, out=c)# print(b, c)print()print('%.6f - without out' % get_run_time(f1a))print('%.6f - out' % get_run_time(f2b))print('%.6f - new data' % get_run_time(f3c))# Press the green button in the gutter to run the script.
if __name__ == '__main__':print_hi('Numpy 的 View 与 Copy')# See PyCharm help at https://www.jetbrains.com/help/pycharm/
复制粘贴并覆盖到你的 main.py 中运行,运行结果如下。
Hi, Numpy 的 View 与 Copy
在 copy 上修改数据,不会影响源数据:[[1 2][3 4][5 6]]
在 view 上修改数据,会影响'窗里'的源数据:[[1 2][3 0][5 6]]
0.000711 - b = 2*b
0.000232 - a *= 20.000638 - flatten
0.000000 - ravel0.000404 - [indices]
0.000177 - take0.000066 - [mask]
0.000023 - compress0.000017 - a + 1
0.000017 - np.add(a, 1)0.000801 - without out
0.000235 - out
0.000647 - new data
七 源码地址
代码地址:
国内看 Gitee 之 numpy/Numpy 的 View 与 Copy.py
国外看 GitHub 之 numpy/Numpy 的 View 与 Copy.py
引用 莫烦 Python