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])],但元胞数组可以是多维的,列表是一维的,多维元胞需要嵌套列表或展平后重建。

MATLAB ↔ Python 数据类型映射与转换路径 MATLAB 侧 Python 侧 double 矩阵 A(3×4) 列优先存储 · 1 基索引 struct s.name / s.value 字段名 · 可嵌套 · 可成数组 cell c{1} = 'abc' 异构容器 · 可多维 ndarray shape=(3,4) 行优先 · 0 基索引 dict {'name': ..., 'value': ...} 键值对 · 可嵌套 list [1, 'abc', array] 异构容器 · 一维 savemat / loadmat 转置需显式 .T struct2dict 递归 dict2struct 递归 cell2list 逐元素 list2cell 逐元素 关键校验:形状一致 · dtype 一致 · 索引基准一致 · 嵌套层级一致

二、矩阵与数组的转换实现

最稳妥的通道是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把等长列表误判为二维数值数组。

常见陷阱:把Python的[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 变化;隐式转置
structdict按字段名递归映射结构体数组被加载为对象数组
celllist按列优先展平后逐元素转换等长列表被误判为数值矩阵

把上述规则固化为项目内的matpy_bridge.py与matpy_bridge.m一对文件,所有跨语言数据都经过这两个入口,而不是在各处零散调用loadmat。这样类型映射就从“每次凭记忆处理”变成“一次定义、处处复用”,联合计算的稳定性会明显提升。