1.3 数组形状操作
创建数组后,经常需要调整它的结构:把一维数据排成矩阵、交换坐标轴、把多个数组合并、把一个数组拆成几块。这些操作不改变数据本身,只改变数据在维度上的组织方式。本节将系统讲解形状查询与改变、转置、拼接、拆分、维度增删、重复与平铺这些核心的形状操作,并说明哪些操作返回视图、哪些返回副本。
1.3.1 形状查询与改变
shape 属性查询
数组的形状通过 shape 属性查看,它是描述各维度大小的元组:
import numpy as np
a = np.arange(24)
print(a.shape) # (24,)
b = a.reshape(4, 6)
print(b.shape) # (4, 6)reshape() 改变形状
reshape() 返回一个新形状的数组,数据按行优先(最后一个轴变化最快)重新排列。总元素数必须保持不变:
import numpy as np
a = np.arange(12)
b = a.reshape(3, 4)
print(b)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
c = a.reshape(2, 2, 3)
print(c.shape) # (2, 2, 3)某一维可以用 -1 表示"自动推断",让 NumPy 根据总元素数和其他维度算出该维大小:
a = np.arange(12)
print(a.reshape(3, -1).shape) # (3, 4)
print(a.reshape(-1, 4).shape) # (3, 4)
print(a.reshape(-1).shape) # (12,),还原为一维reshape 在能表示时返回视图(共享内存),否则返回副本。判断是否复制用 np.shares_memory:
a = np.arange(12)
b = a.reshape(3, 4)
print(np.shares_memory(a, b)) # True,共享内存resize() 修改形状
resize() 与 reshape 的区别在于,它会原地修改数组并可能改变元素总数:
a = np.arange(6)
a.resize(2, 3) # 原地改成 2x3
print(a)
# [[0 1 2]
# [3 4 5]]
b = np.arange(6)
b.resize(2, 4) # 元素数从 6 变 8,多出的位置补 0
print(b)
# [[0 1 2 3]
# [4 5 0 0]]resize() 直接改原数组且返回 None。若元素数减少,尾部数据被丢弃;增多,新增位置填充 0。
flatten() 与 ravel()
两者都把多维数组展平为一维。区别:ravel() 优先返回视图,flatten() 始终返回副本:
import numpy as np
a = np.arange(6).reshape(2, 3)
print(a.ravel()) # [0 1 2 3 4 5]
print(a.flatten()) # [0 1 2 3 4 5]
print(np.shares_memory(a, a.ravel())) # True,视图
print(np.shares_memory(a, a.flatten())) # False,副本ravel() 只有在无法表示为视图时才复制,因此更省内存。两者都支持 order 参数控制展平顺序(C 行优先默认,F 列优先)。
1.3.2 转置:.T、transpose()、swapaxes()
.T 属性转置
.T 返回转置数组,即交换两个轴。对二维数组就是行列互换:
import numpy as np
a = np.arange(6).reshape(2, 3)
print(a)
# [[0 1 2]
# [3 4 5]]
print(a.T)
# [[0 3]
# [1 4]
# [2 5]].T 对一维数组无效(形状不变),对多维数组是交换最后两个轴,不适用于任意轴交换。
transpose() 指定轴顺序
transpose() 接受轴顺序元组,可以按任意顺序重排维度:
a = np.arange(24).reshape(2, 3, 4)
b = a.transpose(1, 0, 2) # 交换前两个轴
print(a.shape) # (2, 3, 4)
print(b.shape) # (3, 2, 4)
c = a.transpose(2, 0, 1) # 重排为 (4, 2, 3)
print(c.shape) # (4, 2, 3)transpose 返回视图,共享底层数据。
swapaxes() 交换两个轴
swapaxes(axis1, axis2) 交换指定的两个轴,是高维数组常用的轴操作:
a = np.arange(24).reshape(2, 3, 4)
b = a.swapaxes(0, 2)
print(a.shape) # (2, 3, 4)
print(b.shape) # (4, 3, 2)transpose 可以一次重排任意个轴,swapaxes 只交换两个轴,swapaxes(i, j) 是 transpose 的特例。
1.3.3 数组拼接
拼接把多个数组合成一个,沿指定轴连接。参与拼接的数组在非拼接轴上的大小必须一致。
np.concatenate()
np.concatenate() 按指定轴拼接多个数组,是最通用的拼接函数:
import numpy as np
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
# 沿轴 0(行方向)拼接
print(np.concatenate((a, b), axis=0))
# [[1 2]
# [3 4]
# [5 6]
# [7 8]]
# 沿轴 1(列方向)拼接
print(np.concatenate((a, b), axis=1))
# [[1 2 5 6]
# [3 4 7 8]]vstack()、hstack()、dstack()
这三个是沿固定轴的便捷函数,语义更直观:
np.vstack()沿轴 0(垂直方向)堆叠,即纵向拼行np.hstack()沿轴 1(水平方向)堆叠,即横向拼列np.dstack()沿轴 2(深度方向)堆叠,常用于构造三维数据
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(np.vstack((a, b))) # 纵向堆叠成 2 行
# [[1 2 3]
# [4 5 6]]
print(np.hstack((a, b))) # 横向拼接成一维
# [1 2 3 4 5 6]
x = np.array([[[1], [2]]])
y = np.array([[[3], [4]]])
print(np.dstack((x, y)).shape) # (1, 2, 2)vstack 和 hstack 会自动把一维数组升维,使用上更省心。vstack 等价于 concatenate(..., axis=0),hstack 等价于 concatenate(..., axis=1)。
np.stack()
np.stack() 沿新轴堆叠,即增加一个维度把数排列起来,而不是在已有轴上拼接:
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
# 沿新轴 0 堆叠
c = np.stack((a, b), axis=0)
print(c.shape) # (2, 3)
print(c)
# [[1 2 3]
# [4 5 6]]
# 沿新轴 1 堆叠
d = np.stack((a, b), axis=1)
print(d.shape) # (3, 2)
print(d)
# [[1 4]
# [2 5]
# [3 6]]stack 与 concatenate 的关键区别:stack 增加一个维度,concatenate 在已有维度上连接。stack 要求各数组形状完全一致。
1.3.4 数组拆分
拆分与拼接相反,把数组沿某个轴切成若干段。
np.split()
np.split(ary, indices_or_sections, axis) 按给定位置切分:
import numpy as np
a = np.arange(9)
# 等分为 3 份
parts = np.split(a, 3)
print(parts) # [array([0,1,2]), array([3,4,5]), array([6,7,8])]
# 按位置切分:在索引 2、5 处切
parts2 = np.split(a, [2, 5])
print(parts2) # [array([0,1]), array([2,3,4]), array([5,6,7,8])]vsplit()、hsplit()、dsplit()
np.vsplit()沿轴 0 拆分(垂直切行)np.hsplit()沿轴 1 拆分(水平切列)np.dsplit()沿轴 2 拆分
import numpy as np
a = np.arange(16).reshape(4, 4)
print(a)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]
# [12 13 14 15]]
h = np.hsplit(a, 2) # 水平切,每份 2 列
print(h[0])
# [[ 0 1]
# [ 4 5]
# [ 8 9]
# [12 13]]
v = np.vsplit(a, 2) # 垂直切,每份 2 行
print(v[1])
# [[ 8 9 10 11]
# [12 13 14 15]]拆分结果可以是一个数组构成的列表,通过下标访问每个部分。
1.3.5 添加/删除维度
np.newaxis
np.newaxis 在指定位置插入一个长度为 1 的新轴,用于改变数组的维度:
import numpy as np
a = np.array([1, 2, 3])
print(a.shape) # (3,)
row = a[np.newaxis, :] # 变成行向量
print(row.shape) # (1, 3)
col = a[:, np.newaxis] # 变成列向量
print(col.shape) # (3, 1)
print(col)
# [[1]
# [2]
# [3]]newaxis 常与广播配合,把一个一维数组转换成列向量参与矩阵运算。
np.expand_dims()
expand_dims(a, axis) 的功能与 newaxis 相同,但以函数形式按轴位置插入新轴:
import numpy as np
a = np.array([1, 2, 3])
b = np.expand_dims(a, axis=0)
print(b.shape) # (1, 3)
c = np.expand_dims(a, axis=1)
print(c.shape) # (3, 1)
d = np.expand_dims(a, axis=-1)
print(d.shape) # (3, 1),-1 表示在末尾插入np.squeeze()
np.squeeze() 删除所有长度为 1 的轴,用于去掉多余的维度:
import numpy as np
a = np.array([[[1], [2], [3]]])
print(a.shape) # (1, 3, 1)
b = np.squeeze(a)
print(b.shape) # (3,)
# 可以指定要删除的轴
c = np.squeeze(a, axis=0)
print(c.shape) # (3, 1)squeeze 只删除长度为 1 的轴,其他轴不受影响。数据经过拼接、reshape 后常出现单维度,用 squeeze 清理。
1.3.6 重复与平铺
np.tile() 平铺整个数组
np.tile(A, reps) 把整个数组像铺瓷砖一样重复平铺,reps 指定每个轴的重复次数:
import numpy as np
a = np.array([1, 2, 3])
# 一维平铺 2 次
print(np.tile(a, 2)) # [1 2 3 1 2 3]
# 平铺成 2 行 3 列
b = np.tile(a, (2, 3))
print(b)
# [[1 2 3 1 2 3]
# [1 2 3 1 2 3]]np.repeat() 重复元素
np.repeat() 逐个重复数组的元素,而不是平铺整个数组:
import numpy as np
a = np.array([1, 2, 3])
# 每个元素重复 2 次
print(np.repeat(a, 2)) # [1 1 2 2 3 3]
# 指定每个元素的重复次数
print(np.repeat(a, [1, 2, 3])) # [1 2 2 3 3 3]
# 二维数组按轴重复
b = np.array([[1, 2], [3, 4]])
print(np.repeat(b, 2, axis=1))
# [[1 1 2 2]
# [3 3 4 4]]tile 和 repeat 的语义对比:
a = np.array([1, 2])
print(np.tile(a, 3)) # [1 2 1 2 1 2],整个数组重复
print(np.repeat(a, 3)) # [1 1 1 2 2 2],逐个元素重复练习题
第1题 概念理解
写出下面代码的输出结果,并解释 reshape、ravel、flatten 在返回视图还是副本上的区别。
import numpy as np
a = np.arange(12).reshape(3, 4)
print(a.ravel())
print(a.flatten())
print(a.reshape(-1).shape)
print(np.shares_memory(a, a.ravel()))参考答案
[0 1 2 3 4 5 6 7 8 9 10 11]
[0 1 2 3 4 5 6 7 8 9 10 11]
(12,)
Trueravel() 优先返回视图(共享内存),flatten() 始终返回副本。reshape(-1) 还原为一维。判断共享用 np.shares_memory。
第2题 代码编写
a = np.array([[1, 2, 3], [4, 5, 6]]) 和 b = np.array([[7, 8, 9], [10, 11, 12]])。用三种方式把两个数组合并成一个 4x3 的数组,再拆分回原来的两个 2x3 数组。
参考答案
import numpy as np
a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[7, 8, 9], [10, 11, 12]])
# 三种纵向拼接方式
c1 = np.concatenate((a, b), axis=0)
c2 = np.vstack((a, b))
c3 = np.stack((a, b), axis=0).reshape(4, 3)
# 拆回两个 2x3
parts = np.vsplit(c1, 2)
print(parts[0])
print(parts[1])concatenate、vstack 沿轴 0 拼接;stack 沿新轴堆叠后 reshape。vsplit 按 2 份垂直切回。
第3题 进阶练习
说明 np.tile(np.array([1, 2]), 3) 与 np.repeat(np.array([1, 2]), 3) 的输出差异,并给出把一维数组 np.array([1, 2, 3]) 转成 3x1 列向量再平铺成 3x4 矩阵的代码。
参考答案
import numpy as np
print(np.tile(np.array([1, 2]), 3)) # [1 2 1 2 1 2],整体重复
print(np.repeat(np.array([1, 2]), 3)) # [1 1 1 2 2 2],逐个重复
a = np.array([1, 2, 3])
col = a[:, np.newaxis] # (3, 1) 列向量
print(col.shape) # (3, 1)
b = np.tile(col, (1, 4)) # 平铺成 3x4
print(b.shape) # (3, 4)tile 重复整个数组,repeat 重复每个元素。列向量用 [:, np.newaxis] 得到,再用 tile 平铺。
第4题 项目实践
命令行任务管理器有 6 条任务,每条的属性(标题长度、优先级、剩余天数)记录在数组 task_matrix = np.arange(18).reshape(6, 3) 中。请把任务按属性拆成 3 列分别处理,并把 6 条任务复制成 2 组(共 12 条)以便后续分组实验。
参考答案
import numpy as np
task_matrix = np.arange(18).reshape(6, 3)
cols = np.hsplit(task_matrix, 3) # 拆成 3 列
print(cols[0].shape) # (6, 1) 标题长度
print(cols[1].shape) # (6, 1) 优先级
print(cols[2].shape) # (6, 1) 剩余天数
dup = np.tile(task_matrix, (2, 1)) # 复制成 2 组
print(dup.shape) # (12, 3)常见错误
错误 1 · ValueError: cannot reshape array of size 6 into shape (2,4)
原因:reshape 要求新旧元素总数一致。arange(6) 有 6 个元素,不能重排成 8 个元素的 (2,4)。
解决:确认总元素数不变,或某一维用 -1 让 NumPy 自动推断,如 a.reshape(2, -1)。
错误 2 · ValueError: all the input array dimensions except for the concatenation axis must match exactly
原因:拼接时非拼接轴的维度不一致。例如按列拼接两数组,但它们的行数不同。
解决:检查各数组 shape,确保拼接轴外的维度完全一致。用 vstack/hstack 时会自动处理一维升维,但维度仍必须匹配。
错误 3 · 修改 reshape 或 transpose 的结果导致原数组变化
原因:这些操作返回视图,共享底层数据。
解决:需要独立数据时调用 .copy()。判断是否共享用 np.shares_memory(a, b)。
错误 4 · resize() 改变元素总数导致数据丢失或填充 0
原因:a.resize() 原地修改,若新形状元素数少于原数会丢弃尾部,多于原数会补 0,结果可能不符合预期。
解决:需要改变元素总数时先明确新形状大小;reshape 不能改变元素总数,resize 才可以但需注意补 0 行为。