MATLAB 学习笔记——5. 脚本与函数
.M 文件
MATLAB 的.m
文件可以分成两类:
- 脚本文件,不接受输入参数,它们处理工作区中的变量和数据。
- 函数文件,可接受输入参数,并且可以有返回值,内部变量是函数的局部变量。
除此之外,MATLAB也提供了.mlx
格式的实时脚本(实时函数)文件,大致就是Jupyter
Notebook的模仿,但是用起来并没有后者那么好用,各种操作不够自然。如果我们需要在.m
文件和.mlx
文件之间转换,直接改文件后缀的做法是不安全的,可能会导致格式错误,需要用MATLAB专门提供的工具进行转换。
脚本文件
载入脚本文件会依次执行所有命令,在重复执行大量命令时,可以整理为一个脚本进行执行。
对于当前目录下的myfile.m
脚本文件,在命令行窗口可以输入脚本的名称来执行脚本(不含文件后缀),
执行结果会输出到命令行窗口。脚本文件可以访问当前工作区的所有变量,对变量的创建和修改也会留在当前工作区中。
例如,脚本文件
1 | sum=0;n=0; |
执行结果
1 | >> myfile |
除了直接使用脚本名称,还可以通过run
命令实现
1
run('myscript.m');
脚本虽然可以通过名称直接调用,但是脚本名称本身并不是一个受保护的标识符,我们可以创建与之同名的变量,对于下文中的函数脚本名称同理。(毫无疑问,这是MATLAB在语法上的失败设计)
函数文件
一个典型的函数文件包括一个同名的主函数(主函数是全局函数,可以被外部调用)
1 | function [output1, ...,outptn] = func(input1, ... , inputn) |
例如
1 | function r = rank(A,tol) |
这里在函数定义行之后,可执行代码或空行之前的注释部分,视作函数文件的帮助语句,可以使用help rank
查看
rank 函数的帮助。
补充
在函数文件和脚本文件的后面,还可以加上若干个局部函数,对局部函数的名称没有要求,但是局部函数只能当前文件中被调用,不能被外部调用。
对于局部函数来说,并不存在如C语言中在调用之前要添加函数声明的要求,即使函数定义在最后,在前面的语句中仍然可以直接使用。例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function [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中的函数语法,MATLAB是动态语言,无论是函数参数还是返回值都不存在类型匹配的问题,这即让我们写起来很方便,不需要考虑类型问题,也导致我们很容易出错。
不能直接通过控制台的输入来创建函数,通常需要创建并写入单独的函数文件。
函数的返回值
和Python不同,MATLAB函数不使用return
语句来确定返回值,也不会自动使用最后一个表达式的值作为返回值,必须具体给返回变量赋值,例如
1
2
3function result = hello()
result = 100;
end
return
语句通常不需要出现,它出现的作用是让函数提前终止,此时返回变量所存储的结果就是函数的返回值,例如
1
2
3
4
5
6
7function result = hello()
result = 100;
return; % 返回100
result = 200;
end
函数也可以无返回值,例如 1
2
3function displayMessage()
disp('Hello, this is a message from a MATLAB function!');
end
在脚本文件中也可以使用
return
语句,它的含义为脚本提前结束。
无实参调用
对于不传递实参的函数调用,在调用时可以省略()
,例如
1
2
3function hello()
disp("hello,world!")
end
在调用时下面的语句是完全等价的 1
2
3hello()
hello
这简直离了大谱,严重降低了程序的可读性:
hello
既可能是在显示一个变量hello
的值,也可能是在执行一个脚本文件hello.m
,还可能是在执行一个函数但不传递任何参数hello()
。
持久性变量
在函数调用过程中会创建单独的作用域,除了使用global
声明并使用全局变量之外,MATLAB还提供了持久性变量(使用persistent
声明),相当于C语言中的局部静态变量,变量的生命周期与函数调用过程无关,例如
1 | function count = counter() |
注意这里MATLAB并不会自动忽略掉持久化变量的初始化,因此需要额外的判断保护。
使用例如 1
2
3
4
5
6>> counter()
ans =
1
>> counter()
ans =
2
函数进阶
考虑到实际代码的简洁性和可维护性,下面列举的涉及参数和返回值的各种花里胡哨的操作都不建议使用。
MATLAB的函数语法其实非常的灵活,可以接收不同个数的输入,甚至根据用户提供的接收变量个数生成不同的返回值!
在函数调用过程中,MATLAB提供两个特殊函数:
nargin
表示输入参数个数,可以用于检验当前传入的参数个数是否合法;nargout
表示输出参数个数,可以用于判断调用时请求个数以提供不同的返回值。
不同实参个数
函数的实参并不要求和函数定义时的形参严格对应:
- 实参个数可以更少,这并不会导致语法错误,可以基于此加上
nargin
来判断并提供缺省参数的默认值; - 实参个数不能更多,这会导致语法错误。
例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function result = example(a, b, c)
if nargin < 1
a = 1;
end
if nargin < 2
b = 2;
end
if nargin < 3
c = 3;
end
result = a + b + c;
end
使用例如 1
2
3
4
5
6
7
8
9example(10,20,30) % 60
example(10,20) % 33
example(10) % 15
example() % 6
example(1,1,1,1) % error
可变数量参数
除了使用nargin
来判断实参个数,MATLAB还提供了类似于Python的*args
的特殊参数:varargin
,它会将所有多余参数打包为一个1\times N
的元胞数组,如果没有多余参数则保持为空。在语法上要求varargin
必须是最后一个参数。
例如 1
2
3
4
5
6
7
8
9function total = sum_all(varargin)
% 初始化总和
total = 0;
% 遍历 varargin 中的每一个参数并计算总和
for k = 1:length(varargin)
total = total + varargin{k};
end
end
使用例如 1
2
3
4>> sum_all(1,2)
3
>> sun_all(1,2,3,4)
10
不同输出个数
函数支持多个输出,例如 1
2
3
4
5
6function [a,b] = multi_output()
a=1;
b=2;
end
必须使用对应个数的变量组成的数组来接纳函数的返回值,
1
2
3
4
5>> [x,y] = multi_output()
x =
1
y =
2
如果接收的变量个数过少,则返回值会部分丢失,而且这种情况还不报错!离谱!
1
2
3>> z = multi_output()
z =
1
不同返回值请求
我们可以基于nargout
来获取函数调用时请求的返回值个数,并据此提供合适的返回值,这时至少有两种写法:
- 第一种是函数固定返回多个值,但是仅
nargout
数目的前几个值是有效的,后几个返回无效值(反正也会被接收方丢弃); - 第二种是使用元胞数组实现,返回值是一个元胞数组,数目由
nargout
决定,接收方自动解包。
第一种方式,例如 1
2
3
4
5
6
7
8
9
10
11function [a, b, c] = dynamic_outputs(x)
a = x^2;
b = x^3;
c = x^4;
if nargout < 3
c = [];
end
if nargout < 2
b = [];
end
end
第二种方式,例如 1
2
3
4
5
6
7
8
9
10
11function result = dynamic_outputs(x)
result{1} = x^2;
if nargout > 1
result{2} = x^3;
end
if nargout > 2
result{3} = x^4;
end
end
调用的例子为 1
2
3
4
5
6
7
8% 返回一个值 4
r1 = dynamic_outputs(2);
% 返回两个值 [4,8]
[r1, r2] = dynamic_outputs(2);
% 返回三个值 [4,8,16]
[r1, r2, r3] = dynamic_outputs(2);
这种特殊机制是MATLAB在运行时专门提供的,对于一般的编程语言例如Python和C++,在函数内部是无法获得调用请求的返回值个数的,除非将请求返回值个数作为参数输入。
MATLAB有很多函数都利用了这种机制,来减少所需传递的参数,例如SVD分解
\(A = U S V^T\) 1
2S = svd(A)
[U,S,V] = svd(A)
函数句柄
为了填补前面在语法设计上的坑:
- 函数名称并不是受保护的标识符,我们可以直接创建同名变量
- 无参数调用函数时,可以直接省略括号
()
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 文件的麻烦,可以直接使用内联函数或匿名函数,更推荐使用匿名函数。
内联函数
例如
1 | >> f=inline("x+2") |
其中可以使用x+2
,x^2+y
之类的 MATLAB
表达式组成的字符串,MATLAB 会自动推断自变量(默认为
x),然后直接使用。
匿名函数
匿名函数比内联函数更方便,可以指定输入参数的名称,还可以在表达式中捕获使用工作区的变量,并且效率更高。例如
1
2
3
4>> h=@(x,y)x^2+y^2;
>> h(2,2)
ans =
8
匿名函数的本质就是一个函数句柄,对于匿名无参函数的调用,也是不允许省略括号的。
一种常见的需求是对现有函数进行封装,固定其中的一部分参数,保留剩下的部分参数待定,例如
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)
匿名函数在概念上与其他语言中的lambda表达式是一样的,在实现上则是利用了MATLAB提供的函数句柄机制。
补充
为了提供代码的可读性,不建议使用脚本名称直接调用脚本,建议使用内置函数run
1
run('demo.m');
对于不含任何参数的函数调用,也不建议省略括号()
,保留括号才能体现出函数调用的过程。