2. MATLAB脚本与函数自动化基础

在批处理工程中,最忌讳的是把几十行代码直接堆在命令行窗口里反复粘贴。命令行适合探索,但一旦某个处理流程需要重复执行、需要换参数、需要被别的任务调用,就必须把它固化成可复用的单元。MATLAB 提供的基本复用单元有两类:脚本(script)和函数(function)。理解它们的边界,是搭建批处理流水线的第一步。

脚本与函数:共享工作区还是隔离工作区

脚本文件是以 .m 结尾、首行不是 function 的文件。它没有输入参数,也没有输出参数,运行时直接在当前工作区(base workspace)里逐行执行。这意味着脚本里定义的变量会留在工作区中,脚本之间可以互相“看到”对方的变量。对于一次性数据清洗、临时画图,脚本很方便;但在批处理中,这种共享工作区恰恰是隐患:循环到第 37 个文件时,某个变量被上一次迭代残留的值污染,结果很难排查。

函数文件则把工作区隔离起来。函数内部有自己的局部工作区,外部看不到内部变量,内部也看不到外部变量,数据只能通过参数和返回值进出。这种“黑盒”特性正是可复用单元的核心价值。一个典型的批处理函数长这样:

function [stats, okFlag] = analyzeFile(filePath, opts)
% analyzeFile 读取单个数据文件并返回统计结果
% filePath : 字符向量或字符串,待处理文件路径
% opts     : 结构体,字段 winLen、detrendFlag
    arguments
        filePath (1,:) char
        opts.winLen (1,1) double {mustBePositive} = 256
        opts.detrendFlag (1,1) logical = true
    end
    raw = readmatrix(filePath);
    if opts.detrendFlag
        raw = raw - mean(raw, 'omitnan');
    end
    stats.mean = mean(raw, 'omitnan');
    stats.std  = std(raw, 'omitnan');
    stats.n    = numel(raw);
    okFlag = all(isfinite([stats.mean, stats.std]));
end

这里用到了 arguments 块,它是较新版本 MATLAB 推荐的参数校验方式:既能声明类型和尺寸,又能给默认值,还能用 mustBePositive 之类的验证函数在入口处拦截非法输入。相比在函数体里手写一堆 if nargin < 2,它更清晰,也更不容易漏。

局部函数:把大函数拆成可读的小块

一个函数文件里可以包含多个函数。第一个是主函数(文件名必须与它同名),后面的是局部函数(local functions)。局部函数只能被同一文件内的函数调用,外部不可见。这带来两个好处:一是把长流程拆成命名清晰的步骤,二是把只服务于本模块的辅助逻辑“藏”起来,不污染全局函数空间。

例如上面的 analyzeFile 可以把去趋势和统计拆开:

function [stats, okFlag] = analyzeFile(filePath, opts)
    arguments
        filePath (1,:) char
        opts.winLen (1,1) double {mustBePositive} = 256
        opts.detrendFlag (1,1) logical = true
    end
    raw = readmatrix(filePath);
    if opts.detrendFlag
        raw = removeTrend(raw);
    end
    stats = computeStats(raw);
    okFlag = all(isfinite([stats.mean, stats.std]));
end

function y = removeTrend(x)
    y = x - mean(x, 'omitnan');
end

function s = computeStats(x)
    s.mean = mean(x, 'omitnan');
    s.std  = std(x, 'omitnan');
    s.n    = numel(x);
end

注意局部函数不能访问主函数的变量,必须通过参数传递。这一点常被初学者忽略,误以为“在同一个文件里就能共享”。

函数句柄:把函数当作数据来传递

函数句柄(function handle)用 @ 创建,例如 f = @(x) x.^2 + 1,或 g = @mean。句柄的价值在于它让函数可以像数值一样被赋值、存入元胞数组、作为参数传给另一个函数。批处理中常见的场景是:同一套流程要支持多种预处理方式,就可以把处理函数作为参数传入。

function out = applyPipeline(data, preprocessFcn)
    out = preprocessFcn(data);
end

% 调用时按需切换
r1 = applyPipeline(x, @(v) v - mean(v));
r2 = applyPipeline(x, @(v) movmean(v, 5));

句柄还支持捕获外部变量,形成闭包。例如在循环里为每个文件生成一个带固定阈值的判断函数,就不必把阈值硬编码进函数体。需要提醒的是,句柄捕获的是变量在创建时刻的引用,若后续修改被捕获变量,行为可能不符合直觉,建议捕获后不再改动。

参数传递:值传递语义与写时复制

MATLAB 在语义上是值传递:函数内修改输入参数,不会影响调用者的变量。但底层实现采用写时复制(copy-on-write),只要函数内不修改该变量,就不会真正复制数据。因此把大矩阵传进函数并不必然带来内存翻倍,真正触发复制的是函数内的写操作。批处理中若发现内存暴涨,应检查是否在函数内对输入大数组做了原地修改式的赋值。

参数设计上,推荐“少量必选参数 + 一个选项结构体”的模式。必选参数放前面,可选配置用 arguments 块或 name-value 对表达。这样调用点可读性强,后续新增选项也不会破坏已有调用。

返回值设计:让调用者能判断成败

批处理最怕“静默失败”。一个文件读不出来、数据全是 NaN、长度不足窗口,如果函数只返回一个数值,调用者无从判断。推荐的做法是:主结果放在第一个返回值,第二个返回值专门用于状态或诊断信息。可以是逻辑标志,也可以是包含 status、message、elapsed 的结构体。

[stats, info] = analyzeFile('run_037.csv', struct('winLen', 512));
if ~info.ok
    fprintf('跳过 %s:%s\n', info.file, info.message);
end

下面的图把本节几个关键概念串成一条数据流:脚本负责组织循环与调度,函数作为隔离的复用单元,参数从左侧进入,返回值从右侧流出,局部函数在主函数内部承担子步骤,函数句柄则作为可替换的处理策略被传入。

脚本调度 · 函数复用 · 参数与返回值的数据流 脚本 batch_run.m for k = 1:numel(files) 共享 base 工作区 组织循环与调度 输入参数 filePath opts.winLen opts.detrendFlag 函数 analyzeFile(隔离工作区) readmatrix 读数据 removeTrend 去趋势 computeStats 统计 局部函数(仅本文件可见) 返回值 stats.mean / std / n 主结果 info.ok 状态诊断 函数句柄:可替换的处理策略 @(v) v - mean(v) 去均值策略 @(v) movmean(v,5) 滑动平均策略 调用点判断 info.ok 为真 → 汇总 stats;为假 → 记录文件名与 message,继续下一个文件

把单元组织成批处理骨架

有了上述基础,一个可复用的批处理骨架通常包含三层:最外层脚本负责枚举文件、维护结果表、捕获异常;中间层函数负责单个文件的完整处理;底层局部函数负责具体算法步骤。外层脚本不要直接写算法细节,中间层函数不要依赖外部变量,底层局部函数不要跨文件调用。这样当处理逻辑需要修改时,只动一个文件;当需要换一种预处理策略时,只换一个句柄。

实践建议:为每个函数写一行 H1 注释(函数名后的第一行注释),说明输入、输出和副作用。批处理脚本在循环中调用函数时,用 try/catch 包住单次调用,把失败文件记录到日志而不是让整个批次中断。这样即使第 37 个文件损坏,前 36 个结果仍然可用。
常见误区:把批处理脚本写成“一个巨型脚本”,所有变量都在 base 工作区里滚动。一旦需要并行(parfor)或换机器运行,变量污染和路径依赖会立刻暴露。尽早把处理逻辑收进函数,是后续并行化和工程化的前提。

小结本节的关键判断:需要共享中间变量、只跑一次,用脚本;需要被反复调用、需要隔离、需要并行,用函数。参数用 arguments 块声明并校验,返回值至少两个——主结果加状态。局部函数用于拆分步骤,函数句柄用于把策略变成可传入的数据。把这四点落实,批处理任务就从“一次性代码”变成了可复用单元。