五、过程式编程和调试技巧
五、过程式编程和调试技巧 返回目录页
1、循环
2、判断
3、模块化
4、循环+判断
5、调试技巧
MMA支持所有标准的过程式编程(Procedural programming)结构,但往往通过集成把它们扩展到更为普遍的符号编程环境中。
-------------------------------------------------------------------------
1、循环
MMA支持多种循环,常用的是Do循环与While循环。
----------------------------------------
特殊形式的赋值
i++ i 加 1
i-- i 减 1
++i 先给 i 加 1
--i 先给 i 减 1
i+=di i 加 di
i-=di i 减 di
x*=c x 乘以 c
x/=c x 除以 c
x=y=value 对 x 和 y 赋同一值
{x,y}={value,value} 对 x 和 y 赋不同的值
{x,y}={y,x} 交换 x 和 y 的值
----------------------------------------
迭代器。
没啥新鲜的,以前在用Table函数时,就已经用到过。
最后一行指控制多重循环。
----------------------------------------
语句块
因为在MMA中,只有表达式(唯一的例外是序列,但序列不能单独使用),所以语句块,也是表达式块。
语句块的作用,是将多个语句组合起来,而形成一个语句(表达式)。
在MMA中,没有专用的语句块符号,如Pascal语言中的Begin/End,如C语言中的{}。
如果实在要用,就用()。而且有时候是必须的,而有时候又不是必须的。
test[n_, i_] := (t = 2^n; Plus[t, i])
test[2, 2]
前一行中,()是必须的。()内两个语句,第一句运行没输出。第二句运行也没输出,但计算结果作为函数的返回值返回了——从而在函数被调用的时候,产生输出。
Do[
(Print[a];
Print[b];
Print[c];
Print[d]),
{1}]
这个()是多余的,不是必须。用上与没用上效果一样。
但用上了之后,代码的可读性似乎增加了。
语句块是支持嵌套的:
(((a; b; c); (d; e)); (f; g))
但不常用,基本上见不到这种用法。(倒是让人想到了Lisp语言家族的表达式)
----------------------------------------
Do 循环。
Do循环的构成非常简洁:
Do[expr,迭代器]
Do[Print[你好], {2}]
Table[你好, {2}]
在语法上,Do函数与Table很像。Table函数创造出表,Do函数运行表达式数次。
expr部分,可以是单条语句,也可以是语句块。
Do[Print[你好吗]; Print[很好], {2}]
这个语句块中的()不是必须,但如果用上也行。
Do[(Print[你好吗]; Print[很好]), {2}]
因为步长可以是负值,实现“Down to”就很容易。
Do[Print[i], {i, 10, 8, -1}]
迭代器实际上是很灵活的,不一定用整数,可以用符号值。
Do[Print[n], {n, x, x + 3 y, y}]
实际多重循环,也是易如反掌:
Do[Print[{i, j}], {i, 3}, {j, i - 1}]
在MMA中,循环又叫迭代。
t = x; Do[t = 1/(1 + t), {5}]; t
Nest[1/(1 + #) &, x, 5]
这两行语句的效果是一样的。
Do[Print[RandomInteger[{1, 30}]], {10}]
输出10个1到30之内的伪随机数。
其实有内置函数来做这事:
RandomSample[Range[30], 20]
内置函数不熟悉的话,就是比较吃亏。
要注意的一点是,Do函数本身没有返回值(即返回Null)。
Null
是一个符号,用来指明一个表达式或结果不存在。 在普通输出中它不显示。
当 Null 显示为一个完全输出表达式时,没有任何输出显示。
Null
得:无输出。
所以为了观察循环体内的运算过程,我们用了很多Print输出。
----------------------------------------
While 循环。
基本格式也极其简洁:
While[test,body]
重复计算 test,然后是 body,直到 test 第一次不能给出 True。
body部分,可以是单条语句,也可以是语句块。语句块用不用(),均可。
n = 1; While[n < 3, Print[n]; n++; Print[hello]]
n = 1; While[n < 3, (Print[n]; n++; Print[hello];)]
最后一个分号,写不写上均可。
n = 1; While[n < 4, Print[n]; n++]
写成以下这样,效果都一样:
n = 1; While[n < 4, (Print[n]; n++;)]
n = 1; While[n < 4, n++; Print[n - 1];]
n = 1; While[n++ < 4, Print[n - 1]]
{a, b} = {27, 6};
While[b != 0, {a, b} = {b, Mod[a, b]}];
a
辗转相除法求最大公约数。
类似的方法,可以求出一系列的Fib数。
{a, b} = {0, 1}; n = 0;
While[n++ < 10, {a, b} = {b, a + b}; Print[b]]
Break 退出 While:
{a, b} = {0, 1}; n = 0;
While[True, n++; If[n > 10, Break[]]; {a, b} = {b, a + b}; Print[b]]
While 返回的最后结果是 Null。
-------------------------------------------------------------------------
2、判断
判断指一些条件函数(conditional function),依据条件(condition)的不同返回值(True/False/不确定)而返回不同表达式的值。
----------------------------------------
IF 判断。
If[condition,t]
如果 condition 计算为 True 给出 t,如果它计算为 False,则给出 Null。
If[condition,t,f]
如果 condition 计算为 True 给出 t,如果它计算为 False 给出 f。
If[condition,t,f,u]
如果 condition 计算既不为 True 也不为 False 给出 u。
奇怪哈,居然条件计算结果有第三种可能?
这就是符号计算的特色了。看:
a =.; b =.;
If[a == b, t, f, u]
得u
因为a、b只是符号,还不知道是啥值,所以a与b是不是相等?不知道。
condition部分,可以是语句块。()不是必须。
If[(a = 1; b = 2; a == b), t, f, u]
If[a = 1; b = 2; a == b, t, f, u]
分号、逗号一大堆,加上清楚好多。
当然,顺理成章,t/f/u部分,都可以是语句块。
a =.; b =.;
If[a == b, t, f]
得:If[a == b, t, f]
如果 condition 计算既不为 True 也不为 False,If[condition,t,f] 保持不计算。
abs[x_] := If[x < 0, -x, x]
abs /@ {-1, 0, 1}
If[TrueQ[2 < 4], 1, 0]
得:1
If[TrueQ[2 < 1], 1]
得:Null(无输出)
If支持嵌套。
如果x为1,得a,x为2,得b,x为3,得c,x为其他,得other。用If嵌套这样写:
x = 20;
If[x == 1, a,
If[x == 2, b,
If[x == 3, c, other]]]
其实这样写也可以。但用以下函数Which更好。
----------------------------------------
Which
看程序就很明白了。
x=2;
Which[x==1,a,x==2,b,x==3,c,True,other]
----------------------------------------
Switch
一样,看程序:
Switch[x, 1, a, 2, b, 3, c, _, other]
-------------------------------------------------------------------------
3、模块化
MMA一般假设变量是全局变量。即每次使用 x 等名字时,MMA总认为在调用同一对象。
然而在编程时,不需要将所有变量都作为全局变量。很多时候,我们希望用局部变量,比如在自定义函数体内。
在MMA中,可以用Module函数定义局部变量,可以用With函数定义局部常量。
----------------------------------------
Module
Module[{x,y,...},body] 具有局部变量 x,y,...的模块
t = 1;
Module[{t=t+2}, t=t+2; Print[t]];
t
这个程序很能说明问题了。
全局变量t,可以到Module的{}中去,而局部变量t是仅模块内可用的。
仔细观察可以看到,两个t的颜色是不同的。
如图,在“偏好设置”中,把三种变量设置成不同的颜色,是很重要的。
Module函数是有返回值的,即body表达式的返回值。
t = 1;
x = Module[{t = t + 2}, (t = t + 2; Print[t]; t + 2)];
t
x
body可以是语句块,这个语句块可以加上(),也可以不加。
----------------------------------------
With
在 Module 中可以定义局部变量,这样我们可以对其赋值并且改变其值。
然而,通常我们所需要的是局部常量,对其我们仅需要赋值一次。
With 结构可以建立局部常量。
With[{x=x0,y=y0...},body] 定义局部常量 x,y,...
这样是不可以的,因为局部常量不可以在body中再改变。
t = 1;
With[{t = t + 2}, t = t + 2; Print[t]];
t
这样就可以了:
t = 1;
With[{t = t + 2 + 2}, Print[t]];
t
可以将 With 理解为 /. 运算的推广:
t = 1;
t /. t -> t + 2 + 2
t
With 类似于局部变量仅赋值一次的 Module 的特殊形式。
With函数有返回值,即返回body表达式的输出值。
t = 1;
x = With[{t = t + 2 + 2}, Print[t]; t + 2];
t
x
还有个模块函数叫块(Block)。因为Block与Module有细微差别,这里不展开,有兴趣的小朋友可以研究帮助文档。
-------------------------------------------------------------------------
4、循环+判断
过程式编程,用循环+判断,可以解决很多问题。
如果要自定义函数,则要结合模块。
这里举几个栗子。
----------------------------------------
fib[n_] := Module[{f},
f[1] = f[2] = 1;
f[i_] := f[i] = f[i - 1] + f[i - 2];
f[n]
]
fib[5]
----------------------------------------
对最大公约数(GCD)使用初始化局部变量的欧几里德(Euclid)算法。
通常叫辗转相除法。
gcd[m0_, n0_] := Module[{m = m0, n = n0},
While[n != 0, {m, n} = {n, Mod[m, n]}];
m
]
gcd[120, 48]
----------------------------------------
筛法求素数。
primes2[n_] :=
Module[{primes = Range[n], p = 2},
Print["Start: primes = ", primes, ", p=", p];
While[p != n + 1, Do[primes[[i]] = 1, {i, 2 p, n, p}];
p = p + 1;
While[p != n + 1 && primes[[p]] == 1, p = p + 1];
Print["In loop: primes=", primes, ", p= ", p];];
Select[primes, (# != 1) &]]
primes2[12]
这个程序输出了中间过程,帮助理解筛法的思路。
以下程序加了中文注释,是不可以运行的,只能用于理解:
primes2[n_] :=
Module[{primes = Range[n], p = 2},
Print["Start: primes = ", primes, ", p=", p]; 输出初始表,及p值
While[p != n + 1,
Do[primes[[i]] = 1, {i, 2 p, n, p}]; 置1是在这里进行的,i从2p到n,步长为p
p = p + 1;
While[p != n + 1 && primes[[p]] == 1, p = p + 1]; 如果越界,或者p位置上已经是1,跳过,再p+1
Print["In loop: primes=", primes, ", p= ", p]; 输出循环时的表及p值
];
Select[primes, (# != 1) &]] 将置1的值从表中删除,剩下的就是素数
primes2[12]
把中间过程去掉:
primes[n_] :=
Module[{primes = Range[n], p = 2},
While[p != n + 1, Do[primes[[i]] = 1, {i, 2 p, n, p}];
p = p + 1;
While[p != n + 1 && primes[[p]] == 1, p = p + 1];];
Select[primes, (# != 1) &]]
primes[100]
这里有两个primes,名称相同,意义完全不同(一个是全局变量、另一个是局部变量)。在程序中,显示的颜色也不同。
如果用内置函数,一句就行:
Prime[Range[25]]
-------------------------------------------------------------------------
5、调试技巧
MMA中,有自带的调试器,菜单中:计算/调试,就可以打开调试器。这里不展开。
这里说一些小技巧。
A、如果发现莫名其妙的结果,多数是某个名称在前面被取绰号了,赶紧用Clear试试。
B、如果函数名记不全,有自动完成功能,很好用,可以先输入一两个字母(注意,大小写敏感的)。
C、多按F1,查帮助文档。MMA的帮助文档,可能是史上最强大的。
D、表达式的计算过程很难理解的时候,或者表达式很长的时候,用绰号来拆分。
E、用Trace函数来跟踪输出。
F、在程序中加入Abort函数来终止程序执行。加入到不同部分,可以分步调试。
G、很重要的一点是,用Print函数来输出中间结果,从而观察中间结果。输出中间结果是万精油,各种语言都可以通过此大法来调试。
H、长时间调试程序容易疲倦。疲倦时就休息,坚持下去经常效率并不高。
I、程序长时间运行,得不出结果,很可能进入死循环了,按 Alt+. 终止程序执行。
J、MMA是很庞大的系统,短时间内不精通不用心急。先掌握简单的。
K、越扯越远。。打住
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
23:44 2016-10-29
+++++++++++++++++++++++++++
扩展阅读:为什么在 Mathematica 中使用循环是低效的?