6.7. 视图与副本¶
视图是通向与源相同数据块的第二个窗口。不会复制任何数据;视图持有一个全新的描述符(拥有自己的形状、步幅和 dtype),但共享缓冲区。视图的开销基本可以忽略不计。
副本会向摄像头请求一个新缓冲区,并遍历源来填充它。副本既耗费时间又耗费 RAM。
大多数改变形状的方法产生视图。大多数转换数据的方法产生副本。弄清哪个是哪个,就能决定一个热循环会不会把摄像头的 RAM 耗尽。
6.7.1. 重塑¶
reshape() 返回一个具有所请求形状的数组。元素总数必须保持不变,否则会引发 ValueError:
a = np.arange(12, dtype=np.uint8)
m = a.reshape((3, 4))
结果是一个视图——m 和 a 共享数据。通过 m[0, 0] = 99 写入也会改变 a[0]。
为 shape 赋一个新元组是这一相同操作的简写形式:
a = np.arange(9)
a.shape = (3, 3)
6.7.2. 转置¶
transpose()(或 .T 简写)反转各轴。它通过反转步幅来实现——不移动任何数据:
m = np.arange(6, dtype=np.uint8).reshape((2, 3))
t = m.T # shape (3, 2), shares m's buffer
转置后的视图不会连续地遍历数据块。逐行读取上面的 t 时访问的内存位置是 0, 3, 1, 4, 2, 5,而不是这些字节实际排列的底层顺序 0, 1, 2, 3, 4, 5。普通的算术运算和归约可以很好地处理这种情况——它们会按步幅逐步遍历——但 tobytes() 不行,因为它直接交还底层缓冲区而不进行复制。缓冲区所保存的字节顺序与视图形状所暗示的顺序不匹配,所以该方法在任何非连续视图上都会引发 ValueError。当需要按转置后的顺序获取字节时,应先强制生成一份全新的连续副本:
bytes_out = t.copy().tobytes()
6.7.3. Flatten 与 flat¶
flatten() 返回数组的一份一维副本:
f = m.flatten() # new dense 1-D ndarray
传入 order='C'(默认)会先遍历最后一个轴,传入 order='F' 则先遍历第一个轴:
m = np.arange(6, dtype=np.uint8).reshape((2, 3))
# m = [[0, 1, 2],
# [3, 4, 5]]
m.flatten() # array([0, 1, 2, 3, 4, 5], dtype=uint8)
m.flatten(order='F') # array([0, 3, 1, 4, 2, 5], dtype=uint8)
flat 是迭代器形式。它将任意维 ndarray 的每个元素以标量的形式逐个产出,而不分配一份平坦副本:
for x in m.flat:
print(x)
当应用程序需要遍历每个元素时,优先使用 flat;当它需要一个稠密的一维缓冲区传给另一个函数时,使用 flatten()。
6.7.4. 迭代¶
迭代一维数组会产出标量;迭代高维数组会产出 (n-1) 维视图:
m = np.array([[0, 1, 2], [3, 4, 5]], dtype=np.uint8)
for row in m:
print(row) # array([0, 1, 2]), array([3, 4, 5])
迭代一个矩阵所产出的各行都是视图,因此修改它们会修改源。
6.7.5. 副本¶
copy() 是获取一个独立 ndarray 的显式方式,对它的修改不会影响原数组。它会分配一个新缓冲区,并将源遍历写入其中:
c = a.copy()
tobytes() 返回一个与数组数据块共享内存的 bytearray。通过该 bytearray 写入会原地修改数组。如果数组不是稠密的(切片视图、转置等),则会引发 ValueError。
tolist() 以可能嵌套的 Python list 形式返回内容。对于序列化小结果很有用;对于大结果则代价高昂,因为每个元素都会变成一个独立的 Python 对象。
6.7.6. 哪些操作返回哪种结果¶
完整规则如下:
以下操作返回视图:
切片——
a[1:5]、a[::2]、m[:, 0];对高维数组的单轴索引——
m[0];迭代一个 n 维数组;
reshape(),当所请求的布局兼容时;transpose()/.T;asarray(),当 dtype 匹配时。
以下操作返回副本:
布尔索引——
a[mask];算术运算——
a + b、a * 2、np.sin(a);array()——始终复制,即使是从另一个数组复制;
只有在确实需要一个独立缓冲区时才使用显式复制。在 RAM 有限的摄像头上,视图与副本之间的差别,往往就是代码能否装得下与装不下之间的差别。