原标题:从程序员到数据科学家:SAS 编程基础 (12)- SAS 宏(上)
技术大咖——巫银良先生继续分享“如何从程序员成为一名数据科学家”系列文章,今天将与大家分享“ SAS 宏”,赶紧学习起来吧~~
在传统编程语言 C/C++ 中,我们可以使用预处理语句 #define 来定义宏变量,也可以使用其他以 # 号开头的宏语句来实现条件编译功能,比如 #ifdef ….#else… #endif 等。很多语言(如 Java )则不提供预处理宏支持,编程人员只好使用静态常量以及泛型技术来实现类似的控制机制。C# 语言为了灵活的编译控制依然保留了 #define、#if、#else、#endif 等宏语句。那么宏到底是什么神秘的东西?SAS 语言中的宏又是如何工作的呢?
总体来说,宏技术在编译领域的作用就是告诉编译器在正式代码编译之前,由预处理器根据一系列预定义的规则有条件地对源代码进行文本替换,称之为宏展开。宏技术是编程语言之上的更大范围更高级的一种抽象(据《康熙字典》:宏賁皆大也),它能在源码更高的层次上提供灵活的逻辑控制能力。
SAS 宏比传统编程语言的宏支持有过之而无不及,它实现了 SAS 代码的一种复用,让 SAS 编程更加精巧和灵活。SAS 宏易于学习,但也很容易令人困惑,因为程序员很容易将宏本身,跟宏展开后的输出混为一谈。SAS 宏系统非常强大,不但包括一般编程语言支持的宏变量替换和条件分支控制,而且还支持更加高级的循环控制、以及宏函数(系统宏和用户自宏)。SAS 宏本身已经发展成一种计算机语言,在更高的层面上对 SAS 源代码进行操纵实现分析数据、编写报告以及自动执行代码等功能。
通过前面的学习我们知道 SAS 程序由各种 SAS 语言编写的语句(比如 DATA/PROC 步、全局语句等)组成,但 SAS 代码中其实还可以包含 SAS 宏语言、 SCL 语言和 SQL 语言编写的语句。 SAS 代码提交执行时,源代码首先被读入计算机内存的输入缓冲区进行字符扫描,如果代码包含 SAS 宏特定的字符 & 和 % ( 前者标识宏变量,后者标识宏语句 ),这些代码就会触发宏处理器进行宏展开,展开后的代码会再次输出到输入缓冲区继续扫描处理。宏展开完毕的 SAS 代码才会交由 SAS 编译器进行词法分析和编译运行。需要特别注意一点是,SAS 宏技术的引入并未减少 SAS 程序的执行时间,而是减少编写重复或类似的 SAS 代码,从而增强了 SAS 程序的可读性。
SAS 代码预处理机制如下:
宏变量本质上就是一种字符类型的变量,由它的名字和值共同构成一个符号表。宏变量的值可以来自真正的 SAS 变量,数字或文本甚至是宏表达式。 SAS 源代码中出现 % 号和 & 号的地方(包括双引号括起来的字符串中)都是 SAS 宏出没的地方。
命名:SAS 宏变量命名遵循 SAS 命名规则,可以是字母、数字或下划线,但必须以字母或下划线开头,最大长度不得超过 32 字节长度。如果定义宏变量的时候该宏变量在系统符号表中已经存在,系统会自动覆盖已有的宏变量。
宏变量可以使用 %GLOBAL 或 %LOCAL 语句预先定义,也可以直接用 %LET 语句定义并赋值(%LET 语句主要用于赋值,仅在宏变量没有定义时才自动创建)。随后的代码中可使用 & 宏变量名 对宏变量进行引用。其基本语法如下:
%LET
macro-variable =<value>;
%LET
FOO=VALUE;
%PUT
&FOO;
系统输出:
VALUE
当使用 %LET 语句对宏变量进行赋值时,其等号右侧的“内容” 遵循如下规则:
1、首尾的连续空格在赋值前会被自动删除
2、数值也是被作为文本看待,数学表达式也不会被求值,除包含在特定宏函数中进行求值。
3、引号本身也是作为宏值的一部分而存在,而不是字符串标记符号。
4、文本长度必须介于 1 - 65534 字符之间;注意:比程序员最熟悉的 16 位无符号整数的最大值 65535 少 1 !
作用域:宏变量有作用域的概念,宏变量可以在 SAS 会话中全局有效,或者在 SAS 宏函数中局部有效。如果宏变量是定义在一个 SAS 宏函数(因为通常用来封装成函数,常称为宏函数)内部,它的作用域就是局部的,只能在该宏函数内起作用。但如果我们定义在任何宏函数外,则我们可在 SAS 程序任何地方都可使用这种已经定义的全局宏变量。考察如下代码:
%PUT
&FOO;
/*
试图引用一个不存在的宏变量
*/
WARNING: Apparent symbolic reference FOO not resolved.
如果改为:
%LET
FOO=GLOBAL;
%PUT
&FOO;
/*
输出:
GLOBAL*/
输出
:
GLOBAL
如果 %LET 语句在一个 SAS 宏函数内部给某个宏变量赋值,默认情况下该宏变量是局部变量。如果 %LET 语句在开型代码中,则该宏变量为全局变量。
在调用宏变量赋值语句 %LET 之前,我们可使用 %GLOBAL 和 %LOCAL 语句对宏变量进行预先定义,分别指定宏变量的作用域是全局还是局部。如果已经定义了某个全局宏变量,同时在某个宏函数内部再次用 %LOCAL 定义一个同名宏变量,则宏处理器在执行该宏时使用局部定义的宏变量值,而在该宏外部使用全局宏变量的值。比如下面的代码将输出: GLOBAL GLOBAL LOCAL
GLOBAL
,最后一个输出的是 GLOBAL 而不是宏 MyMacroFunction 内指定的值 LOCAL 。归根结底,
局部宏是定义于宏函数自带的符号表中,而非全局符号表中;宏变量的查找顺序是先从宏函数局部符号表先查找,然后再从全局符号表中查找。
%LET
FOO=GLOBAL;
%PUT
&FOO;
/*
输出:
GLOBAL*/
%Macro
MyMacroFunction
;
%PUT
&FOO;
/*
输出:
GLOBAL*/
%LOCAL
FOO;
%LET
FOO=LOCAL;
%PUT
&FOO;
/*
输出:
LOCAL*/
%MEND
;
%
MyMacroFunction
;
%PUT
&FOO;
/*输出:
GLOBAL
*/
系统宏:系统宏是预先定义在全局符号中的一系列宏变量,它们在当前 SAS 会话中全局有效。比如全局宏变量 &SYSDATE 和 &SYSTIME,分别表示当前 SAS 会话开始的日期和时间。检测当前 SAS 会话中到底有哪些宏被定义了,可调用 %PUT 语句来显示。比如:
%PUT
_ALL_;
/*
列出所有宏变量
*/
%PUT
_AUTOMATIC_;
/*
列出所有系统定义的宏变量
*/
%PUT
_GLOBAL_;
/*
列出所有全局(会话级)宏变量
*/
%PUT
_LOCAL_;
/*
列出所有局部(正在执行的宏)宏变量
*/
%PUT
_USER_;
/*
列出所有用户定义的宏变量
*/
对已经定义的宏变量使用 & 宏变量名 来进行引用,但 SAS 系统对包含在单引号内的文本不会进行宏展开。程序员可在代码中直接引用宏变量,也可在双引号文本中引用宏变量。比如:
%LET
dsname=sashelp.class;
%LET
author=Yinliang Wu;
title
"Content of dataset &
dsname
"
;
/*
双引号
中引用
宏变量
*/
title2
'Author: &author'
;
/*单引号中
不作
宏展开*/
proc
print
data
=&dsname;
/*
直接
引用
宏变量
*/
run
;
上面的代码会输出期望的报表标题,但 title 2 语句中的宏变量 &author 并不工作,原因是我们在 title 2 语句中使用了单引号来包含字符串,而单引号字符串不会被宏展开;如果换成双引号即可正常工作。
x
%LET
var1=Leon;
data
_null_
;
put
"&var1a and &var1ardo"
;
run
;
运行代码后系统报告
没有解析符号引用 VAR1A
和
VAR1ARDO
,而实际上我们是希望输出
Leona and Leonardo
,可以将
put
语句修改如下即可:
put
"
&var1.
a and
&var1.
ardo"
;
由于
&
符号作为宏变量的标识,如果需要生成
&
符本身用于二次宏展开,可以使用
&&
代替。由于这一特性,我们可以定义多重宏变量引用,实现多层宏变量的逐步展开。比如:
%LET
var1=Leon;
%PUT
&var1;
%LET
var2=&&var1;
%PUT
&var2;
以上两个
%PUT
语句都是输出
Leon
宏代码调试
SAS 主要提供两个系统选项 mprint 和 mlogic 来帮助我们调试宏代码,用于静态检查宏展开生成的代码以及动态跟踪宏的执行过程。
静态检查宏展开代码
options
mprint
;
/*
是否打印生成的
SAS
语句代码? 缺省为
NOMPRINT*/
options mprintnest;
/*
mprint
时是否
显示嵌套
?
缺省为
NOMPRINTNEST*/
options mfile;
/*
打印时
是否输出到外部文件
,
缺省为
NOMFILE*/
/*
若
已
启用
mfile
系统
选项且指定了
mprint
文件
引用,代码
会输出到外部文件
*/
filename
mprint
'c:mymacro.sas'
;
%macro
printclass();
proc print data=sashelp.class;run;
%mend
;
%
printclass
;
动态跟踪宏执行过程
options
mlogic
;
/*
是否跟踪宏处理器执行过程?缺省为
NOMLOGIC*/
options mlogicnest;
/*
mlogic
时是否显示嵌套?
缺省为
NOMLOGICNEST*/
与 SAS 表达式类似,SAS 宏也支持表达式,称为宏表达式。它可用于求值运算或逻辑控制。SAS 宏表达式可包含算术运算符、逻辑运算符和比较操作符。SAS宏表达式在语义上跟 SAS 表达式基本一致,很多普通SAS语句加上%号即可从普通 SAS 代码变为 SAS 宏代码。但需要注意:
宏代码中的逻辑运算符 AND、OR、LT、GT… 不必且不能使用百分号。
宏表达式不支持 BETWEEN … AND 这种特殊的 SAS 比较运算形式。
比如 SAS 代码 50.0 <= &Weight <= 80.0 语义等价的宏代码必须使用多个比较表达式,然后用 OR 或 AND 连起来。
宏表达式中可以有括号 () 来进行分组,但有时省略括号 () 也可正常工作。
%Macro
TEST
;
%LET
A=8;
%IF
0
< &A AND &A <
5
%THEN
%PUT
Inside;
%ELSE
%
PUT
Outside;
%MEnd
;
由于宏的本质是文本替换,宏里面的字符内容不需要使用单引号或双引号括起来,而且宏表达式作为字符串值是大小写敏感的。
%LET
a=Hello World;
%LET
b=HELLO WORLD;
%LET
c="Hello World";
%PUT
&a &b &c;
/*
输出
:
Hello World HELLO WORLD "Hello World"*/
由于宏展开本质上是文本替换,宏变量只能包含文本(哪怕是数值也是以文本存在的)内容,但 SAS 宏支持对宏表达式进行算术运算、逻辑运算和比较运算。SAS 提供 %EVAL 和 %SYSEVALF 两个系统宏函数,可对宏表达式进行求值。两者的区别是前者只支持没有小数点的整数表达式,而后者可支持浮点运算(表达式中或求值结果中可包含小数点),甚至可以指定转换的数据类型。然而,如果表达式中不包括运算符时,宏函数会直接返回表达式原值。两个系统宏函数的语法如下:
%EVAL
(
expression
)
%SYSEVALF
(expression, conversion-type )
请考察如下代码:
%LET
exp=4/3;
%LET
eval_V1=
%eval
(&exp);
%PUT
&eval_V1;
/*
输出:
1*/
%LET
exp1=3;
%LET
exp2=4;
%PUT
%sysevalf
( &exp1 * &exp1 + &exp2 * &exp2 );
/*
输出平方和:
25*/
%LET
eval_V2=
%sysevalf
(&exp);
/*
输出:
1.33333333333333
而非
1*/
%LET
sysevalf_V1=
%sysevalf
(2>1,boolean);
/*
输出:
1 */
%LET
sysevalf_v4=
%sysevalf
(5.49,ceil);
/*
输出向上
取整
:
6 */
%LET
sysevalf_v5=
%sysevalf
(5.49,integer);
/*
输出四舍五入:
5 */
%LET
sysevalf_v6=
%sysevalf
(5.49,floor);
/*
输出向下
取整
:
5 */
%LET
sysevalf_v4x=
%sysevalf
(-5.49,ceil);
/*
输出向上
取整
:
-5 */
%LET
sysevalf_v5x=
%sysevalf
(-5.49,integer);
/*
输出四舍五入:
-5 */
注意:在编写宏代码时可能会遇到错误: ERROR : 检测到开型代码语句的递归 ,这往往是因为宏语句忘了用分号进行结束,从而导致宏展开无法继续。
SAS 宏函数
宏变量比较简单也容易理解,但 SAS 宏函数就相对复杂。比如我们需要对一系列的数据集执行一系列相同或类似的 SAS 分析代码时,我们可以考虑利用宏函数来封装“重复或类似的 SAS 代码”。 SAS 程序员不必使用拷贝/粘贴弄得源代码非常冗长,而是像封装函数一样对 SAS 代码进行高层次的代码封装,在代码编译运行前进行宏展开。
SAS 宏函数定义的基本语法如下:
%
M
acro
macro-name
<(parameter-list)> <
/
option-1 <… option-n>> ;
MACRO STATEMENTS
;
%
M
End;
在宏定义语句 %Macro 和 %MEnd 语句之间,我们可以包括任何 SAS 语句(如 DATA/PROC ),也可以包含宏变量引用,宏语句或表达式、或者对其他宏的调用,甚至是纯文本,代码注释等。需要记住的一点是,宏展开后的代码必须符合 SAS 的语法规范。
最常见的一个错误是,在一个
DATA
步内调用一个宏,而该宏已经封装了一个或多个 DATA 步的调用,这时系统就会编译不过。原因很简单,宏展开后会导致 DATA 步代码嵌套,从而导致语法错误。另外,用 SAS 宏封装的代码中不建议使用行注释(以 * 开始,分号结束),而应该使用块注释(以 /* 开始, */ 结束)来避免不期望的宏展开。
对于一个已经定义好的
宏函数
,可使用
%macro-name
来进行引用;如果宏函数引用名后面是空格时,宏语句的结束符分号是可选的。
%
macro-name
[;]
比如,我们可以将打印数据集的 SAS 代码封装成宏,供重复调用:
%Macro
PRINTDS
;
title
"Content of dataset &dsname"
;
proc print data=&dsname;
%MEnd
;
/*
调用
1
:
打印
sashlep.
class
*/
%LET
dsname=sashelp.class;
%
PRINTDS
;
/*
调用
2
:
打印
sashlep.
prdsale
*/
%LET
dsname=sashelp.prdsale;
%
PRINTDS
;
运行上面的代码可以看到 SAS 宏被执行两次,分别输出 sashelp.class 和 sashelp.prdsale 两个数据集的内容。但由于上面的代码依赖于全局宏变量 &dsname,代码的耦合性不好。这时我们就需要使用宏函数的参数列表来进行封装。
SAS 宏函数的参数可以用两种方式进行定义,一种叫顺序参数(或位置参数),另一种叫命名参数(或键值参数)。
顺序参数:就是定义宏的时候,按照先后顺序定义所需的形式参数;在宏调用的时候,也需要按照同样的顺序提供实际参数。比如:
%Macro
Foo(arg1, arg2,… , argn);
MACRO STATEMENTS
;
%MEnd
;
%
Foo
(V1, V2,…, Vn);
据此
,我们修改
前面打印
数据集
的
SAS
宏
如下
,并
可以同时指定
报表标题
:
%Macro
PRINTDS(
dsname
,
title
);
title
"Content of dataset
&title
"
;
proc print data=
&dsname
;
%MEnd
;
/*
调用
形式如下
*/
%
PRINTDS
(sashelp.class, Student);
%
PRINTDS
(sashelp.prdsale, Product Sales);
命名参数:在宏定义时可给参数命名,然后在实际调用的时候也采用 参数名=参数值 的方法来指定实际参数。鉴于每个参数已经定义了名称入口,函数调用时的参数位置就不再需要,并且如果在调用时没有指定参数,SAS 也会使用宏函数定义时所指定缺省值进行调用。语法如下:
% Macro
Foo(arg1= def_V1, arg2=def_V2,…, argn= def_Vn);
MACRO STATEMENTS
;
%MEnd
;
%
Foo
(arg2=V2, argn=Vn);
我们再次修改前面打印数据集的样例代码,给宏函数 PRINTDS 指定参数名称和默认值:
%Macro
PRINTDS(
dsname=sashelp.class
,
title=Class
);
title
"Content of dataset
&title
"
;
proc print data=
&dsname
;
%MEnd
;
%
PRINTDS
();
/*
以
宏函数定义的默认值进行调用
*/
%
PRINTDS
(title=My Class);
/*
指定
title
参数
*/
%
PRINTDS
(dsname=sashelp.prdsale, title=My Product Sales);
宏代码的逻辑控制
与传统的 C 或 C++ 宏编译不同,SAS 不但提供条件分支控制,还提供循环控制。这样 SAS 宏技术就演变为 SAS 代码之上的超级代码。因此,合理巧妙地利用 SAS 宏可以写出简洁优美的 SAS 程序,而滥用 SAS 宏则会导致代码可读性差且调试困难。
与 SAS 代码一样,我们可以使用 DO-END 将多行 SAS 宏语句进行分组,DO 和 END 宏语句需要加上百分号,但功能类似。
%DO
;
…
%END;
基于一个或多个条件,选择性地执行条件块里面的代码;与非宏的 SAS 代码一样,%ELSE 语句是可选的。
%IF
<MACRO EXPRESSION>
%THEN
<TRUE TEXT>;
%ELSE
<FALSE TEXT>;
%IF
/
%ELSE
语句也
可以
使用
上面的宏语句块
%DO-%END
来
嵌套其他的宏语句
%IF
<MACRO EXPRESSION>
%THEN
%DO
;
…TRUE TEXT…
%END
;
%ELSE
%DO
;
…FALSE TEXT
…
%END
;
与 SAS 代码的循环控制类似,SAS 宏的循环控制有如下几种形式:
1) 确定性循环 DO-TO-BY:具有固定循环次数或步长时使用,类似于传统编程中的 FOR 循环,其中控制循环起点的 START 、终点的 END 和步长 STEP 可以是宏常量、宏变量或者任何能够展开为整数的宏表达式,%BY 语句是可选,默认步长为 1 。
%DO
<MACRO-VAR> = <START>
%TO
<END> [
%BY
STEP];
…LOOP TEXT…
%END
;
2) 不确定循环 DO-WHILE:在进入循环体前进行判断,为真则执行循环体,为假则离开循环。
%DO
%WHILE
<MACRO EXPRESSION>;
…LOOP TEXT…
%END
;
3) 不确定循环 DO-UNTIL:执行循环体后进行判断,为真则退出循环体,为假则继续循环;这种循环方式至少会执行循环体一次。
%DO
%UNTIL
<MACRO EXPRESSION>;
…LOOP TEXT…
%END
;
为综合演示 SAS 宏的逻辑控制,下面我们完全用 SAS 宏来实现前面黄金分割数的例子:
%macro
Fbnc
;
%local
y1 y2;
%do
n=
1
%to
20
;
%if
%eval
(&n=
1
or &n=
2
)
%then
%let
y=1;
%else
%let
y=
%eval
( &y1 + &y2 );
%let
y2=&y1;
%let
y1=&y;
%put
n=&n y=&y;
/*
打印到
SAS
日志
*/
%end
;
%mend
;
%
Fbnc
;
/*
调用宏
函数
Fbnc*/
系统输出为:
n=1 y=1
n=2 y=1
n=3 y=2
n=4 y=3
n=20 y=6765
SAS 宏为 SAS 编程语言提供了语言之上的超级语言,可以让 SAS 程序自身在必要的时候改变自己,呈现出千变万化的可能。但我们也不能忘记,SAS 宏展开生成的任何 SAS 代码依然要遵守 SAS 语言的基本规范。编写 SAS 程序时脑海里要很清楚 SAS 宏代码、SAS 代码在编译前后的差别,避免自己在 SAS 语言元素太过丰富的丛林里迷失而失去对程序的控制。由于 SAS 宏太过强大和容易令人困惑,我将 SAS 宏分两部分进行讲述,后一部分将探讨一些高级话题。
请用所学的函数封装技巧分别实现阶乘功能;比如5!就是5 x 4 x 3 x 2 x 1,即 facterial(5)= 120
作者:
巫银良
SAS
北京研发中心商业智能和可视化分析产品部技术总监
分析行业资深专家,大数据可视化分析负责人, SAS北京研发中心商业智能和可视化分析产品部技术总监,资深商业智能技术专家。
@所有SAS用户,SAS微信写手招募! 欢迎大家积极投稿!!!与小伙伴分享您的独特见解和卓越技术。您的来稿一经采用,将有神秘大奖赠予!
投稿邮箱:sas@head-way.com
想跟文章作者直接沟通交流吗?长按下方左侧二维码,加入SAS用户小组交流群,与文章作者直接沟通交流,还在等什么,快来加入吧~~
加入SAS用户小组交流群
关注
PowerToKnow公众号
关于更多SAS编程基础,您可以阅读:
【
表达式
】
更多关于SAS干货,您可以阅读:
欲了解更多信息,请点击
阅读原文
,
或
访问
SAS中国官网 www.sas.com.cn
↓
↓
↓
(
本文作者:巫银良
,如
有转载,请注明出处。
)
添加好友 搜号码【saschina】
查找公众账号【SAS数据分析】
如果您的微信是最新版本,也可以:
分享:
返回搜狐,查看更多
责任编辑:
声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。