可爱的 Python:创建声明性迷你语言
发表于:2007-07-04来源:作者:点击数:
标签:
Python 的 面向对象 和透明自省功能使您可以轻松地创建用于编程任务的声明性迷你语言。在本专栏文章中,David 并未仔细研究如何使用 Python 来解释或翻译其它的专门语言(尽管可以做得到),而是研究如何将 Python 代码本身有效地限定在一组声明性元素中。他
Python 的
面向对象和透明自省功能使您可以轻松地创建用于编程任务的声明性迷你语言。在本专栏文章中,David 并未仔细研究如何使用 Python 来解释或翻译其它的专门语言(尽管可以做得到),而是研究如何将 Python 代码本身有效地限定在一组声明性元素中。他将向您说明
开发人员如何能使用声明技术来简练清晰地陈述应用程序的要求,而让“幕后的”框架来完成繁重的工作。
大多数
程序员考虑编程时,他们都要设想用于编写应用程序的命令式样式和技术。最受欢迎的通用编程语言(包括 Python 和其它面向对象的语言)在样式上绝大多数都是命令式的。另一方面,也有许多编程语言是声明性样式,包括函数语言和逻辑语言,还包括通用语言和专用语言。
让我们列出几个属于各个种类的语言。许多读者已经使用过这些工具中的许多工具,但不见得考虑过它们之间的种类差别。Python、C、C++、
Java、Perl、
Ruby、Smalltalk、Fortran、Basic 和 xBase 都是简单的命令式编程语言。其中,一些是面向对象的,但那只是组织代码和数据的问题,而非基本编程样式的问题。使用这些语言,您命令程序执行指令序列:把某些数据放入(put)变量中;从变量中获取(fetch)数据;循环(loop)一个指令块直到(until)满足了某些条件;如果(if)某个命题为 true,那么就进行某些操作。所有这些语言的一个妙处在于:便于用日常生活中熟悉的比喻来考虑它们。日常生活都是由做事、选择、再做另一件事所组成的,期间或许会使用一些工具。可以简单地将运行程序的计算机想象成厨师、瓦匠或汽车司机。
诸如 Prolog、
Mercury、
SQL、XSLT 这样的语言、EBNF 语法和各种格式的真正配置文件,都声明某事是这种情况,或者应用了某些约束。函数语言(比如 Haskell、ML、Dylan、Ocaml 和 Scheme)与此相似,但是它们更加强调陈述编程对象(递归、列表,等等)之间的内部(函数)关系。我们的日常生活(至少在叙事
质量方面)没有提供对这些语言的编程构造的直接模拟。然而,对于那些可以用这些语言进行描述的问题来说,声明性描述远远比命令式
解决方案来得简明且不易出错。例如,请研究下面这个线性方程组:
清单 1. 线性方程式系统样本
10x + 5y - 7z + 1 = 0
17x + 5y - 10z + 3 = 0
5x - 4y + 3z - 6 = 0
这是个相当漂亮的说明对象(x、y 和 z)之间几个关系的简单表达式。在现实生活中您可能会用不同的方式求出这些答案,但是实际上用笔和纸“求解 x”很烦,而且容易出错。从调试角度来讲,用 Python 编写求解步骤或许会更糟糕。
Prolog 是与逻辑或数学关系密切的语言。使用这种语言,您只要编写您知道是正确的语句,然后让应用程序为您得出结果。语句不是按照特定的顺序构成的(和线性方程式一样,没有顺序),而且您(程序员或用户)并不知道得出的结果都采用了哪些步骤。例如:
清单 2. family.pro Prolog 样本
/* Adapted from sample at:
<http://www.engin.umd.umich.edu/CIS/course.des/cis479/prolog/>
This app can answer questions about sisterhood & love, e.g.:
# Is alice a sister of harry?
?-sisterof( alice, harry )
# Which of alice' sisters love wine?
?-sisterof( X, alice ), love( X, wine)
*/
sisterof( X, Y ) :- parents( X, M, F ),
female( X ),
parents( Y, M, F ).
parents( edward, victoria, albert ).
parents( harry, victoria, albert ).
parents( alice, victoria, albert ).
female( alice ).
loves( harry, wine ).
loves( alice, wine ).
它和 EBNF(扩展巴科斯范式,Extended Backus-Naur Form)语法声明并不完全一样,但是实质相似。您可以编写一些下面这样的声明:
清单 3. EBNF 样本
word := alphanums, (wordpunct, alphanums)*, contraction?
alphanums := [a-zA-Z0-9]+
wordpunct := [-_]
contraction := "'", ("clock"/"d"/"ll"/"m"/"re"/"s"/"t"/"ve")
如果您遇到一个单词而想要表述其看上去可能会是什么,而实际上又不想给出如何识别它的序列指令,上面便是个简练的方法。正则表达式与此相似(并且事实上它能够满足这种特定语法产品的需要)。
还有另一个声明性示例,请研究描述有效 XML 文档方言的文档类型声明:
清单 4. XML 文档类型声明
<!ELEMENT dissertation (chapter+)>
<!ELEMENT chapter (title, paragraph+)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT paragraph (#PCDATA | figure)+>
<!ELEMENT figure EMPTY>
和其它示例一样,DTD 语言不包含任何有关如何识别或创建有效 XML 文档的指令。它只描述了如果文档存在,那它会是怎么样的。声明性语言采用虚拟语气。
Python 作为解释器 vs Python 作为环境
Python 库可以通过两种截然不同的方式中的一种来利用声明性语言。或许更为常用的技术是将非 Python 声明性语言作为数据来解析和处理。应用程序或库可以读入外部来源(或者是内部定义的但只用作“blob”的字符串),然后指出一组要执行的命令式步骤,这些步骤在某种形式上与那些外部声明是一致的。本质上,这些类型的库是“数据驱动的”系统;声明性语言和 Python 应用程序执行或利用其声明的操作之间有着概念和范畴差别。事实上,相当普遍的一点是,处理那些相同声明的库也被用来实现其它编程语言。
上面给出的所有示例都属于第一种技术。库 PyLog 是 Prolog 系统的 Python 实现。它读取像样本那样的 Prolog 数据文件,然后创建 Python 对象来对 Prolog 声明建模。EBNF 样本使用专门变体 SimpleParse,这是一个 Python 库,它将这些声明转换成可以被 mx.TextTools 所使用的状态表。mx.TextTools 自身是 Python 的扩展库,它使用底层 C 引擎来运行存储在 Python 数据结构中的代码,但与 Python 本质上几乎没什么关系。对于这些任务而言,Python 是极佳的粘合剂,但是粘合在一起的语言与 Python 差别很大。而且,大多数 Prolog 实现都不是用 Python 编写的,这和大多数 EBNF 解析器一样。
DTD 类似于其它示例。如果您使用象 xmlproc 这样的验证解析器,您可以利用 DTD 来验证 XML 文档的方言。但是 DTD 的语言并不是 Python 式的,xmlproc 只将它用作需要解析的数据。而且,已经用许多编程语言编写过 XML 验证解析器。XSLT 转换与此相似,也不是特定于 Python 的,而且像 ft.4xslt 这样的模块只将 Python 用作“粘合剂”。
虽然上面的方法和上面所提到的工具(我一直都在使用)都没什么不对,但如果 Python 本身是声明性语言的话,那么它可能会更精妙,而且某些方面会表达得更清晰。如果没有其它因素的话,有助于此的库不会使程序员在编写一个应用程序时考虑是否采用两种(或更多)语言。有时,依靠 Python 的自省能力来实现“本机”声明,既简单又管用。
自省的魔力
解析器 Spark 和 PLY 让用户用 Python 来声明 Python 值,然后使用某些魔法来让 Python 运行时环境进行解析配置。例如,让我们研究一下与前面 SimpleParse 语法等价的 PLY 语法。Spark 类似于下面这个示例:
清单 5. PLY 样本
tokens = ('ALPHANUMS','WORDPUNCT','CONTRACTION','WHITSPACE')
t_ALPHANUMS = r"[a-zA-Z0-0]+"
t_WORDPUNCT = r"[-_]"
t_CONTRACTION = r"'(clock|d|ll|m|re|s|t|ve)"
def t_WHITESPACE(t):
r"\s+"
t.value = " "
return t
import lex
lex.lex()
lex.input(sometext)
while 1:
t = lex.token()
if not t: break
我已经在我即将出版的书籍 Text Processing in Python 中编写了有关 PLY 的内容,并且在本专栏文章中编写了有关 Spark 的内容(请参阅参考资料以获取相应链接)。不必深入了解库的详细信息,这里您应当注意的是:正是 Python 绑定本身配置了解析(在这个示例中实际是词法分析/标记化)。PLY 模块在 Python 环境中运行以作用于这些模式声明,因此就正好非常了解该环境。
PLY 如何得知它自己做什么,这涉及到一些非常奇异的 Python 编程。起初,中级程序员会发现可以查明 globals() 和 locals() 字典的内容。如果声明样式略有差异的话就好了。例如,假想代码更类似于这样:
清单 6. 使用导入的模块名称空间
import basic_lex as _
_.tokens = ('ALPHANUMS','WORDPUNCT','CONTRACTION')
_.ALPHANUMS = r"[a-zA-Z0-0]+"
_.WORDPUNCT = r"[-_]"
_.CONTRACTION = r"'(clock|d|ll|m|re|s|t|ve)"
_.lex()
这种样式的声明性并不差,而且可以假设 basic_lex 模块包含类似下面这样的简单内容:
清单 7. basic_lex.py
def lex():
for t in tokens:
print t, '=', globals()[t]
这会产生:
% python basic_app.py
ALPHANUMS = [a-zA-Z0-0]+
WORDPUNCT = [-_]
CONTRACTION = '(clock|d|ll|m|re|s|t|ve)
PLY 设法使用堆栈帧信息插入了导入模块的名称空间。例如:
清单 8. magic_lex.py
import sys
try: raise RuntimeError
except RuntimeError:
e,b,t = sys.exc_info()
原文转自:http://www.ltesting.net