宏错误是如何产生的,如何避免并更正
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 = ) ;
title1 "My
important print" ;
proc print
data = &data ;
format _all_ ;
%
nextstep
()
%mend
printplus ;
%macro
nextstep ( data =sashelp.class )
title1 "Basic
Analysis Variables" ;
proc means
data = &data ;
run ;
%mend
nextstep ;
%
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 ;
do obs =
1
to
10
;
if obs <=
5
%let x = 1 ;
end ;
%let x = 2;
end ;
y = &x ;
output ;
end ;
run
;
我们是不是觉得这里的结果是前五个Y的值为5,后面的Y的值为0;但结果是Y的值都为0,为什么呢?因为我们在执行%let宏指令的时候,就已经进入了宏编译器,并且直到宏编译完,生成的代码就是先赋值5给宏变量x,然后赋值0给宏变量x,然后原理的SAS代码中的do循环其实已经为空,最后将宏变量x的值0赋给变量y,最后y的值就全为0了。
还要注意一点的就是,上面的程序虽然有错误,但是系统并没有提示有任何错误或警告,因此,在写SAS宏代码时一定要非常认真地测试程序
为了更好地说明SAS宏编译时间和SAS代码编译时间的问题,我们再看一下下面这个例子:
data
w ;
do obs =
1
to
10
;
if obs <=
5
then
%let x = 5 ;
%let x = 0 ;
y = &x ;
output ;
end ;
run
;
这里在SAS日志中会出现ERROR 160-185: 没有匹配的 IF-THEN 子句。
为什么呢?原因其实是一样的,执行到if then语句时,SAS编译中止,转而执行宏编译%let x =
5 ;而这里是赋值语句,最后不生成SAS代码,使得then后面没有SAS语句,因此就出现日志的错误。
所以,我们在%let后面分别加上一个分号,就能解决这个问题:
data
w ;
do obs =
1
to
10
;
if obs <=
5
then
%let x = 5 ;;
x = 0 ;;
y = &x ;
output ;
end ;
run
;
现在执行就不会再报刚才的错误了。
2.3 CALL SYMPUT的时间问题
CALL SYMPUT是一个非常重要的函数,特别是数据步时传递宏函数的值。我们看下面这个例子:
data w ;
do obs = 1 to 10 ;
if obs <= 5 then
call symput ( "x" , "5" ) ;
call symput ( "x" , "0" ) ;
y = &x ;
output ;
end ;
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 ;
do obs = 1 to 10 ;
if obs <= 5 then
call symput ( "x" , "5" ) ;
call symput ( "x" , "0" ) ;
y = input ( symget ( "x" ), best12. ) ;
output ;
end ;
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_ ;
x = 1 ;
%if x = 1 %then
%do ;
put x= ;
%end ;
run ;
%mend
bug ;
%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
) ;
data w ;
retain a b c
0
;
if a =
1
then ;
call symputx ( "nvars" ,
3
) ;
run ;
debugging: nvars=&nvars ;
data test
retain y
.
;
do i =
1
to
10
;
x = ranuni(
0
) ;
z = x + y ;
%if &proc = freq %then
x = round ( x ) ;
output test ;
end ;
run ;
proc freq data
= test ;
table x ;
run ;
%mend
macbug ;
%
macbug
(
proc
= freq , debug =
1
) ;
运行后我们可以看一下日志:
MPRINT(MACBUG):
data
w ;
MPRINT(MACBUG):
retain a b c
0
;
MPRINT(MACBUG):
if a =
1
then ;
MPRINT(MACBUG):
call symputx ( "nvars" ,
3
) ;
MPRINT(MACBUG):
run ;
NOTE: 数据集 WORK.W 有
1
个观测和
3
个变量。
NOTE: “DATA 语句”所用时间(总处理时间):
0.01
秒
debugging: nvars=
3
MPRINT(MACBUG):
data
test ;
MPRINT(MACBUG):
retain y
.
;
MPRINT(MACBUG):
do i =
1
to
10
;
MPRINT(MACBUG):
x = ranuni(
0
) ;
MPRINT(MACBUG):
z = x + y ;
NOTE: 由调用宏“MACBUG”生成行。
output test
end ;
run
;
------
MPRINT(MACBUG):
x = round ( x ) output test ;
MPRINT(MACBUG):
end ;
MPRINT(MACBUG):
run ;
ERROR
79
-
322
: 期望‘;’。
NOTE: SAS 系统由于错误而停止了该步的处理。
WARNING: 数据集 WORK.TEST 可能不完整。该步停止时,共有
0
个观测和
4
个变量。
WARNING: 数据集 WORK.TEST 由于该步已停止,而没有被替换。
NOTE: “DATA 语句”所用时间(总处理时间):
0.00
秒
0.00
秒
MPRINT(MACBUG):
proc
freq data = test ;
MPRINT(MACBUG):
table x ;
MPRINT(MACBUG):
run ;
NOTE: 数据集 WORK.TEST 中没有观测。
NOTE: “PROCEDURE FREQ”所用时间(总处理时间):
0.00
秒
0.00
秒
首先我们看到很多MPRINT(MACBUG):开头的语句,这些就是mprint选项所生成的SAS宏编译后的SAS代码。我们日志里唯一报错的地方:
MPRINT(MACBUG):
x = round ( x ) output test ;
MPRINT(MACBUG):
end ;
MPRINT(MACBUG):
run ;
ERROR
79
-
322
: 期望‘;’。
从日志里我们就很容易看出,x = round ( x ) output test ;, x =
round ( x ) 这里差了一个分号,那为什么我们在原宏代码里不容易发现这个错误呢,我们可以看一下这里的原代码:
%if &proc = freq %then
x = round ( x ) ;
output test ;
这里的编译过程如下:首先在SAS宏编译器里编译
%if &proc = freq %then
x = round ( x ) ;
注意这里是有分号的,这时SAS宏编译后得到的SAS代码是x = round ( x
),这时就没有分号了,然后后面直接接着output test ;这一句,因此就出现了刚才的错误。正确的代码应该是加两个分号:
%if &proc = freq %then
x = round ( x ) ; ;
output test ;
因此用mprint选项能帮助我们识别SAS宏中一些不容易发现的错误。
4.3 MFILE选项
将刚才那个分号加上后,运行代码,就可以在日志中看到以下note:
debugging: nvars=3
MPRINT(MACBUG):
data test ;
MPRINT(MACBUG):
retain y . ;
MPRINT(MACBUG):
do i = 1 to 10 ;
MPRINT(MACBUG):
x = ranuni(0) ;
MPRINT(MACBUG):
z = x + y ;
MPRINT(MACBUG):
x = round ( x );
MPRINT(MACBUG):
output test ;
MPRINT(MACBUG):
end ;
MPRINT(MACBUG):
run ;
NOTE: 缺失值的生成是对缺失值执行操作的结果。
指定每个位置的方式: (次数)(行:列)。
103:93
NOTE: 数据集 WORK.TEST 有 10 个观测和 4 个变量。
NOTE: “DATA 语句”所用时间(总处理时间):
0.48 秒
0.01 秒
这里的行:列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) ;
%if &run
%then
%do ;
filename mprint &file ;
%if &freshstart = 1 %then
%do ;
_null_ ; file mprint ; run ;
%end ;
options mprint mfile ;
%end ;
%mend
debugsetup ;
%macro debugexec(run=1, file="c:\juk\macbug.sas") ;
%if &run
%then
%do ;
options nomfile ;
%include mprint / source2 ;
filename mprint clear ;
%end ;
%mend
debugexec ;
4.4 MLOGIC选项
MLOGIC选项主要是看宏逻辑的,特别是有循环时的逻辑。看下面的代码:
%macro
words1 ( n = ) ;
%do n =
1
%to &n ;
abc&n
%end ;
%mend
words1 ;
%macro
words2 ( n = )
%do i =
1
%to &n ;
abc&i
%end ;
%mend
words2 ;
%macro
repeatline ( n = ,
mac =
1
) ;
%do i =
1
%to &n ;
%if &mac =
1
%then
%put %words1( n = 3 ) ;
%else
%put %words2( n = 3 ) ;
%end ;
%mend
repeatline ;
options nomlogic ;
%put %words1( n = 3 ) ;
%put %words2( n = 3 ) ;
%
repeatline
( n =
3
, mac =
%
repeatline
( n =
3
, mac =
这里我们看日志的输出结果:
116
options
nomlogic ;
119
%put %words1( n = 3 ) ;
121
%put %words2( n = 3 ) ;
124
%repeatline ( n = 3 , mac = 1
)
;
126
%repeatline ( n = 3 , mac = 2
)
;
我们看119和121行中,运行的结果都是一行,但为什么124 上的宏运行结果是三行,而126上的运行结果是一行。
这时我们在原代码中的最后一行即%repeatline ( n = 3 , mac = 2
;之前加入以下语句:options mlogic ;进行宏逻辑测试,在SAS日志中出现以下结果:
157
options
mlogic ;
158
%repeatline ( n = 3 , mac = 2
)
;
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): 准备结束执行。
MLOGIC(REPEATLINE): %DO 循环的索引变量 I 当前为
5;循环将not会再迭代。
MLOGIC(REPEATLINE): 准备结束执行。
注意这里:
MLOGIC(WORDS2): %DO 循环的索引变量 I 当前为 4;循环将not会再迭代。
MLOGIC(WORDS2): 准备结束执行。
MLOGIC(REPEATLINE): %DO 循环的索引变量 I 当前为
5;循环将not会再迭代。
为什么最后一行会出现“%DO 循环的索引变量 I 当前为 5”;一般说来循环三次的话,会出现“%DO 循环的索引变量 I 当前为
4”,即第一行的内容。这就是因为宏变量i的值不但在宏%WORDS2中被累加,并且在宏%
REPEATLINE中也被累加。当然,我们将宏变量全用%local定义就不会出现这个问题了。我们还可以用不同的宏变量来进行循环以规避此类错误。
新浪简介
|
About Sina
|
广告服务
|
联系我们
|
招聘信息
|
网站律师
|
SINA English
|
产品答疑