添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

宏错误是如何产生的,如何避免并更正

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 | 产品答疑