你所需要的,不仅仅是一个好用的代理。
NumPy是Python中众多科学软件包的基础。它提供了一个特殊的数据类型ndarray,其在向量计算上做了优化。这个对象是科学数值计算中大多数算法的核心。
相比于原生的Python,利用NumPy数组可以获得显著的性能加速,尤其是当你的计算遵循单指令多数据流(SIMD)范式时。然而,利用NumPy也有可能有意无意地写出未优化的代码。
在这篇文章中,我们将看到一些技巧,这些技巧可以帮助你编写高效的NumPy代码。我们首先看一下如何避免不必要的数组拷贝,以节省时间和内存。因此,我们将需要深入NumPy的内部。
NumPy数组计算可能涉及到内存块之间的内部拷贝。有时会有不必要的拷贝,此时应该避免。相应地这里有一些技巧,可以帮助你优化你的代码。
import numpy as np
1. 查看静默数组拷贝的第一步是在内存中找到数组的地址。下边的函数就是做这个的:
def id(x):
# This function returns the memory
# block address of an array.
return x.__array_interface__['data'][0]
2. 有时你可能需要复制一个数组,例如你需要在操作一个数组时,内存中仍然保留其原始副本。
a = np.zeros(10); aid = id(a); aid
71211328
b = a.copy(); id(b) == aid
False
具有相同数据地址(比如id函数的返回值)的两个数组,共享底层数据缓冲区。然而,共享底层数据缓冲区的数组,只有当它们具有相同的偏移量(意味着它们的第一个元素相同)时,才具有相同的数据地址。共享数据缓冲区,但偏移量不同的两个数组,在内存地址上有细微的差别,正如下边的例子所展示的那样:
id(a), id(a[1:])
(71211328, 71211336)
在这篇文章中,我们将确保函数用到的数组具有相同的偏移量。下边是一个判断两个数组是否共享相同数据的更可靠的方案:
def get_data_base(arr):
"""For a given Numpy array, finds the base array that "owns" the actual data."""
base = arr
while isinstance(base.base, np.ndarray):
base = base.base
return base
def arrays_share_data(x, y):
return get_data_base(x) is get_data_base(y)
print(arrays_share_data(a,a.copy()), arrays_share_data(a,a[1:]))
False True
感谢Michael Droettboom指出这种更精确的方法,提出这个替代方案。
3. 数组计算包括就地操作(下面第一个例子:数组修改)或隐式拷贝操作(第二个例子:创建一个新的数组)。
a *= 2; id(a) == aid
True
c = a * 2; id(c) == aid
False
一定要选择真正需要的操作类型。隐式拷贝操作很明显很慢,如下所示:
%%timeit a = np.zeros(10000000)
a *= 2
10 loops, best of 3: 19.2 ms per loop
%%timeit a = np.zeros(10000000)
b = a * 2
10 loops, best of 3: 42.6 ms per loop
4. 重塑一个数组可能涉及到拷贝操作,也可能涉及不到。原因将在下面解释。例如,重塑一个二维矩阵不涉及拷贝操作,除非它被转置(或更一般的非连续操作):
a = np.zeros((10, 10)); aid = id(a); aid
53423728
重塑一个数组,同时保留其顺序,并不触发拷贝操作。
b = a.reshape((1, -1)); id(b) == aid
True
转置一个数组会改变其顺序,所以这种重塑会触发拷贝操作。
c = a.T.reshape((1, -1)); id(c) == aid
False
因此,后边的指令比前边的指令明显要慢。
5. 数组的flatten和revel方法将数组变为一个一维向量(铺平数组)。flatten方法总是返回一个拷贝后的副本,而revel方法只有当有必要时才返回一个拷贝后的副本(所以该方法要快得多,尤其是在大数组上进行操作时)。
d = a.flatten(); id(d) == aid
False
e = a.ravel(); id(e) == aid
True
%timeit a.flatten()
1000000 loops, best of 3: 881 ns per loop
%timeit a.ravel()
1000000 loops, best of 3: 294 ns per loop
6. 广播规则允许你在形状不同但却兼容的数组上进行计算。换句话说,你并不总是需要重塑或铺平数组,使它们的形状匹配。下面的例子说明了两个向量之间进行矢量积的两个方法:第一个方法涉及到数组的变形操作,第二个方法涉及到广播规则。显然第二个方法是要快得多。
n = 1000
a = np.arange(n)
ac = a[:, np.newaxis]
ar = a[np.newaxis, :]
%timeit np.tile(ac, (1, n)) * np.tile(ar, (n, 1))
100 loops, best of 3: 10 ms per loop
%timeit ar * ac
100 loops, best of 3: 2.36 ms per loop