的实现)
前言标题里的「交换Swapping」是个有歧义的说法得先说明白否则整篇都会讲歪。在 NumPy 的语境里它至少可以指三件不同的事轴的交换numpy.swapaxes(a, axis1, axis2)交换两根轴numpy.transpose重新排列所有轴。这是最常见的理解。迭代时的缓冲交换nditer在遍历不连续的数组时会先把数据拷进缓冲区、算完再拷回去内部存在一次「进出缓冲区」的交换过程。这是文档里讲的 buffering 机制。字节序交换ndarray.byteswap把每个元素的字节序倒过来比如大端换小端。这是另一个话题本文只在必要时提一句。本文重点讲前两种因为它们共享同一个核心概念——视图view与副本copy。轴交换之所以会让人踩坑归根到底是「它返回的是视图不是副本」nditer的缓冲机制之所以需要存在也恰恰是因为「原始数组不连续没法直接按顺序访问」。还有两个前提NumPy 是第三方库需要pip install numpy本机没有 Python 解释器也没有 NumPy本文代码未在本机运行验证只作逐行人工推演行为描述以 NumPy 官方文档为准。另外Python 2.7 已于 2020 年 1 月 1 日停止维护本文一律按 Python 3 写。一、视图与副本所有「交换」讨论的基础要把「交换」讲清楚必须先讲清 NumPy 数组的内部结构。官方文档的解释是一个ndarray由两部分组成——一段连续的数据缓冲区存放真正的数据元素以及描述这块缓冲区的元数据包括数据类型dtype、跨距strides等。由此引出两个术语视图view只改变元数据比如 strides、dtype不改变数据缓冲区得到一块「换个角度看同样数据」的新数组。因为共享同一块数据改视图就是改原数组。副本copy把数据缓冲区和元数据都复制一份。改副本不影响原数组但复制更慢、更占内存。规则很简单但要记住操作结果是基本索引切片a[1:3]、a[:, 0]视图花式索引布尔索引a[[1, 2]]、a[a 3]副本ndarray.view()强制视图ndarray.copy()强制副本reshape能改 strides 时是视图否则是副本ravel尽可能返回连续的扁平视图flatten总是副本transpose/.T/swapaxes视图下面详述判断一个数组是视图还是副本看它的base属性视图的base指向原数组副本的base是None。官方文档特别提醒一句base只能用来判断「是不是别的数组的视图」不要拿它来判断这个数组对象是不是新建的。# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npx np.arange(9).reshape(3, 3)y x.reshape(3, 3)print(y.base is not None) # Truereshape 创建了视图z y[[2, 1]]print(z.base is None) # True花式索引创建了副本二、理解一轴的交换numpy.swapaxes(a, axis1, axis2)的作用是交换数组的两根轴。官方文档对返回值的说明很关键从 NumPy 1.10.0 起如果a是ndarray返回的a的一个视图更早的版本只有在轴顺序真的变了时才返回视图否则直接把输入原样返回。换句话说在现代 NumPy 上swapaxes的结果几乎总是视图。numpy.transpose(a, axesNone)是更一般的版本axes给出一个轴的排列返回数组的第i根轴对应输入的第axes[i]根轴不传axes时默认是range(a.ndim)[::-1]也就是把轴顺序整个反过来。.T属性等价于不带参数的transpose。几个官方文档明确指出的细节容易记错一维数组的transpose返回的是原数组的未改动视图——一个转置的向量还是同一个向量所以它不会有任何变化。想把一维数组变成二维列向量得先加一根轴比如np.atleast_2d(a).T或者a[:, np.newaxis]。对二维数组transpose就是标准的矩阵转置。对 n 维数组transpose(a).shape a.shape[::-1]。# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npx np.array([[1, 2, 3]])print(np.swapaxes(x, 0, 1).shape) # (3, 1)a np.arange(24).reshape(2, 3, 4)print(a.T.shape) # (4, 3, 2)轴整个反转print(np.swapaxes(a, 0, 2).shape) # (4, 3, 2)只换第 0 和第 2 根轴print(np.transpose(a, (1, 0, 2)).shape) # (3, 2, 4)按给定排列换注意swapaxes和transpose在三维上的差别transpose默认反转全部轴swapaxes只动你指定的两根。对二维数组两者结果一样对三维及以上就不一样了——这是「以为换了 0 和 2 就等于转置」的常见误解。三、轴交换为什么「改一个另一个也变」因为它是视图。这一点单独讲因为它造成的 bug 最隐蔽。# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa np.arange(6).reshape(2, 3)t a.T # 视图不是副本t[0, 0] 99print(a[0, 0]) # 99 —— 原数组被改了s a.swapaxes(0, 1) # 同样是视图s[0, 1] -1print(a[1, 0]) # -1 —— 又改到了原数组如果你只是想拿一份转置来做别的事、不希望影响原数组就显式复制# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa np.arange(6).reshape(2, 3)t a.T.copy() # 副本a 不受影响t[0, 0] 99print(a[0, 0]) # 0 —— 原数组保持原样还有一种容易忽略的情况视图会影响原数组的内存布局特性。转置后的数组通常是非连续的它的 strides 不再满足 C 序的规律这会在两个地方咬你一是某些要求连续内存的底层操作会因此多做一次复制二是你在nditer里遍历它时迭代顺序会和你想的不一样因为默认按内存布局访问。这就自然过渡到第二种「交换」。四、理解二nditer的缓冲机制nditer默认按数组在内存里的实际布局访问元素这是为了效率。但当元素无法用固定跨距顺序访问时比如对一个 C 序存储的数组强制用orderF遍历迭代器就只能把数据切成小块一块一块地给你。官方文档举的例子正好说明了这点np.arange(6).reshape(2, 3)在flags[external_loop]下默认序会给出一整块[0 1 2 3 4 5]而强制orderF时会给出三块[0 3] [1 4] [2 5]——因为按 Fortran 序访问 C 序数据元素之间没有恒定跨距凑不出连续块。缓冲buffering就是解决这个问题的机制打开buffered标志后迭代器先把数据拷进一块缓冲区、按目标顺序排好再把整块交给你你处理完之后如果需要写回它再拷回去。官方文档给的对比是——同样的orderF加上buffered后三块[0 3] [1 4] [2 5]合并成了一整块[0 3 1 4 2 5]。所以「交换」在这里体现为数据从原数组拷贝到缓冲区可能伴随 dtype 转换或顺序重排迭代结束后再拷贝回原数组。这也解释了为什么修改数组必须显式结束迭代退出with或调用close()——写回的动作发生在那一刻。# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa np.arange(8).reshape(2, 4) # 换成 2x4 的数组再演示一次同样的现象for x in np.nditer(a, flags[external_loop], orderF):print(x, end )# 强制 F 序每一列被切成一块共四块# [0 4] [1 5] [2 6] [3 7]for x in np.nditer(a, flags[external_loop, buffered], orderF):print(x, end )# 加 buffered 后四块合并成一整块# [0 4 1 5 2 6 3 7]要提醒的是缓冲区走的是拷贝语义不是视图迭代器给你的那个块是临时缓冲修改它不等于直接改原数组——只有在迭代正常结束、写回动作执行之后改动才会落到原数组上。这个语义上的「两种读写路径」是nditer里最容易出错的地方尤其在循环中途break或抛异常时写回可能没发生所以推荐用try / finally包住close()。五、两种「交换」分别在什么场景遇到场景你碰到的是典型症状处理方式转置后改了数据原数组也跟着变了轴交换视图「我没动 aa 怎么变了」明确这是视图不想要就.copy()把转置结果传给要求连续内存的代码轴交换非连续性能下降或有额外的复制用np.ascontiguousarray显式取得连续副本遍历时顺序和预期不符内存布局 nditer「为什么先遍历到第 3 行」显式指定order或先用.copy(orderC)规整布局迭代中改了元素原数组没变nditer写回时机「赋值了却没生效」用with或try / finally保证close()拿到的是零维数组x ...没效果nditer缓冲语义「循环里赋的值丢了吗」写x[...] ...不要重新绑定名字可以看出这两个「交换」其实是同一件事的两面NumPy 为了避免拷贝而到处使用视图和惰性布局当底层操作无法直接处理这种布局时就必须借助一次显式的拷贝缓冲区来「交换」数据。理解了这个张力你就知道什么时候该防着视图什么时候该主动促成一次拷贝。常见坑点❌ 把a.T或a.swapaxes(0, 1)当副本用改动之后发现原数组也变了。✅ 轴交换返回的是视图确实要独立的数据就.copy()或用np.ascontiguousarray得到连续副本。❌ 在三维数组上认为np.swapaxes(a, 0, 2)和a.T等价。✅transpose不带参数时反转全部轴swapaxes只交换指定的两根二维时结果才相同。❌ 对一维数组调用transpose期待它变成二维列向量。✅ 一维的transpose返回未改动视图要列向量得用np.atleast_2d(a).T或a[:, np.newaxis]。❌ 用base属性来判断一个数组「是不是新创建的」。✅base只表示它是不是别的数组的视图副本的base是None视图的base指向原数组。❌ 在nditer的循环体里写x x * 2想改原数组。✅ 写x[...] x * 2并确保迭代以with或close()正常结束——写回在那一刻发生。❌ 循环中途break或抛异常后以为改动已经写回。✅ 用try / finally保证close()一定被调用否则缓冲区里的改动可能丢失。❌ 用flatten()之后又困惑「为什么这个不是视图」。✅flatten总是返回副本想要「尽量返回视图」的扁平化用ravel但它在无法保持连续时仍会复制。❌ 认为reshape一定是视图或一定是副本。✅ 官方说法是「能改 strides 时返回视图否则需要拷贝」判断依据看base不要靠猜。总结概念轴交换swapaxes/transposenditer缓冲共享数据吗视图与原数组共享缓冲区缓冲区是拷贝写回后原数组才变会改变原数组吗直接给视图元素赋值就会迭代正常结束、写回时才变主要触发原因重新排列轴的元数据无需搬数据元素无法用固定跨距顺序访问什么时候需要「主动拷贝」想要独立数据时.copy()打开buffered以及op_dtypes判断依据base属性是否发生写回with/close()把「交换」拆成这两种理解之后真正的核心概念就浮出来了——视图与副本。轴的交换返回视图所以「改一个另一个也变」nditer的缓冲是一次拷贝所以必须在正确的时刻写回。写 NumPy 代码时只要每次遇到可能共享内存的操作就问一句「这是视图还是副本」绝大多数「数据莫名其妙变了」或者「改了却没生效」的问题都能提前避开。参考视图与副本的定义、swapaxes的返回值说明、transpose的axes语义、nditer的 buffering 行为均以 NumPy 官方文档的arrays.ndarray、arrays.nditer与「Copies and views」章节为准NumPy 为第三方库需pip install numpy本文代码未在本机运行仅作人工推演。