给 Lisp 程序员的 Python 简介
这是一篇为Lisp程序员写的Python简介(一些Python程序员告诉 我,这篇文章对他们学习Lisp也有帮助,尽管这不是我的本意)。基本上,Python可以看作一个拥有“传统”语法(Lisp社区称之为“中缀”或者 “m-lisp”语法)的Lisp方言。一个来自comp.lang.python的帖子说到“我一直不明白为什么LISP是一个不错的想法,直到我开始 玩上了Python”。Python支持除了宏(macros)之外的所有Lisp的关键特征,并且你不会太想念宏因为Python拥有求值,运算符重载,和正则表达式解析,所以一些(不是所有)宏的使用场景都被涵盖了。
我深入学习Python的原因是,我正在考虑把Russel & Norvig的人工智能教材配套的Lisp代码转化成Java代码。一些教师和学生想要Java代码,因为:
-
那是他们在其他课程中最熟悉的语言。
-
他们希望有图形应用程序(graphical applications)。
-
少数人想要能在浏览器中运行的applets。
-
一些人只是因为在他们可以投入的有限的上课时间内,不能习惯于Lisp的语法。
然而,我们编写Java版本的第一次尝试很不成功。Java太罗嗦了,并且书中的伪代码和java代码之间的差异太大。我环顾四周,寻找一种和书中伪代码相近的语言,并最终发现 Python是最接近的。此外,有了Jython,我可以定位于Java的JVM。
我的结论
对于我的使用目的而言,Python是一个非常优秀的语言。它使用简单(交互式的,没有“编译-链接-加载-运行”循环),这点对于我的教学目的是很重要 的。虽然Python不满足被拼写为J-A-V-A的前提条件,但是Jython已经很接近了。对于没有其他语言经验的人而言,Python似乎比 Lisp更容易阅读。我开发的Python代码看起来比 Lisp代码更像书中(独立开发)的伪代码。这点是很重要的,因为一些同学抱怨说,他们在把书中的伪代码和网上的Lisp代码对应起来的过程中,有一段困难的时间(即使这个过程对Lisp程序员来说是显然的)。
从我的观点来看,Python的两个主要的缺点是:1)只有很少的编译时的错误分析(compile-time error analysis)和类型声明(type declaration),甚至比Lisp还少。2)运行时间比Lisp慢很多,通常相差10倍(有时100倍,有时1倍)。定性地说,Python和解 释型的Lisp的速度差不多,但是很明显地慢于编译型的Lisp。基于这个原因,对于那些(或者会逐渐变为)计算密集性的应用(除非你愿意把速度瓶颈部分 用c重写),我不会推荐使用Python。但是我的目的是面向教育的,而不是产品,所以速度不是问题。
Python 简介
Python既可以看作一个实用(更好的库)版本的Scheme,也可以看作一个净化(没有了“$@&%”符号)版本的Perl。然而Perl的 哲学是TIMTIWTDI(there's more than one way to do it,即都种方法解决同意问题),Python试图提供一个最小子集,使得人们以同样的方式使用它(即TOOWTDI,there's only one way to do it,但是如果你努力尝试的话,通常会有多种方式去解决同一问题)。其中一个具有争议的Python特征是,用缩进层次代替begin/end或者花括 号,启发它的哲学思想是:因为这里没有括号,所以也就没有了因为如何放置括号而引起的风格战争(style wars)。有趣的是,Lisp在这点上有同样的哲学思想:每个人都用emacs去缩进代码,所以他们没有去争论缩进方式。拿到一个Lisp代码,对它进 行合理的缩进,删除行首的左括号(opening parens),以及与之匹配的闭括号(close parens),这样我们就得到看起来类似Python的程序。
Python有做出明智妥协的哲学思想,使得容易的事情更容易,并且不排除太多困难的事情。在我看来Python做的不错。简单的事情简单,困难的事情逐 渐困难,并且你甚至注意不到这种不一致。Lisp有做出较少妥协的思想:提供一个强大并且完全一致的核心。这让Lisp很难去学,因为你从一开始就操作在 一个较高的抽象层次上,而且你需要理解你正在做什么,而不是仅仅依靠感觉活着看起来漂亮。但是它同样意味着更容易为Lisp添加抽象层次和复杂 性;Lisp优化的目标是使得很困难的事情不太困难,而Python优化的目标是使得中等难度的事情更简单。
这里我从Python.org摘了一段对Python的简介,并我创造了两个版本:一个是用蓝色斜体显示的Python,另一个是用绿色粗体显示的Lisp。其他大部分,两个语言的共同部分,是黑色的。
Python/Lisp 是一个解释型的和编译型的, 面向对象的,高级编程语言,并且拥有动态语义。它的高级的内部数据结构,以及动态类型和动态绑定,使得它对于快速应用开发特别有吸引力,同样地,Python作为脚本或者胶水语言去链接已存在的部分,也是非常有吸引力。Python/Lisp简单、易学的语法强调的是可读性,并因此降低程序维护的成本。Python/Lisp支持模块(modules)和包(packages),这鼓励了程序的模块化和复用。Python/Lisp解释器和广泛的标准库在大多数平台上都是以源码或者二进制形式发布的,并且可以自由的散发。通常,程序员爱上Python/Lisp是因为它提供地不断增加的生产力。因为这里没有分离的(separate)编译步骤,“编辑-测试-调试”循环难以置信的快。调试Python/Lisp程序很简单:一个bug或者坏输入不会造成段错误。相反,当解释器发现一个错误,它抛出一个异常。当程序没有抓获这个异常时,解释器将打印栈轨迹。代码级别的调试允许查看局部和全局变量,求值任意表达式,设置断点,单步执行,等等。调试器是由Python/Lisp自己写的,表明了Python/Lisp的自省能力。另一方面,通常最快的调试一个程序的方法是向源码中添加一些打印语句:快速的“编辑-测试-调试”循环使得这个简单的方法特别有效。
我只添加下面这句:
尽管有些人对于缩进作为块结构/括号有原始的抵制,但是大多数人最后喜欢/深深的感激 他们。
想学习更多关于Python的知识,如果你是一个有经验的程序员,我推荐你去Python.org的下载页面,下载文档包,并且特别要注意Python参考手册和Python库参考。这里有各种各样的辅导材料(tutorials)和出版的书籍,但是这些参考是你真正需要的。
下面的表作为一个Lisp/Python转化的指南。红色的表项标记了这个位置中的语言,明显的比较糟糕(我自己的观点)。粗体标记了这个位置上,两个语言明显不同,但是没有一个方式明显的好于另一个。用正常字体显示的表项意味着两个语言是相似的;语法可能稍有不同,但是概念是相同的,或者非常相近。表后面是一个要点列表和一些Python的示例程序.
关键特征 | Lisp 特征 | Python 特征 |
一切都是对象 | 是 | 是 |
对象有类型,变量没有 | 是 | 是 |
支持异构的(heterogenous)列表 | 是 (linked list and array/vector) | 是 (array) |
多范式语言 | 是:函数式,命令式,OO,Generic | 是:函数式,命令式,OO |
存储管理 | 自动垃圾收集 | 自动垃圾收集 |
包/模块 | 使用困难 | 使用简单 |
对象,类的自省 | 强大 | 强大 |
元编程的宏 | 强大的宏 | 没有宏 |
交互式的REPL(Read-eval-print loop) | > (string-append "hello" " " "world") "hello world" | >>> ' '.join(['hello', 'world']) 'hello world' |
简介、富有表达力的语言 | (defun transpose (m) (apply #'mapcar #'list m)) > (transpose '((1 2 3) (4 5 6))) ((1 4) (2 5) (3 6)) | def transpose (m): return zip(*m) >>> transpose([[1,2,3], [4,5,6]]) [(1, 4), (2, 5), (3, 6)] |
跨平台可移植性 | Windows, Mac, Unix, Gnu/Linux | Windows, Mac, Unix, Gnu/Linux |
实现的数量 | 很多 | 一个主要的,附加一些分支(例如:Jython, Stackless) |
开发模式 | 专有和开源 | 开源 |
效率 | 大约比C++慢1到2倍 | 大约比C++慢2到100倍 |
GUI, Web 等库 | 没有标准 | GUI, Web 标准库 |
方法 方法分派 | 动态, (meth obj arg) 语法 runtime-type, multi-methods | 动态, obj.meth(arg) 语法 runtime-type, single class-based |
数据类型 | Lisp 数据类型 | Python 数据类型 |
Integer Bignum Float Complex String Symbol Hashtable/Dictionary Function Class Instance Stream Boolean Empty Sequence Missing Value Lisp List (linked) Python List (adjustable array) Others | 42 100000000000000000 12.34 #C(1, 2) "hello" hello (make-hash-table) (lambda (x) (+ x x)) (defclass stack ...) (make 'stack) (open "file") t, nil (), #() linked list, array nil (1 2.0 "three") (make-arrary 3 :adjustable t :initial-contents '(1 2 3)) 很多 (in core language) | 42 100000000000000000 12.34 1 + 2J "hello" or 'hello' ## 不可变的 'hello' {} lambda x: x + x class Stack: ... Stack() open("file") True, False (), [] tuple, array None (1, (2.0, ("three", None))) [1, 2.0, "three"] 很多 (in libraries) |
控制结构 | Lisp 控制结构 | Python 控制结构 |
语句和表达式 | 一切都是表达式 | 区分语句和表达式 |
假值(False) | nil是唯一的假值 | False, None, 0, '', [ ], {}都是假值 |
函数调用 | (func x y z) | func(x,y,z) |
条件测试 | (if x y z) | if x: y else: z |
条件表达式 | (if x y z) | y if x else z |
While循环 | (loop while (test) do (f)) | while test(): f() |
其他循环 | (dotimes (i n) (f i)) (loop for x in s do (f x)) (loop for (name addr salary) in db do ...) | for i in range(n): f(i) for x in s: f(x) ## s为任意序列 for (name, addr, salary) in db: ... |
赋值 | (setq x y) (psetq x 1 y 2) (rotatef x y) (setf (slot x) y) (values 1 2 3) 在栈上 (multiple-value-setq (x y) (values 1 2)) | x = y x, y = 1, 2 x, y = y, x x.slot = y (1, 2, 3) 在堆中 x, y = 1, 2 |
异常 | (assert (/= denom 0)) (unwind-protect (attempt) (recovery)) (catch 'ball ... (throw 'ball)) | assert denom != 0, "denom != 0" try: attempt() finally: recovery() try: ...; raise 'ball' except 'ball': ... |
其他控制结构 | case, etypecase, cond, with-open-file, etc. | 扩展的with语句 没有其他控制结构 |
词法结构 | Lisp 词法结构 | Python 词法结构 |
注释 | ;; 分号直到行尾 | ## 井号直到行尾 |
界定符(Delimiters) | 用括号来界定表达式 (defun fact (n) (if (<= n 1) 1 (* n (fact (- n 1))))) | 用缩进来界定语句 def fact (n): if n <= 1: return 1 else: return n * fact(n — 1) |
高阶函数 | Lisp 高阶函数 | Python 高阶函数 |
应用函数 执行一个表达式 执行一个语句 加载文件 | (apply fn args) (eval '(+ 2 2)) => 4 (eval '(dolist (x list) (f x))) (load "file.lisp") or (require 'file) | apply(fn, args) or fn(*args) eval("2+2") => 4 exec("for x in list: f(x)") execfile("file.py") or import file |
序列函数 | (mapcar length '("one" (2 3))) => (3 2) (reduce #'+ numbers) (every #'oddp '(1 3 5)) => T (some #'oddp '(1 2 3)) => 1 (remove-if-not #'evenp numbers) (reduce #'min numbers) | map(len, ["one", [2, 3]]) => [3, 2] or [len(x) for x in ["one", [2, 3]]] reduce(operator.add, numbers) all(x%2 for x in [1,3,5]) => True any(x%2 for x in [1,2,3]) => True filter(lambda x: x%2 == 0, numbers) or [x for x in numbers if x%2 == 0] min(numbers) |
其他高阶函数 | count-if 等 :test, :key 等关键字参数 | 没有其他内置的高阶函数 map/reduce/filter函数没有关键字参数 |
Close over read-only var Close over writable var | (lambda (x) (f x y)) (lambda (x) (incf y x)) | lambda x: f(x, y) Can't be done; use objects |
参数列表 | Lisp 参数列表 | Python 参数列表 |
可选参数 变长参数 未确定的关键字参数 调用约定 | (defun f (&optional (arg val) ...) (defun f (&rest arg) ...) (defun f (&allow-other-keys &rest arg) ...) 只有明确声明时,才可以用关键词方式调用: (defun f (&key x y) ...) (f :y 1 :x 2) | def f (arg=val): ... def f (*arg): ... def f (**arg): ... 用关键字方式调用任何函数: def f (x,y): ... f(y=1, x=2) |
效率 | Lisp 效率问题 | Python 效率问题 |
编译 函数引用解析 声明 | 编译到本机代码 大多数“函数/方法”的查找很快 为了效率可以进行声明 | 编译到字节码 大多数“函数/方法”的查找比较慢 没有声明 |
特征 | Lisp 特征和函数 | Python 特征和函数 |
引用(Quotation) | 引用整个列表结构: 'hello '(this is a test) '(hello world (+ 2 2)) | 引用单个的字符串或者.split(): 'hello' 'this is a test'.split() ['hello', 'world', [2, "+", 2]] |
自省(Introspectible)文档字符串 | (defun f (x) "compute f value" ...) > (documentation 'f 'function) "compute f value" | def f(x): "compute f value" ... >>> f.__doc__ "compute f value" |
列表访问 | 通过函数: (first list) (setf (elt list n) val) (first (last list)) (subseq list start end) (subseq list start) | 通过语法: list[0] list[n] = val list[-1] list[start:end] list[start:] |
哈希表访问 | 通过函数: (setq h (make-hash-table)) (setf (gethash "one" h) 1.0) (gethash "one" h) (let ((h (make-hash-table))) (setf (gethash "one" h) 1) (setf (gethash "two" h) 2) h) | 通过语法: h = {} h["one"] = 1.0 h["one"] or h.get("one") h = {"one": 1, "two": 2} |
列表上的操作 | (cons x y) (car x) (cdr x) (equal x y) (eq x y) nil (length seq) (vector 1 2 3) | [x] + y but O(n); also y.append(x) x[0] x[1:] but O(n) x == y x is y () or [ ] len(seq) (1, 2, 3) |
数组上的操作 | (make-array 10 :initial-element 42) (aref x i) (incf (aref x i)) (setf (aref x i) 0) (length x) #(10 20 30) 如果大小不变的话 | 10 * [42] x[i] x[i] += 1 x[i] = 0 len(x) [10, 20, 30] |
对大多数人来说,一个重要的方面就是Python和Lisp相对于其他语言而言的执行速度如何。我很难得到和你的应用相关的基准测试数据,但是下面这个表可能对你有用:
The Great Computer Language Shootout5种语言在基准测试集上的相对速度.
测试 | Lisp | Java | Python | Perl | C++ | | |
---|---|---|---|---|---|---|---|
哈希访问 | 1.06 | 3.23 | 4.01 | 1.85 | 1.00 | | |
异常处理 | 0.01 | 0.90 | 1.54 | 1.73 | 1.00 | | 图例说明 |
对文件中的数字进行求和 | 7.54 | 2.63 | 8.34 | 2.49 | 1.00 | | > 100 x C++ |
反转行 | 1.61 | 1.22 | 1.38 | 1.25 | 1.00 | | 50-100 x C++ |
矩阵相乘 | 3.30 | 8.90 | 278.00 | 226.00 | 1.00 | | 10-50 x C++ |
堆排序 | 1.67 | 7.00 | 84.42 | 75.67 | 1.00 | | 5-10 x C++ |
数组访问 | 1.75 | 6.83 | 141.08 | 127.25 | 1.00 | | 2-5 x C++ |
列表处理 | 0.93 | 20.47 | 20.33 | 11.27 | 1.00 | | 1-2 x C++ |
对象实例化 | 1.32 | 2.39 | 49.11 | 89.21 | 1.00 | | < 1 x C++ |
单词计数 | 0.73 | 4.61 | 2.57 | 1.64 | 1.00 | | |
| | | | | | | |
中位数 | 1.67 | 4.61 | 20.33 | 11.27 | 1.00 | | |
25% to 75% | 0.93 to 1.67 | 2.63 to 7.00 | 2.57 to 84.42 | 1.73 to 89.21 | 1.00 to 1.00 | | |
范围 | 0.01 to 7.54 | 0.90 to 20.47 | 1.38 to 278 | 1.25 to 226 | 1.00 to 1.00 | | |
对速度进行了规格化处理,以使得利用g++编译器编译过的c++代码的速度是1.00,所以2.00意味着比c++慢2倍;0.01意味着比c++快 100倍。对于Lisp而言,使用的是CMUCL编译器。背景颜色是按照右边图例说明进行绘制的。最后的3行给出了中位数值,25%到75%的 quartile值(即去掉两个最低值和去掉两个最高值),以及整体范围。在去掉两个最低的和两个最高的之后,比较Lisp和Python,我们发现 Python比Lisp要慢3到85倍,Perl和Python差不多,但是比Java和Lisp都慢。Lisp大约比Java要快2倍
给Lisp程序员的Python要点
在这里我列出了一些概念上的问题,是为像我一样转到Python的Lisp程序员准备的:
-
Lists are not Conses。 Python的列表其实比较像Lisp的可变数组,或者Java的向量(Vector)。这意味着列表的存取是O(1)的,但是与cons和cdr等价的 操作却产生了O(n)的新空间。你真的应该使用map或者for e in x:,而不是基于car/cdr的递归。注意Python里有多个空列表,而不是一个。这修正了Lisp一个常见的bug,即用户调用(nconc old new),并期待old被修改了,但是当old是nil的时候,它并没有被修改。在Python里,即使old是[ ],old.extend(new)也可以正常工作。但是这将意味着你必须用==测试列表是否为[];而不是用is,同时这也意味着,如果你把一个默认参 数的值设置为[],你最好不要修改这个值。
-
Python is less functional。 部分原因是Python的列表不是conses,相比于Lisp,Python使用了更多方法去改变列表的结构,并且为了强调它们的改变,它们通常返回 None值。例如像list.sort, list.reverse, 和list.remove这样的方法都是,但是Python的新版本中引入了函数式的版本,即作为一个函数而不是方法,我们现在有了sorted and reversed(但是没有removed)。
-
Python classes are more functional.在Lisp(CLOS)中,当你重 定义一个类C时,表示类C的对象也相应地得到修改。已经存在的C的实例和子类也因此重新指向新的类。这有时候会引起一些问题,但是在交互式的调试中,这却 是很有用的。在Python中,当你重定义一个类时,你会得到一个新的类对象,但是实例和子类还是指向旧类。这就意味着大多数时候你必须重新载入你的子 类,并且重建你的数据结构。如果你忘记了,将会引起混淆。
-
Python is more dynamic, does less error-checking. 在 Python中,对于未定义的函数或者域,或者传给了函数错误的参数个数,或者其他载入阶段的其他大多数问题,你都不会得到任何警告信息;你必须等到运行 时,才能得到错误信息。商业的Lisp实现将会把这些大多数问题标识为警告;简单的Lisp实现(如clisp)不会。一个演示Python危险性的地方 是,当你想写self.field = 0时,却敲入了 self.feild = 0,后者将会动态的创建一个新的域。与之相对的Lisp等价物为,(setf (feild self) 0),它将给你一个错误。另一方面,访问你一个未定义的域时,两个语言都会报告一个错误。
-
Don't forget self.这点更应该引起Java程序员的注意:在一个方法中,确保你写的是self.field,而不是field。这里没有隐式的作用域(scope)。通常这会引起一个运行时错误。这很令人讨厌,但是我认为过一段时间后,人们将学会不这么干。
-
不要忘记return.写 函数def twice(x): x+x是很诱人的,并且这不会发出任何警告或者异常信号,但是你可能真正想要的是def twice(x): return x+x。这点特别令人厌烦,因为在一个lambda表达式中,return语句是被禁止的,但是它的语义却是执行return。
-
注意单元素元组(tuple)。一 个元组是一个不可变的列表,并且用圆括号围起来,而不是用方括号。()是空元组,(1, 2)是一个含有两个元素的元组,但是(1)表示的是1。而一个元素的元组却要用(1,)表示,我擦!Damian Morton指出,如果你把元组看成是由逗号(,)产生的,打印时带有圆括号的数据,这样就好理解了。圆括号只是起了消除歧义的作用,在这个解释下,1, 2是含有两个元素的元组,1,是含有一个元素的元组,圆括号有时是需要的,这主要取决于元组出现的位置。例如,虽然2, + 2,是一个合法的表达式,但是更加清晰的方式是(2,) + (2,)或者(2, 2)。
-
注意特定的异常 小心: 当key不存在时,dict[key]会抛出KeyError; lisp哈希表的用户则期望得到nil。你应该捕获这个异常或者用key in dict测试。
-
Python是Lisp-1的。 我的意思是Python只有一个命名空间,里面包含了变量和函数,就像scheme一样,而不是想Common Lisp那样,有两个命名空间。例如:
def f(list, len): return list((len, len(list))) ## bad Python (define (f list length) (list length (length list))) ;; bad Scheme (defun f (list length) (list length (length list))) ;; legal Common Lisp
对于域和方法也是一样:你不能提供一个和域名相同的方法名:
class C: def f(self): return self.f ## bad Python ...
-
Python字符串不同于Lisp的符号(symbol)。Python通过把字符串内化到模块或类的哈希表中,然后进行符号查找。也就是说,当你写obj.slot时,Python在运行阶段会去obj类的哈希表中查找字符串"slot"。Python也会内化一些用户代码中的字符串,例如,当你写x = "str"时。但是Python并不内化那些看起来不像变量的字符串,例如x = "a str"(感谢Brian Spilsbur指出这点)。
-
Python没有宏(macro)。 Python可以访问一个程序的抽象语法树,但是这不是适合“心脏不好”的人。从积极的方面来看,该模块容易理解,并且在5分钟内,我用5行代码就可以得到:
>>> parse("2 + 2") ['eval_input', ['testlist', ['test', ['and_test', ['not_test', ['comparison', ['expr', ['xor_expr', ['and_expr', ['shift_expr', ['arith_expr', ['term', ['factor', ['power', ['atom', [2, '2']]]]], [14, '+'], ['term', ['factor', ['power', ['atom', [2, '2']]]]]]]]]]]]]]], [4, ''], [0, '']]
这令我相当失望,同样的表达式在Lisp中的解析结果是(+ 2 2)。看来,只有真正的专家才会想去操纵Python的解析树,相 反,Lisp的解析树对任何人来说都是简单可用。我们任然可以在Python中,通过连接字符串,来创建一些类似于宏的东西,但是它不能和其他语言集成, 所以在实践中不这样做。在Lisp中,两个使用宏的主要目的是:新的控制结构和定制针对特定问题的语言。前者没有在Python中实现。后者可以通过在 Python中,用适合特定问题的数据格式来做到:下面我在Python中定义了一个上下文无关语法,分别通过1)组合字典的内置语法,2)解析字符串的 预处理过程完成的。第一种方式和Lisp的宏一样优雅。但是对于复杂的任务,例如为逻辑编程语言写一个编译器这样的事,在Lisp中很容易,但是在 Python将很困难。
比较Lisp和Python程序
我从《Paradigms of Artificial Intelligence Programming》一书中取了一个简单的随机句子生产器程序,并把它翻译成Python。结论:简介性相当;Python因为grammar[phrase]比 (rule-rhs (assoc phrase *grammar*))简单,而获得一分,但是Lisp因为'(NP VP)比['NP', 'VP']更简介而扳平比分。Python程序很可能比较低效,但是这不是我们关注的点。两个语言看起来都很适合这样的程序。调整浏览器窗口到合适的宽度以便阅读代码。
Lisp程序 simple.lisp | Python程序 simple.py |
(defparameter *grammar* '((sentence -> (noun-phrase verb-phrase)) (noun-phrase -> (Article Noun)) (verb-phrase -> (Verb noun-phrase)) (Article -> the a) (Noun -> man ball woman table) (Verb -> hit took saw liked)) "A grammar for a trivial subset of English.") (defun generate (phrase) "Generate a random sentence or phrase" (cond ((listp phrase) (mappend #'generate phrase)) ((rewrites phrase) (generate (random-elt (rewrites phrase)))) (t (list phrase)))) (defun generate-tree (phrase) "Generate a random sentence or phrase, with a complete parse tree." (cond ((listp phrase) (mapcar #'generate-tree phrase)) ((rewrites phrase) (cons phrase (generate-tree (random-elt (rewrites phrase))))) (t (list phrase)))) (defun mappend (fn list) "Append the results of calling fn on each element of list. Like mapcon, but uses append instead of nconc." (apply #'append (mapcar fn list))) (defun rule-rhs (rule) "The right hand side of a rule." (rest (rest rule))) (defun rewrites (category) "Return a list of the possible rewrites for this category." (rule-rhs (assoc category *grammar*))) | from random import choice grammar = dict( S = [['NP','VP']], NP = [['Art', 'N']], VP = [['V', 'NP']], Art = ['the', 'a'], N = ['man', 'ball', 'woman', 'table'], V = ['hit', 'took', 'saw', 'liked'] ) def generate(phrase): "Generate a random sentence or phrase" if isinstance(phrase, list): return mappend(generate, phrase) elif phrase in grammar: return generate(choice(grammar[phrase])) else: return [phrase] def generate_tree(phrase): """Generate a random sentence or phrase, with a complete parse tree.""" if isinstance(phrase, list): return map(generate_tree, phrase) elif phrase in grammar: return [phrase] + generate_tree(choice(grammar[phrase])) else: return [phrase] def mappend(fn, list): "Append the results of calling fn on each element of list." return reduce(lambda x,y: x+y, map(fn, list)) |
Running the Lisp Program | Running the Python Program |
> (generate 'S) (the man saw the table) | >>> generate('S') ['the', 'man', 'saw', 'the', 'table'] >>> ' '.join(generate('S')) 'the man saw the table' |
Python中的grammer比Lisp中的丑陋,这让我很担心,所以我考虑在Python中写一个解析器(后来发现已经有一些写好的,并且可以免费获得的),以及重载一些内置的操作符。第二种方法在一些应用中是可行的,例如我写Expr class, 这是用来表现和操纵逻辑表达式的。但是对于这个应用而言,一个简单、定制的语法规则解析器就够了:一个语法规则是一个用“|”分开的,可选部分的列表,每 个可选部分都是由空格(" ")分隔的单词列表。把grammar程序重写为一个更加符合Python惯用法的程序,而不是Lisp程序的翻译,下面就是该程序:
Python Program simple.py (idiomatic version) |
"""Generate random sentences from a grammar. The grammar consists of entries that can be written as S = 'NP VP | S and S', which gets translated to {'S': [['NP', 'VP'], ['S', 'and', 'S']]}, and means that one of the top-level lists will be chosen at random, and then each element of the second-level list will be rewritten; if it is not in the grammar it rewrites as itself. The functions rewrite and rewrite_tree take as input a list of symbols. The functions generate and generate_tree are convenient interfaces to rewrite and rewrite_tree that accept a string (which defaults to 'S') as input.""" import random def make_grammar(**grammar): "Create a dictionary mapping symbols to alternatives." for (cat, rhs) in grammar.items(): grammar[cat] = [alt.split() for alt in rhs.split('|')] r eturn grammar grammar = make_grammar( S = 'NP VP', NP = 'Art N', VP = 'V NP', Art = 'the | a', N = 'man | ball | woman | table', V = 'hit | took | saw | liked' ) def rewrite(symbols): "Replace each non-terminal symbol in the list with a random entry in grammar (recursively)." return [terminal for symbol in symbols for terminal in (rewrite(random.choice(grammar[symbol])) if symbol in grammar else [symbol])] def rewrite_tree(symbols): "Replace the list of symbols into a random tree, chosen from grammar." return [{symbol: rewrite_tree(random.choice(grammar[symbol]))} if symbol in grammar else symbol for symbol in symbols] def generate(symbols='S'): """Replace symbol(s) in the space-delimited input string by a random entry in grammar (recursively until terminals); join back into a string.""" return ' '.join(rewrite(symbols.split())) def generate_tree(symbols='S'): "Replace symbol(s) in the space-delimited input string by a random entry in grammar (recursively until terminals); return a tree.""" return rewrite_tree(symbols.split()) |