3. 数据类型映射与转换
在MATLAB与Python联合计算项目中,数据能否在两种运行时之间“无损、可控”地往返,往往比算法本身更容易出问题。MATLAB的默认数值容器是双精度矩阵,NumPy的默认容器是ndarray;MATLAB用结构体(struct)组织命名字段,Python用字典(dict)组织键值对;MATLAB用元胞数组(cell)装异构内容,Python用列表(list)装异构内容。这三组对应关系构成了跨语言数据交换的主干。本节围绕“映射规则—转换实现—形状与类型陷阱—验证方法”展开,重点说明实际工程中如何写出可靠的转换代码。
一、三组核心映射关系
先建立一张稳定的心智模型。MATLAB的数值矩阵与NumPy数组在语义上最接近,但默认维度顺序不同:MATLAB以列优先(column-major)存储,NumPy默认按行优先(row-major)解释内存。因此当你在Python侧用scipy.io.loadmat读取一个MATLAB的3×4矩阵时,得到的ndarray形状仍是(3, 4),索引A[i, j]与MATLAB的A(i+1, j+1)一一对应,这一点是安全的;真正需要警惕的是把一维数据在两侧来回搬运时的“行列翻转”。
结构体与字典的映射是“字段名→键、字段值→值”的递归映射。MATLAB结构体数组(struct array)是一个容易踩坑的点:s(1).name、s(2).name这种结构体数组在Python侧通常被加载为形状为(1, N)的对象数组,而不是一个字典列表。元胞数组与列表的对应则更直接:{1, 'abc', [1 2 3]}对应[1, 'abc', np.array([1,2,3])],但元胞数组可以是多维的,列表是一维的,多维元胞需要嵌套列表或展平后重建。
二、矩阵与数组的转换实现
最稳妥的通道是MAT文件。MATLAB侧用save('data.mat', 'A', 's', 'c')写出,Python侧用scipy.io.loadmat('data.mat')读入。读入结果是一个字典,键为变量名,值即对应数据。此时A是numpy.ndarray,s是结构化数组或对象数组,c是对象数组。反向则用scipy.io.savemat('out.mat', {'B': B}),MATLAB侧load('out.mat')即可得到B。
% MATLAB 侧:写出
A = reshape(1:12, 3, 4); % 3×4 double
s.name = 'sensor'; s.value = 25.6;
c = {1, 'abc', [1 2 3]};
save('data.mat', 'A', 's', 'c');
# Python 侧:读入并检查
import scipy.io as sio
import numpy as np
d = sio.loadmat('data.mat')
A = d['A'] # shape (3,4), dtype int64
print(A.shape, A.dtype, A[0, 0]) # (3, 4) int64 1
B = A.T.astype(np.float64) # 显式转置 + 类型统一
sio.savemat('out.mat', {'B': B})
注意两个细节。第一,MATLAB的double在Python侧通常保持float64,但整数矩阵可能被加载为int64,若下游算法假定浮点,应显式astype(np.float64)。第二,若在Python侧对矩阵做了转置再写回,MATLAB读到的就是转置后的结果——转置不会自动“抵消”,必须由调用方明确约定方向。工程上建议在接口层固定一条规则:跨语言传递的矩阵一律保持原始形状,不做隐式转置,需要改变布局时在文档中写明。
三、结构体与字典、元胞与列表的递归转换
结构体转字典的关键是递归。一个结构体字段的值可能仍是结构体、矩阵或元胞,因此转换函数必须按值类型分派。下面给出一个可直接使用的Python侧转换骨架,配合MATLAB侧对称实现即可完成往返。
import numpy as np
def mat2py(obj):
if isinstance(obj, np.ndarray) and obj.dtype == object:
return [mat2py(x) for x in obj.ravel(order='F')]
if isinstance(obj, np.ndarray) and obj.dtype.names:
return {n: mat2py(obj[n][0, 0]) for n in obj.dtype.names}
if isinstance(obj, np.ndarray):
return obj
return obj
def py2mat(obj):
if isinstance(obj, dict):
return {k: py2mat(v) for k, v in obj.items()}
if isinstance(obj, list):
arr = np.empty((1, len(obj)), dtype=object)
for i, v in enumerate(obj):
arr[0, i] = py2mat(v)
return arr
return obj
元胞数组转列表时,ravel(order='F')保证按MATLAB的列优先顺序展平,避免多维元胞在展平后元素顺序错乱。若元胞本身是一维的,这个顺序与直觉一致;若是2×2元胞,展平顺序为(1,1),(2,1),(1,2),(2,2),与MATLAB的线性索引完全对应。反向构造时用np.empty((1, n), dtype=object)再逐元素赋值,可避免NumPy把等长列表误判为二维数值数组。
[1, 2, 3]直接savemat后,MATLAB侧得到的是1×3的double行向量而非元胞。若语义上要求元胞,必须显式构造dtype=object的数组,或在MATLAB侧用num2cell包装。同理,MATLAB的1×1元胞在Python侧是长度为1的对象数组,不要误当作标量。四、验证转换是否可靠
转换完成后不能只看“没报错”。建议做三项检查:形状与元素总数是否一致(A.shape与MATLAB的size(A)逐维比对);数值是否逐元素相等(用np.allclose或isequal,注意浮点容差);嵌套结构是否保持层级(对字典递归比较键集合,对列表比较长度与各元素类型)。在联合项目中,把这三项封装成一个assert_roundtrip函数,在每次跨语言调用后执行,能提前暴露绝大多数映射错误。
| MATLAB 类型 | Python 对应 | 转换要点 | 易错点 |
|---|---|---|---|
| double 矩阵 | ndarray (float64) | savemat/loadmat,形状不变 | 整数矩阵 dtype 变化;隐式转置 |
| struct | dict | 按字段名递归映射 | 结构体数组被加载为对象数组 |
| cell | list | 按列优先展平后逐元素转换 | 等长列表被误判为数值矩阵 |
把上述规则固化为项目内的matpy_bridge.py与matpy_bridge.m一对文件,所有跨语言数据都经过这两个入口,而不是在各处零散调用loadmat。这样类型映射就从“每次凭记忆处理”变成“一次定义、处处复用”,联合计算的稳定性会明显提升。