SAS Macro Debug宏测试专题之一:宏错误是如何产生的
(2009-02-19 08:51:59)
标签:
sasmacrodebug宏测试教育 |
分类: Macro宏 |
宏错误是如何产生的,如何避免并更正
Macro Bugs - How to Create, Avoid and Destroy Them
原文地址:http://www2.sas.com/proceedings/sugi30/252-30.pdf
转载请注明出处:http://blog.sina.com.cn/s/blog_5d3b177c0100c0xx.html
好的宏的开发比一般的简单的SAS代码开发要困难得多,以下原因使得更加难以对宏进行测试:
宏所生成的不同版本的SAS代码可能会出现错误
程序的错误可能是编写的代码引起的,也可能是宏生成的代码引起的
SAS将宏代码作为文本进行处理,因为我们更难以通过SAS系统对宏进行测试
对于不同的程序,你想要生成的SAS代码也不尽相同
SAS Macro Debug宏测试的一般原理是:首先要了解SAS宏的运行机理,并且如果出现错误,一定要想到是你的代码有问题。更具体地说:首先要看到哪里报了错,然后定位到这个错误,然后理解并修正这个错误。
除了SAS代码的一般错误外,SAS Macro宏变量在传递参数的过程中,也可能会产生错误。另外,对于SAS Macro宏编程来说,程序员要处理两种语言,SAS语言和宏语言,这两种语言的编译和执行的时间不一样,如果理解得不透彻也容易产生错误。
1 时间问题
SAS宏代码执行时有四个时间:
宏编译时间:%MACRO和%MEND之间的代码被读取
宏执行时间:宏编译后生成SAS代码
SAS编译时间:生成的SAS代码进行编译
SAS执行时间:编译后的SAS代码的执行
2 下面来讲一些时间问题的经典案例:
2.1 步骤边界问题:
%macro printplus ( data = ) ;
%mend
%macro nextstep ( data =sashelp.class ) ;
%mend
%printplus( data = sashelp.class );
这里的运行结果是过程步print的title成了"Basic Analysis Variables" 而不是应该的"My important print",为什么呢,原因就是因为上面提到的执行时间的问题。当执行宏printplus时,首先是然后编译title1 "My important print" ;然后编译print过程步,这时,由于没有过程步的边界run,因此,编译器继续编译宏%nextstep(),这时,SAS编译器将会中止,转换为宏编译器,编译宏nextstep并生成SAS代码,再继续由SAS编译器编译生成的SAS代码,这时,编译到title1 "Basic Analysis Variables" ;时,title1的值就改变了,最后SAS代码执行时,就出现了非预期的结果。
解决的方法就是在format _all_ ;后面加run;这样就解决了边界的问题。
2.2 在数据步的宏指令时间问题
我们看一下下面的代码:
data w ;
run ;
我们是不是觉得这里的结果是前五个Y的值为5,后面的Y的值为0;但结果是Y的值都为0,为什么呢?因为我们在执行%let宏指令的时候,就已经进入了宏编译器,并且直到宏编译完,生成的代码就是先赋值5给宏变量x,然后赋值0给宏变量x,然后原理的SAS代码中的do循环其实已经为空,最后将宏变量x的值0赋给变量y,最后y的值就全为0了。
还要注意一点的就是,上面的程序虽然有错误,但是系统并没有提示有任何错误或警告,因此,在写SAS宏代码时一定要非常认真地测试程序
为了更好地说明SAS宏编译时间和SAS代码编译时间的问题,我们再看一下下面这个例子:
data w ;
run ;
这里在SAS日志中会出现ERROR 160-185: 没有匹配的 IF-THEN 子句。
为什么呢?原因其实是一样的,执行到if then语句时,SAS编译中止,转而执行宏编译%let x = 5 ;而这里是赋值语句,最后不生成SAS代码,使得then后面没有SAS语句,因此就出现日志的错误。
所以,我们在%let后面分别加上一个分号,就能解决这个问题:
data w ;
run ;
现在执行就不会再报刚才的错误了。
2.3 CALL SYMPUT的时间问题
CALL SYMPUT是一个非常重要的函数,特别是数据步时传递宏函数的值。我们看下面这个例子:
data w ;
run ;
这里日志报的错误是WARNING: 没有解析符号引用 X。也就是说宏变量x没有初始化,为什么呢?我们可以在do循环前再加一条语句%let x = Not initialized ;这时运行程序,发现可以运行,但得到的y的值为1,这又是为什么?
值为1的原因是因为编译器将Not initialized的值赋给了y,这时的y=1即y为true。而x的值没有初始化的原因是因为编译时,symput并未将值赋给x,因此y也没有值。
要让该程序运行,需要用到symget函数,它可以将宏变量的值赋给目标参数,如下程序所示:
data w ;
run ;
因此,我们在写SAS宏时,一定要分清楚这四个时间,不要将SAS执行时才能得到的值赋给SAS编译时就需要的变量,不要将SAS编译时的循环来控制宏编译时的语句。
2.4 单引号问题
下面这个例子:
%let root = c:\project\data ;
filename in '&root\stuff.dat' ;
这里,filename将不会被执行,因为SAS宏编译器并不能识别这里的&root,一个简单的解决方法就是将单引号改为双引号:filename in "&root\stuff.dat" ;当我们只能用单引号时,我们就得应用宏引用宏数了:filename in %unquote(%str(%')&root\stuff.dat%str(%')) ;
CALL EXECUTE时间问题
跟上一个问题差不多
2.5 If还是%if
在写SAS宏时到底该用If还是%if,这个问题经常让很多人困惑。这里说一下两者的区别:%if只能用于宏,它决定生成哪些SAS代码,而if必须用于数据步,在SAS代码执行时它决定哪些代码将被执行。
看下面这个例子
%macro bug ( dummy = );
data _null_ ;
run ;
%mend
%bug()
这里的%if应该不是我们需要的,因为我们并不需要决定是否生成put x= ;这句代码,而是要决定是否运行put x= ;这句代码。
3 执行宏的环境
这里最好看一看以前关于宏编译的文章。这里主要强调一下:尽量不要用全局宏变量,主要原因是全局宏变量的值很容易被无意修改,例如某个宏的局部宏变量与全局宏变量相同时,就会修改,这里再用这个全局宏变量时就会产生意想不到的效果。
SAS宏设计问题参见以前的文章。下面介绍本文的重点:
4 宏测试工具
%PUT statement
MPRINT option
MFILE option
MLOGIC option
4.1 %PUT语句
这个太有用了,当要查看某个宏变量的值时,就用这个语句。例如%put var=>>>&var<<< ;就可以在日志中查看宏变量var的值。
这个语句在循环以及条件语句中特别有用,因为这里宏变量的值可能并不是预期的结果,这时如果将宏变量的值都输出到日志中,我们就能检查到程序是否按预期在进行。
%put语句常用到的系统变量有:_USER_, _LOCAL_, _GLOBAL_, _ALL_, _AUTOMATIC_。这个前面也有介绍。
4.2 MPRINT选项
这个选项是我经常用的,它可以将SAS宏生成的SAS代码输出到日志中,这样就可以看到SAS到底在执行哪些语句了。特别是当结果出现偏差时,我们得看宏是否按我们的预期产生代码。例如下面的程序出现的错误是宏编程时经常出现的。
option mprint;
%macro macbug ( proc = freq , debug = 1 ) ;
运行后我们可以看一下日志:
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
NOTE: 数据集 WORK.W 有 1 个观测和 3 个变量。
NOTE: “DATA 语句”所用时间(总处理时间):
debugging: nvars=3
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
NOTE: 由调用宏“MACBUG”生成行。
7
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
ERROR 79-322: 期望‘;’。
NOTE: SAS 系统由于错误而停止了该步的处理。
WARNING: 数据集 WORK.TEST 可能不完整。该步停止时,共有 0 个观测和 4 个变量。
WARNING: 数据集 WORK.TEST 由于该步已停止,而没有被替换。
NOTE: “DATA 语句”所用时间(总处理时间):
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
NOTE: 数据集 WORK.TEST 中没有观测。
NOTE: “PROCEDURE FREQ”所用时间(总处理时间):
首先我们看到很多MPRINT(MACBUG):开头的语句,这些就是mprint选项所生成的SAS宏编译后的SAS代码。我们日志里唯一报错的地方:
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
ERROR 79-322: 期望‘;’。
从日志里我们就很容易看出,x = round ( x ) output test ;, x = round ( x ) 这里差了一个分号,那为什么我们在原宏代码里不容易发现这个错误呢,我们可以看一下这里的原代码:
%if &proc = freq %then
这里的编译过程如下:首先在SAS宏编译器里编译
%if &proc = freq %then
注意这里是有分号的,这时SAS宏编译后得到的SAS代码是x = round ( x ),这时就没有分号了,然后后面直接接着output test ;这一句,因此就出现了刚才的错误。正确的代码应该是加两个分号:
%if &proc = freq %then
因此用mprint选项能帮助我们识别SAS宏中一些不容易发现的错误。
4.3 MFILE选项
将刚才那个分号加上后,运行代码,就可以在日志中看到以下note:
debugging: nvars=3
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
MPRINT(MACBUG):
NOTE: 缺失值的生成是对缺失值执行操作的结果。
NOTE: 数据集 WORK.TEST 有 10 个观测和 4 个变量。
NOTE: “DATA 语句”所用时间(总处理时间):
这里的行:列103:93是我们在日志里无法知道的,也就没法给我们更多的帮助了。这时就需要mfile选项,它可以让你将生成的SAS代码存入一个文件中,运用这个选项时,你需要FILEREF, MPRINT:
filename mprint "c:\junk\macbug.sas" ;
data _null_ ; file mprint ; run ;13
options mprint mfile ;
%macbug()
options nomfile ;
%include mprint / source2 ;
这时我们就看到了提示的这一行的代码:z = x + y ;这里因为y为缺失值,故有些提示。
这里还介绍了两个宏% debugexec和% debugsetup,可以用来控制输出哪一段SAS宏的代码:
%macro debugsetup (run=1, file="c:\junk\macbug.sas", freshstart=1) ;
%mend
%macro debugexec(run=1, file="c:\juk\macbug.sas") ;
%mend
4.4 MLOGIC选项
MLOGIC选项主要是看宏逻辑的,特别是有循环时的逻辑。看下面的代码:
%macro words1 ( n = ) ;
%put %words1( n = 3 ) ;
%put %words2( n = 3 ) ;
%repeatline ( n = 3 , mac =
1 )
%repeatline ( n = 3 , mac =
2 )
这里我们看日志的输出结果:
116
117
118
119
abc1
120
121
abc1
122
123
124
abc1
abc1
abc1
125
126
abc1
我们看119和121行中,运行的结果都是一行,但为什么124 上的宏运行结果是三行,而126上的运行结果是一行。
这时我们在原代码中的最后一行即%repeatline ( n = 3 , mac = 2
)
157
158
MLOGIC(REPEATLINE): 准备开始执行。
MLOGIC(REPEATLINE): 参数 N 的值为 3
MLOGIC(REPEATLINE): 参数 MAC 的值为 2
MLOGIC(REPEATLINE): %DO 循环正准备开始;索引变量为 I;起始值为 1;截止值为 3;增量值为 1。
MLOGIC(REPEATLINE): %IF 条件 &mac = 1 为 FALSE
MLOGIC(REPEATLINE): %PUT %words2( n = 3 )
MLOGIC(WORDS2): 准备开始执行。
MLOGIC(WORDS2): 参数 N 的值为 3
MLOGIC(WORDS2): %DO 循环正准备开始;索引变量为 I;起始值为 1;截止值为 3;增量值为 1。
MLOGIC(WORDS2): %DO 循环的索引变量 I 当前为 2;循环将会再迭代。
MLOGIC(WORDS2): %DO 循环的索引变量 I 当前为 3;循环将会再迭代。
MLOGIC(WORDS2): %DO 循环的索引变量 I 当前为 4;循环将not会再迭代。
MLOGIC(WORDS2): 准备结束执行。
abc1
MLOGIC(REPEATLINE): %DO 循环的索引变量 I 当前为 5;循环将not会再迭代。
MLOGIC(REPEATLINE): 准备结束执行。
注意这里:
MLOGIC(WORDS2): %DO 循环的索引变量 I 当前为 4;循环将not会再迭代。
MLOGIC(WORDS2): 准备结束执行。
abc1
MLOGIC(REPEATLINE): %DO 循环的索引变量 I 当前为 5;循环将not会再迭代。
为什么最后一行会出现“%DO 循环的索引变量 I 当前为 5”;一般说来循环三次的话,会出现“%DO 循环的索引变量 I 当前为 4”,即第一行的内容。这就是因为宏变量i的值不但在宏%WORDS2中被累加,并且在宏% REPEATLINE中也被累加。当然,我们将宏变量全用%local定义就不会出现这个问题了。我们还可以用不同的宏变量来进行循环以规避此类错误。