MATLAB 学习笔记——5. 脚本与函数
.M 文件
MATLAB 的.m
文件可以分成两类:
- 脚本文件,不接受输入参数,它们处理工作区中的变量和数据。
- 函数文件,可接受输入参数,并且可以有返回值,内部变量是函数的局部变量。
MATLAB 对于 .m
文件的文件名有一些特殊要求:
- 文件名允许由字母、数字、下划线(
_
)和点(.
)组成,注意不包括-
和空格; - 文件名必须以字母开头;
- 文件名区分大小写。
较新版本的MATLAB提供了后缀为
.mlx
的实时脚本/函数文件,大致就是对Jupyter Notebook的模仿,但是用起来并没有后者那么好用,各种操作不够自然。由于.mlx
文件不是纯文本文件,如果我们需要在.m
文件和.mlx
文件之间转换,必须通过MATLAB专门提供的工具进行转换,vscode等编辑器也并不支持.mlx
文件的显示。
脚本文件
载入脚本文件会依次执行所有命令,在重复执行大量命令时,可以整理为一个脚本进行执行。
对于当前目录下的myfile.m
脚本文件,在命令行窗口可以输入脚本的名称来执行脚本(不含文件后缀),
执行结果会输出到命令行窗口。脚本文件可以访问当前工作区的所有变量,对变量的创建和修改也会留在当前工作区中。
例如,脚本文件 1
2
3
4
5
6
7
8
9sum=0;n=0;
while sum<100
n=n+1;
sum=sum+n;
end
sum=sum-n;
n=n-1;
n,sum
执行结果 1
2
3
4
5
6>> myfile
n =
13
sum =
91
除了直接使用脚本名称,还可以通过run
命令实现
1
run('myscript.m');
为了提供代码的可读性,在脚本中更建议使用这种方式调用其它脚本。
脚本虽然可以通过名称直接调用,但是脚本名称本身并不是一个受保护的标识符,我们可以创建与之同名的变量,对于下文中的函数脚本名称同理。(毫无疑问,这是MATLAB在语法上的失败设计)
函数文件
一个典型的函数文件包括一个与文件名同名的主函数(主函数是全局函数,可以被外部调用)
1 | function [output1, ...,outptn] = func(input1, ... , inputn) |
例如
1 | function r = rank(A,tol) |
这里在函数定义行之后,可执行代码或空行之前的注释部分,视作函数文件的帮助语句,可以使用help rank
查看
rank 函数的帮助。
注意:
- 如果函数文件的名称和文件中提供的实际函数名不一致,那么 MATLAB 会发出警告,并且会以文件名为准,不可以通过函数名调用,只能通过文件名调用。
- 只有一个函数的.M文件甚至不需要使用
end
标记来结束function
语句;
局部函数
在函数文件和脚本文件的后面,还可以加上若干个局部函数,局部函数的名称不能和当前文件重名,但是局部函数只能当前文件中被调用,不能被外部调用。
对于局部函数来说,并不存在如C语言中在调用之前要添加函数声明的要求,即使函数定义在最后,在前面的语句中仍然可以直接使用。例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14function [x_max,x_min] = max_min_values(x)
x_max=func1(x);
x_min=func2(x);
end
function r=func1(x)
x1=sort(x, 'descend');
r=x1(1);
end
function r=func2(x)
x1=sort(x);
r=x1(1);
end
如果当前脚本中的局部函数与外部函数重名,那么局部函数的优先级更高。
在MATLAB 2024a 之前,如果一个脚本文件中希望定义局部函数,必须要放在脚本文件的最后,否则语法报错,但是最新版去掉了这种无意义的限制。
嵌套函数
除此之外,MATLAB还允许函数的嵌套定义,嵌套函数显然只允许在定义的函数内部被调用,定义无需出现在使用之前。
嵌套函数相比于局部函数最大的优势是,它可以直接访问外部函数中的变量(按照引用捕获,类似于全局变量),类似于无须声明的全局变量,例如
1
2
3
4
5
6
7
8
9
10function outer_function
x = 3;
y = nested_function(x);
disp(y); % 12
function y = nested_function(z)
% 嵌套函数可以直接访问外部变量 x
y = z^2 + x;
end
end
嵌套函数可以直接访问(按照引用捕获)外部函数的变量,但是附带的代价是运行效率的降低。
嵌套函数 vs 局部函数:
- 从追求效率和可读性的角度,更好的做法是使用局部函数替代嵌套函数,将所有需要的变量以函数参数的形式显式传递。
- 从内存占用的角度,如果嵌套函数需要处理的是外部函数中的一个大型数组,那么使用嵌套函数相比于内部函数可以减少大型数组的拷贝,从而减少内存占用。
函数打包
虽然MATLAB支持局部函数和嵌套函数,但是仍然改变不了一个函数文件只能对外提供一个可用函数的事实,我们为了代码复用,不得不将很多小函数拆分为单独的文件,这种做法非常不合理。
我们的需求是将若干个小函数打包到一个文件中,并且希望这些函数都对外可见,有两种方案可以做到:
- 返回局部函数的句柄组成的元胞数组;
- 基于面向对象,将所有函数设置为一个类的静态函数。
第一种方案例如:
1 | function funcs = mytools_auto() |
主函数还可以进一步简化,通过内置函数 localfunctions()
自动获取当前的所有局部函数的句柄所组成的元胞数组 1
2
3function funcs = mytools_auto()
funcs = localfunctions();
end
第二种方案例如:
1 | classdef Tool |
这两种方案各有优劣:
- 前者的使用比较简洁,不需要加上额外的类名,但是需要使用句柄;
- 后者虽然需要加上类名,但是由于不需要使用句柄,更利于代码提示,代码更容易维护。
补充
虽然直接使用命令/使用脚本/使用函数的方式在原理上是等效的,但是从优化角度考虑,这几种做法的效率是存在差异的, 通常的顺序是:命令 < 脚本 < 函数,也就是针对函数的优化是最好的。
函数基础
下面我们关注MATLAB中的函数语法,MATLAB是动态语言,无论是函数参数还是返回值都不存在类型匹配的问题,这即让我们写起来很方便,不需要考虑类型问题,也导致我们很容易出错。
不能直接通过控制台的输入来创建函数,通常需要创建并写入单独的函数文件。
函数的返回值
和Python不同,MATLAB函数不需要使用return
语句来指定返回值,也不会自动使用最后一个表达式的值作为返回值,必须具体给返回变量赋值,例如
1
2
3
4function result = hello()
result = 100;
return;
end
return
语句通常不需要出现,它出现的作用是让函数执行提前终止,返回变量此时所存储的结果就是函数的返回值,例如
1
2
3
4
5
6
7function result = hello()
result = 100;
return; % 提前返回,返回值 100
result = 200; % 返回值 200
end
函数也可以无返回值,例如 1
2
3function displayMessage()
disp('Hello, this is a message from a MATLAB function!');
end
下面这种写法是等效的,也表明函数没有返回值 1
2
3function [] = displayMessage()
disp('Hello, this is a message from a MATLAB function!');
end
函数还可以存在多个返回值,例如 1
2
3
4
5function [a, b, c] = func()
a = 1;
b = 2;
c = 3;
end
必须使用对应个数的变量组成的数组来接纳函数的返回值,接收的变量个数通常要匹配返回值个数,例如
1
2
3
4[a,b,c] = func()
% a = 1
% b = 2
% c = 3
如果接收变量个数多于提供的返回值个数,语法报错。
如果接收的变量个数少于返回值个数,则多余的返回值会丢弃,而且这种情况完全不报错!(MATLAB的语法真离谱)
1
2a = func()
% a = 1
考虑一个极端情况:函数体内没有对返回变量赋值 1
2
3function s = func()
return
end
此时函数处于一种薛定谔的状态:函数可以被正常调用,但是由于s
没有被赋值(不会被赋值[]
),如果我们在调用后抛弃返回值,没有任何影响,如果尝试使用返回值,则会导致错误,因为返回值是未定义的,不可以用于赋值或调用。(MATLAB的语法真离谱)
补充:在脚本文件中也可以使用return
语句,它的含义为脚本提前结束。
考虑下面这种情况 1
2
3
4
5
6
7
8
9
10func2(func1()) % 1
function func2(c, d)
disp(nargin)
end
function [a,b] = func1()
a = 1;
b = 2;
end
这里看起来是直接将func1()
的返回值传递给了func2
,但是实际上它只获取到了第一个返回值,因此只有一个参数被传递给func2
,显示
1
。
无实参调用
对于不传递实参的函数调用,在定义和调用时都可以省略()
,例如
1
2
3
4
5
6
7
8function hello()
disp("hello,world!")
end
% or
function hello
disp("hello,world!")
end
在调用时下面的语句是完全等价的!!! 1
2
3hello()
hello
这简直离了大谱,严重降低了程序的可读性:
hello
可能是在显示一个变量hello
的值;hello
也可能是在执行一个脚本文件hello.m
;hello
还可能是在执行一个函数但不传递任何参数hello()
。
不仅降低了程序的可读性,还给函数的传递人为造成了阻碍,必须额外提供一个函数句柄的语法。
对于无参数的函数调用,非常不建议省略括号。
持久性变量
在函数调用过程中会创建单独的作用域,除了使用global
声明并使用全局变量之外,MATLAB还提供了持久性变量(使用persistent
声明),相当于C语言中的局部静态变量,变量的生命周期与函数调用过程无关,例如
1 | counter() % 1 |
注意这里MATLAB并不会像C++一样自动忽略掉持久化变量的重复初始化,因此需要额外的判断保护。
基于元胞数组打包
我们可以通过元胞数组给函数一次性提供多个参数,例如 1
2
3
4
5
6
7
8
9
10% 将参数依次存入元胞数组
args = {1, 2, 3};
% 调用函数时展开元胞数组
output = func(args{:})
% output = func(1, 2, 3)
function result = func(a, b, c)
result = a + b + c;
end
对于提供多个返回值的函数,如果我们已知返回值个数,也可以使用元胞数组来接收,例如
1
2
3
4
5
6
7
8output = cell(1,3);
[output{1:3}] = func();
function [a,b,c] = func()
a = 1;
b = 2;
c = 3;
end
这种打包方式可能会有一些效率上的损失,但是让程序更具有通用性,因为固定个数的参数或返回值会限制程序的灵活性。
函数的特殊变量
考虑到实际代码的简洁性和可维护性,下面列举的涉及参数和返回值的各种花里胡哨的操作都不建议使用。
nargin
函数的实参并不要求和函数定义时的形参严格对应:
- 实参个数可以少于形参个数,此时后续的形参处于未定义状态:不能被调用,但是可以赋值后再使用;
- 实参个数不能多于形参,否则会导致语法错误。
我们可以基于nargout
在函数体内部获取函数调用方提供的实参个数,注意这并不是函数定义中的形参个数,而是由本次调用决定的,例如
1
2
3
4
5
6
7
8
9func(1); % Number of inputs: 1
func(1,2); % Number of inputs: 2
func(1,2,3); % Number of inputs: 3
func([1,2,3]); % Number of inputs: 1
func({1,2,3}); % Number of inputs: 1
function func(a,b,c,d,e,f)
disp(['Number of inputs: ', num2str(nargin)]);
end
通常利用nargin
来判断实际传递的参数个数,并据此提供形参列表中最后几个参数的默认值;
例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21func(10,20,30) % 60
func(10,20) % 33
func(10) % 15
func() % 6
func(1,1,1,1) % error
function result = func(a, b, c)
if nargin < 3
c = 3; % default value
end
if nargin < 2
b = 2; % default value
end
if nargin < 1
a = 1; % default value
end
result = a + b + c;
end
nargin
还有第二种用法:在函数外部使用nargin
函数可以获取函数定义中列出的形参个数,需要传递函数句柄,例如
1
2
3
4
5nargin(@func) % 4
function [a,b,c] = func(a,b,c,d)
% ...
end
如果函数定义中的形参中出现varargin
,则会将结果变成负数以提示。
nargout
我们可以基于nargout
在函数体内部获取函数调用方请求的返回值个数,注意这并不是函数定义中的返回值个数,而是由本次调用决定的,例如
1
2
3
4
5
6
7
8a = func(); % Number of outputs: 1
[a, b] = func(); % Number of outputs: 2
[a, b, c] = func(); % Number of outputs: 3
function [x, y, z] = func()
x = 1; y = 2; z = 3;
disp(['Number of outputs: ', num2str(nargout)]);
end
这种特殊机制是MATLAB在运行时专门提供的,对于一般的编程语言(例如Python和C/C++),在函数体内部是不可能获得调用请求的返回值个数的,除非将请求返回值个数作为参数输入。
在函数体内部可以根据nargout
来提供合适的返回值,MATLAB的很多内置函数都利用了这种机制,来动态决定所需返回的内容,例如SVD分解
\(A = U S V^T\) 1
2S = svd(A)
[U,S,V] = svd(A)
函数如果返回的值少于nargout
个,这次调用会报错。
即使函数返回更多的值,仍然只有nargout
个值是有效的,后几个则注定会被接收方丢弃,因此在函数体内部可以直接跳过。
例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14a = func() % 2
[a,b] = func() % 2 4
[a,b,c] = func() % 2 4 6
function [a, b, c] = func()
a = 2;
if nargout > 1
b = 4;
end
if nargout > 2
c = 6;
end
end
nargout
还有第二种用法:在函数外部使用nargout
函数可以获取函数定义中列出的返回值个数,需要传递函数句柄,例如
1
2
3
4
5nargout(@func) % 3
function [a,b,c] = func(a,b,c,d)
% ...
end
如果函数定义中列出的返回值包括varargout
,则会将结果变成负数以提示。
varargin
除了使用nargin
来判断实参个数,MATLAB还提供了类似于Python的*args
的特殊参数:varargin
,
它会将所有多余参数打包为一个 \(1\times
N\) 的元胞数组,如果没有多余参数则保持为空。
语法上要求varargin
必须是最后一个参数。
例如 1
2
3
4
5
6
7
8
9
10
11
12func() % 0
func(1) % 1
func(1,2) % 3
func(1,2,3,4) % 10
function total = func(varargin)
total = 0;
for k = 1:length(varargin)
total = total + varargin{k};
end
end
再例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15func(10) % 0
func(10,1) % 10
func(10,1,2) % 30
func(10,1,2,3,4) % 100
func() % error
function total = func(x, varargin)
total = 0;
for k = 1:length(varargin)
total = total + varargin{k};
end
total = total * x;
end
varargout
与varargin
类似,MATLAB还提供了打包多个返回值的varargout
,例如
1
2
3
4
5
6
7
8
9
10func(0)
[x] = func(1) % x = 1
[x,y] = func(2) % x = 1, y = 2
[x,y,z] = func(3) % x = 1, y = 2, z = 3
function varargout = func(n)
for k = 1:n
varargout{k} = k;
end
end
键值对参数
在 R2021a 之前,MATLAB
只能以非常原始的方式提供可选的键值对参数:将键的名称作为字符串或字符数组传递,随后加上对应的值。例如
1
func(1, "PlotType", "log")
对于新版本的MATLAB,终于支持了类似 Python
等现代语言的键值对参数语法(但是网上很多教程没有更新这部分的知识),例如
1
func(1, PlotType="log")
对某些内置函数的使用可以得到简化,尤其是绘图相关的函数,例如
1 | x = 1:10; |
可以改成下面的可读性更高的方式 1
2
3
4
5
6
7
8
9
10
11x = 1:10;
y1 = rand(1, 10);
y2 = rand(1, 10);
y3 = rand(1, 10);
% 分别为每条线设置不同的 LineStyle 和 Color
hold on; % 保持当前图形
plot(x, y1, LineWidth=2, LineStyle='--', Color='r');
plot(x, y2, LineWidth=2, LineStyle='-.', Color='b');
plot(x, y3, LineWidth=2, LineStyle=':', Color='g');
hold off; % 释放图形
但是如果希望自定义函数也支持这样的键值对参数,配置则比较复杂,需要用到
inputParser
对输入参数进行解析,示例代码如下(多个键值对不需要遵循先后顺序)
1 | demo(); |
函数句柄
MATLAB 在语法设计上存在明显的坑:
- 函数名称并不是受保护的标识符,我们可以直接创建同名变量
- 无参数调用函数时,可以直接省略括号
()
这导致 MATLAB 无法像 Python
一样把函数像普通变量一样直接赋值,而是需要单独设计函数句柄的语法,使用@
可以获取函数句柄,例如对于下面这个简单函数
1
2
3function result = square(x)
result = x^2;
end
使用@
获取函数句柄并赋值给一个变量,通过这个变量就可以直接调用函数
1
2
3f = @square;
y = f(5); % 25
但是和函数名不同,直接使用赋值函数句柄的变量f
本身却不能调用函数,即使这个函数并不需要参数。
函数句柄一旦被获取,就可以和普通变量一样被正常传递,例如在函数参数中传递,起到回调函数的作用
1
2
3
4
5
6
7
8function result = applyFunction(func, x)
result = func(x);
end
f = @sin;
y = applyFunction(f, pi/2); % 1
z = applyFunction(@sin, 0); % 0
特殊函数
为了避免在简单函数的使用时单独构造 .M 文件的麻烦,可以直接使用内联函数或匿名函数,更推荐使用匿名函数。
匿名函数
匿名函数就相当于 C++ 和 Python 中的 lambda 表达式,需要指定输入参数,无需指定返回值。
例如 1
2
3
4>> h=@(x,y)x^2+y^2;
>> h(2,2)
ans =
8
可以在匿名函数的表达式中自动捕获使用工作区的变量,捕获的语义是在定义时按值捕获,此后外部的修改不会对其产生影响。如果输入参数与工作区变量重名,前者会覆盖后者。
例如 1
2
3
4
5
6
7
8
9>> a=1;
>> f = @(s) s + 1;
>> f(1)
ans =
2
>> a=2;
>> f(1)
ans =
2
匿名函数赋值得到的是一个函数句柄,对于匿名无参函数的调用,是不允许省略括号的。
一种常见的需求是对现有函数进行封装,固定其中的一部分参数,保留剩下的部分参数待定,例如
1
2
3
4
5
6
7function result = multiplyAdd(a, b, c)
result = a * b + c;
end
newFunc = @(b, c) multiplyAdd(1, b, c);
result = newFunc(3, 4);
这时newFunc
就变成了一个只接收两个参数的匿名函数。
我们还可以创建多重匿名函数,例如 1
2
3f = @(a,b,c)@(x) a*x^2+b*x+c;
f1 = f(1,2,3); % 相当于f1 = @(x) x^2+2*x+3
f1(3)
注意:匿名函数在定义时没有提供返回值,在机制上相当于统一视作varargout,因此不能依靠nargout获取匿名函数的实际返回值个数。
内联函数
提供几个内联函数的例子即可。
例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25>> f=inline("x+2")
f =
内联函数:
f(x) = x+2
>> f(3)
ans =
5
>> g=inline('x^2+y','x','y')
g =
内联函数:
g(x,y) = x^2+y
>> g(2,3)
ans =
7
其中可以使用x+2
,x^2+y
之类的 MATLAB
表达式组成的字符串,MATLAB 会自动推断自变量(默认为
x),然后直接使用。
由于内联函数的定义是基于字符数组解析的,只能作为玩具和演示使用,在实际编程中并不建议使用。