1、SAS Macro编码的好习惯之一能被大家都使用的 SAS Macro编写指南Guidelines on Writing SAS Macros for Public Use原文地址:http:/ SAS的途径,我们经常在网上看到一些公开的且优秀的源码。今天我们就来讲一些 SAS Macro编写的规则,有了这些规则,你的 SAS代码将更容易被别人读懂,从而让更多的人学习和重用。1 文档化文档化一直是很重要的东西,特别是对 SAS Macro,因为只有这样才有更多的人知道这些 Macro如何才能正地确地执行,并且结果是他们所需要的。下面介绍由 Whitney (1996)提出来的 SAS Macr
2、o的文档模板:上面这些信息可以根据实际情况选择性地列举出来。其次,为了使代码更有可读性,我们还可以将功能、变量等名字取得更具清晰明白,从而让人能根据其名字就知道这个函数或变量是干什么用的。例如要创建一个重复的数据集,我们可以用 PRINT_DUPS来命名这个数据集,而不是用CheckData来命名,还有就是可以用 SAS的保留字来作用后缀或前缀等,例如,SAS Macro里的数据集参数,我们可以用 In_Data或 Out_Data等,这样我们就很清楚地知道这是数据集,其它的如 Keep、Where、Nobs 等。一个非常重要的一点是命名一定要有连续性,也即风格要一致。最后就是 SAS Mac
3、ro的函数或变量最好用描述性的名字命名,这样在后期的Debug或应用时都能帮助理解,例如一般不用 i或 j等一般性的变量来命名,取而代之以 year,visit 等来命名。另外我们还可以加一些选项来对宏进行 Debug,这些选项如 MPRINT,MLOGIC 和SYMBOLGEN等,以及用%put 进行 Debug。这一块我们讲专门介绍如何 Debug Sas Macro。2获取错误信息我们可以被动地通过 SAS自己的错误提示来得到错误信息,另一方面,我们可以主动地写一些代码来获得错误信息。当然我们很难预测并获取所有的错误信息,但是有一些错误是很标准化的,这些错误我们可以尽量自己获取。例如我们
4、可以通过%LENGTH 来判断宏变量是否被指定:%if %length(%put ERROR: Value for macro parameter DATA is missing ;%goto finish ;%end ;%finish:这样,如果没有宏变量,则其 length将为空或 0,这样就会生成一条错误信息并结束过程。另一个例子是判别是否指定的数据集存在:%let error = 0 ;%if %sysfunc(exist(%put ERROR: data set %let error = 1 ;%end ;%if %finish:这些例子都比较简单,我们在应用于的时候可能会遇到更复杂
5、的情况。其它的应用包括判别变量是否为字符型,是否库名或格式合法等等。为了让主 Macro不显得太复杂,我们可以写一些子宏来完成这些功能。还有一个比较好的习惯是我们可以预先赋给宏变量一些默认值,而不是完全由用户来赋值。并且我们还可以增加赋值的变化来减少输入值可能的错误,例如:%let debug = %upcase(通过将 debug全部转化为大写,从而用户可以输入任意的大小字母,对结果都无影响。另一个例子:%let debug = %upcase( %substr(我们只取第一个字母,且将其大写,这样,用户可能多输入几个字母,但对结果亦无影响。3 减少一些可能引起歧议的句子由于程序总会以程序员
6、没有考虑到的方式运行,比如建立了一个已存在的数据集,这样就很可能将原来的数据集覆盖了。因为,一个简单的方式是我们建立数据集时,一定要先检查一下是否已存在这个数据集,另外,对于 Macro生成的数据集,当我们不再需要它时,一定要将其删除。另一个经常发生的问题是全局宏变量,在 Macro中,其值被修改了:%let x = 5 ;%macro check ;%* %local x ; %let x = 1 ; %mend check ;%check%put x = 这里的 x为 1而不是 5。这就是因为全局宏变量 x的值在宏 check里被改为了1。因此我们在创建全局宏变量时,一定要取一个比较唯一的
7、名字,如前面加_,而局部宏时,我们最好用%Local 显性定义。对于宏变量的范围问题前面已有讨论,这里就不再具体介绍了。还有一个要注意的地方就是 title和 footnote用完后一定要注意将其关闭掉:title1 Original title;%macro test ;proc sql noprint;select number, text into: num ,:text from dictionary.titleswhere number = 1;quit ;%let num = %let text = title1 Macro title;title%mend test; 这里最后的
8、 title1为Original title而不是Macro title,因为先前定义的 title1的值还被宏保存着。4 好用好用这个属性太重要了哈。我们介绍一下宏参数主要有两种方式:positional和 keyword%macro recode (data, out, var, groups );%macro recode (data =, out =, var =, groups = );其引用方式为:%recode(sales, salesr, revenue, 5)%recode(data = sales, out = salesr, var = revenue, groups =
9、 5 )Keyword比 positional有很多好处:首先 Keyword更直观,我们可以很清楚地知道 sales是一个数据集,revenue为一个变量,从而减少错误其次我们可以先定义一些默认值,例如%macro recode(data=, out=, var, groups = 5) ;我们就可以只对前三个变量进行赋值了在写宏时,我们一定要让宏变量尽量少,因为宏变量太多就太复杂。如果宏变量确实多,那就看看能不能给一些变量赋予默认值。写宏时另一个好习惯是对宏变量进行注释:%print_vars (data = , var = , nobs = , byvar = )另外,我们还要尽量减少没
10、有%的宏,例如:options implmac ;data test ;a = 1 ;run;%macro printdat(data) / stmt ;proc print data = run;%mend printdat ;printdat test ;这个宏看起来费劲,并且还没实现什么功能,并且宏里面没有一个%,所以这不是一个很好的编程习惯5 权衡与思考首先是当我们写宏时,也要考虑一下大家的实际情况,比如在第九版我们创建一个宏变量,可以简单地用 call symputx (count, count) ;实现,但在第6版时,完整代码是 call symput (count, trim(l
11、eft(put(count,8.) ;,其它情况包括我们可能有 graph qc or ets等产品,而有的却只有 base,这时你写的代码可能就不会被这些人使用。另一个宏的重要特征是 robust,就是可以用到很多平台上,但这样会浪费我们很多时间,这时,我们最好根据实际情况来定是否需要写这样的宏。最后是一些思考,供大家参考:(1)对文档标准,变量命名习惯等要保持连续性(2)在发布程序之前一定要完整地测试你的代码,而不能仅仅 debug。要完整地测试其功能和效率。作者举了个例子,他写了个 proc freq的宏,测试时没事,但用在大数据量操作时,就死掉了,结果只能重写。(3)一定要跟你的客户有
12、个良好的交流,了解客户真正需要什么功能,不然写一个没人用的宏是没有意义的(4)要将公开的宏放在一个只读的文件夹下,这样就能避免对其不经意的更改(5)定期重新看看你写的宏,这样可能会想到更好的方式来修改你的宏。SAS Macro编码的好习惯之二 SAS宏设计问题(1)SAS Macro Design Issues原文地址: SAS宏代码,但什么时候我们应该用 SAS宏来编码以解决我们的问题呢?这篇文章主要考虑了两个重要的问题:一个好的宏应该具备哪些特征,以及如何才能写出一个有用且清晰的宏。1 关于宏变量宏变量的定义有很多种方法,如%let 等,前面的文章已有介绍,这里就不多说,我主要讲一下作者的
13、一些比较好的编写宏的理念。下面从一个简单的宏赋值语句说起:%let data = lib.mydata ;这个语句就是生成一个宏变量 data,其值为 lib.mydata。在这里要提到的就是SAS宏的编译原理,这个在前面已有介绍。SAS 宏编译器前所有的代码都当作文本来处理,因此上面的宏变量 data的值 lib.mydata是没有特殊意义的,因此也不用加双引号” ”。但是在 SAS过程步编译器里,这些值是有意义的,比如lib.mydata中的 lib是库名,mydata 是表名。因此,另一个例子中:libname lib “c:myprojectdat“ ;这里的库 lib的值 c:myp
14、rojectdat是要加双引号的。这里要提到两个好的习惯,其实前面的文章已有介绍:首先是选择变量名时,一定是通过这个名字就知道这个变量的功能,例如前面的 data表示是数据,lib变量表示是库名等。其次,变量的值也要完整的给出来,这样增加程序可读性。第一个好习惯大家应该都能理解,现在说一下第二个好习惯。这里我们举一个反例,例如日期是由月+空格+日+空格+年(DATE = “Jan 10 2002“)组成,我们要转换成 SAS日期(DATE = “10Jan2002“),我们可以用 sasdate = input ( date , date9. );这个 input函数来完成。但在本例中,由于我
15、们的是月日年,而 SAS日期是日月年,因此我们先得将原来的 DATE的内容换一下次序:scan(date,2)|scan(date,1)|scan(date,3),因此可以用下面的解决方法:%let x = scan(date, ;sdt=input(上面的代码可以实现我们将 DATE转换成 SAS日期格式的功能,但是代码却非常难读懂,甚至会觉得这代码是不是乱写的。我们很难清晰明了的知道 sdt的值。引外,这里的变量 x的名字也不好,根本不明白它的真正意义。由于 SAS过程步编译器对代码有很多限制,而 SAS宏编译器将代码视为文本,因此,在编写 SAS宏的时候更容易编写出一些不具可读性,但却能
16、正确执行的代码。至于 SAS宏变量或参数的作用,这个大家都能理解。这里还提到一点,就是双引号和单引号在 SAS宏编译时的区别:SAS 宏编译的时候,如果你用单引号的话,那么 word scanner就将引号内的文本作为单个 token,因此此表达式将不进入宏相关的变量或工具,而双引号它自已变是 token,因此它将作为宏相关的变量或工具。另一个例子是当我们要对同一目录或同一服务器下的多个文件或目录进行操作时,最好能将这同一目录和其它文件分开引用,而不是单个引用。例如:filename in“rk2vol2302statdatds1.txt“ ;filename pgm“rk2vol2302st
17、atprogs“ ;libname stat“rk2vol2302statsasdat“ ;这里只有三个,可能会有更多,因为太多所以很容易引起错误,因此,我们可以定义两个变量,ROOT 和 DSNUM,这样,代码就成了:%let root = “rk2vol2302stat“;%let dsnum = 1 ;filename in“filename pgm“libname stat“这个例子要注意几个要点,一是“这一名的两个点”.”,这个以前的文章也讲过,当库为引用,后面接文件名时,一定要用两个点。然后一点是为什么“ 或“后面不加点呢,因为“”已经起到点的作用了,并且让我们清楚地知道 sasd
18、at还是一个目录。下面是一个反例,来说明为什么用“”而不用点:%let root = rk2vol2302stat ;filename pgm“这里我们就不清楚 progs到底是目录还是文件名。另外,我们对于变量,有时称为变量,有时称为参数,那这两者有什么区别呢。其实参数呢,就是宏里面基本不变的一些变量,参数对宏来说有着重要的作用。而变量,则会经常变化其值,以满足宏的功能需求。最后宏变量要提到一个函数”%put”,它的用途是将宏变量的值输出到 log中,这对宏编程时 debug非常有用。下面介绍一下几个常见的应用%put data= 输出自定义宏变量 data的值%put _all_ ; 输出
19、所有宏变量及其值%put _user_ ; 输出用户自定义变量及其值2 宏宏就是参数化的 SAS代码。下面是一个简单的例子libname lib “c:myprojectdat“ ;%let data = lib.mydata ;title3 “The data set is proc print data = run ;我们可以将其定义为完整的宏:%macro tprint (data=titleproc print data = run ;title%mend tprint ;这个宏名字叫 tprint,有两个参数 data和 tl。这里我们来说明一些编写宏的好习惯。首先是宏名字要能明白地
20、说明该宏的作用(如 tprint为打印)其次考虑好选哪些变量作为宏参数(不能太多,也不能太少),以及它们的名字(data 和 tl:清晰明白)最后是要指定一些好的默认值(tl=3,因为 3基本能满足大多数人需求,1 太小,10 太大)另外,对于 title,footnote 一定要记得删掉。对于 libname,我们一般在代码最开始就定义好,而不是在宏内定义。下面讲一个重要的概念,就是 SAS宏编译时的时间顺序1. Macro compile time 宏编译时间2. Macro execution time 宏执行时间3. SAS compile time SAS过程步编译时间4. SAS
21、execution time SAS过程步执行时间理解了宏编译的时间顺序,对于理解 SAS宏代码及其功能有着重要的作用。这里作者用了一个 print宏的例子来讲解如何编一个好的程序,比较简单,我就不多作介绍了,里面提到一点是它说的 jiggle test,就是每次改变一点点参数或代码,来看结果的变化,从而对宏进行调试和 debug。3 关于宏的选择语句:那就是%if condition %then consequent ;语句。我们先来讲讲%if 与 if的区别吧,好像这两个都可以在 SAS宏里用到,但是什么时候用呢,这就要用到上面说的 SAS宏编译时的时间顺序了。%if 是在宏编译时间和宏执
22、行时间时用到,而 if则是在 SAS过程步编译时间和 SAS过程步执行时间里用到。例如:%macro mktrans (type=A) ;%if not %index(Monday Thrsday, %put Program can only be run ;%put on Mondays and Thursdays ;%goto mexit ;%end ;.data trans ;set %if %length(if upcase(type) =“%upcase(%end ;.run ;%mexit:%mend mktrans ;首先所有的代码进入宏编译器编译,然后执行,这里我们假如倒数第六
23、行中的“%upcase(set if upcase(type) =”Y”.run ;也即只剩下 SAS过程步编译可以编译的内容了,这时再进行 SAS过程步编译和SAS过程步执行,这里才会再执行 if语句。其它的语句如%do 与 do等与此类似。对于 if upcase(type) =“%upcase(其解释也是一样,一个是宏编译器里处理的内容,处理完后再交给 SAS过程步编译器处理。具体的参见 SAS编译原理因此,作者给出的例子if weekday(today()=2 thenset Monday ;elseif weekday(today()=5 thenset Thursday ;else
24、do ;put “Program can only be run on “Mondays and Thursdays“ ;abort ;end ;至少其在宏编译的时候是不会执行的。如果要让其在宏编译的时候执行,就要将代码改为:%macro mktrans (day=1) ;.data trans ;%if set Monday ;%end ;%else%if set Thursday ;%end ;%else%do ;put “Program can only be run“on Mondays and Thursdays“;abort ;%end ;.run ;%mend mktrans ;
25、呵呵,我也经常这样改,因为改起来方便,但其实这个代码可读性也很差。因此作者对代码进行了进一步改进,其核心思想是:将 SAS数据步编译的代码尽量放在一起,从而增加程序的可读性%macro mktrans (day=1) ;%local data ;%if %else%if %else%put Program can only be run ;%put on Mondays and Thursdays ;%goto mexit ;%end ;.data trans ;set data ;.run ;%mexit:%mend mktrans ;作者再用宏函数%index 对程序作了进一步改进:%ma
26、cro mktrans () ;%if not %index(Monday Thrsday,%put Program can only be run ;%put on Mondays and Thursdays ;%goto mexit ;%end ;.data trans ;set .run ;%mexit:%mend mktrans ;%index函数的作用是第二个参数里是否有第一个参数的值,如果没有则返回0。另外作者还提到一个问题,就是将宏里的变量尽量定义为 local。这个思想也很重要,前面的文章也讲过了,就不再重复。4 宏循环语法:%do variable = firstvalue
27、%tolastvalue %by increment ;.%end ;例如:%macro mklist(root=ds,from=1,to=1) ;%local i ;%do i = %mend mklist ;这里%macro name1;%let new=report;%mend name1;New的值为 report,且依然为全避变量。2 在宏内用%local 可强行生成局部变量:程序:%let new=inventry;%macro name1;%local new;%put %mend name1;%put 结果:Report(local)Inventory (global)3 Ca
28、ll symput在最新的 symbol table里创建变量,因此,如果:Call symput在 proc sql之后创建的变量Syspbuff已创建当前宏包括%goto 语句则会生成局部变量。例:%macro env1(param1);data _null_;x = a token;call symput(myvar1,x);run;%mend env1;%env1(10)Myvar1为局部变量,因为宏包括完整数据步,且宏包括一个参数,故最新生成的为一个局部变量 symbol table。%macro env2(param2);data _null_;x = a token;call s
29、ymput(myvar2,x);%mend env2;%env2(20)run;Myvar1为全局变量。%macro env3;data _null_;x = a token;call symput(myvar3,x);run;%put * Inside the macro: *;%put _user_;%mend env3;%env3Myvar1为全局变量。%macro env4 /parmbuff;data _null_;x = a token;call symput(myvar4,x);run;%put * Inside the macro: *;%put _user_;%put %me
30、nd env4;%env4Myvar1为局部变量,由于生成了 syspbuff为局部变量sas macro宏变量引用转载请注明出处:http:/ sashelp.vmacro中,打开后可以看到这些变量的范围(scope)和值。自动生成的宏变量和自定义的宏变量都可以用 title1 “Contents of Data Set 结果:TITLE1 “Contents of Data Set Newdata“;2 与非宏变量字符结合2.1 如果非宏变量字符在前,宏变量在后,则直接引用:程序:%let name=sales;data newset save.run;结果:DATA NEWSALES;S
31、ET SAVE.SALES;RUN;2.2 如果非宏变量字符在后,宏变量在前,则引用后一定要加“.” :程序:data (未加“.”,应为 data )set in(加“.”)run;结果:DATA (引用错误)SET INSALESTEMP;(引用正确)RUN;3 当进行层级引用,例如库名.表名时,库名为宏变量时,一定要在后面加两个“.”程序:set in(如果只有一个“.”,则结果参照 2.2)set 结果:SET INSALES.TEMP;SET SALES.TEMP;4 多级引用(非直接引用),例如%put 如果前面再多加 N个)。Word scanner 读到的第一个非空 token
32、是 Data,SAS 就知道这个一个数据步的起点,然后将这些 token放到 Data step compiler里。Word Scanner 继续读取 token,直到读到 Run,SAS 就知道这是这个数据步的终点。然后在 data set compiler里对刚才读到的程序进行编译。这里提一下,进入编译器的程序,SAS 都会自动转换为大写,这也是为什么 SAS对变量名不进行大小写区分的原因。接下来介绍 SAS宏编译:首先介绍一下 Symbol table,这是放宏变量的一个表,在 sashelp.vmacro这张表里。当有新的宏变量时,就会记录在这张表中。当 word scanner扫到
33、%或这里,SAS 宏编译器就将把 file放入symbol table里,其宏变量为 file,值为 in1。只要宏编译器在处理宏,那么data step compiler将不会进行编译。当宏编译完成后,word scanner 继续读取下一行的 token。当读取以title1 “My important print“ ;proc print data = format _all_ ;%nextstep()%mend printplus ;%macro nextstep ( data =sashelp.class ) ;title1 “Basic Analysis Variables“ ;p
34、roc means 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 编译器将
35、会中止,转换为宏编译器,编译宏 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 then do ;%let x = 1 ;end ;elsedo ;%let x = 2;end ;
36、y = output ;end ;run ;我们是不是觉得这里的结果是前五个 Y 的值为 5,后面的 Y 的值为 0;但结果是 Y 的值都为 0,为什么呢?因为我们在执行%let 宏指令的时候,就已经进入了宏编译器,并且直到宏编译完,生成的代码就是先赋值 5 给宏变量 x,然后赋值 0 给宏变量 x,然后原理的SAS 代码中的 do 循环其实已经为空,最后将宏变量 x 的值 0 赋给变量 y,最后 y 的值就全为 0 了。还要注意一点的就是,上面的程序虽然有错误,但是系统并没有提示有任何错误或警告,因此,在写 SAS 宏代码时一定要非常认真地测试程序为了更好地说明 SAS 宏编译时间和 SAS
37、 代码编译时间的问题,我们再看一下下面这个例子:data w ;do obs = 1 to 10 ;if obs = 5 then%let x = 5 ;else%let x = 0 ;y = output ;end ;run ;这里在 SAS 日志中会出现 ERROR 160-185: 没有匹配的 IF-THEN 子句。为什么呢?原因其实是一样的,执行到 if then 语句时, SAS 编译中止,转而执行宏编译%let x = 5 ;而这里是赋值语句,最后不生成 SAS 代码,使得 then 后面没有 SAS 语句,因此就出现日志的错误。所以,我们在%let 后面分别加上一个分号,就能解决
38、这个问题:data w ;do obs = 1 to 10 ;if obs = 5 then%let x = 5 ;else%let x = 0 ;y = output ;end ;run ;现在执行就不会再报刚才的错误了。2.3 CALL SYMPUT 的时间问题CALL SYMPUT 是一个非常重要的函数,特别是数据步时传递宏函数的值。我们看下面这个例子:data w ;do obs = 1 to 10 ;if obs = 5 thencall symput ( “x“ , “5“ ) ;elsecall symput ( “x“ , “0“ ) ;y = output ;end ;run
39、 ;这里日志报的错误是 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
40、 to 10 ;if obs = 5 thencall symput ( “x“ , “5“ ) ;elsecall symput ( “x“ , “0“ ) ;y = input ( symget ( “x“ ), best12. ) ;output ;end ;run ;因此,我们在写 SAS 宏时,一定要分清楚这四个时间,不要将 SAS 执行时才能得到的值赋给 SAS 编译时就需要的变量,不要将 SAS 编译时的循环来控制宏编译时的语句。2.4 单引号问题下面这个例子:%let root = c:projectdata ;filename in 这里,filename 将不会被执行,因为
41、 SAS 宏编译器并不能识别这里的当我们只能用单引号时,我们就得应用宏引用宏数了:filename in %unquote(%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 statementMPRINT optionMFILE option