作者: Peter Norvig ,译者: jineslong <zzljlu@gmail.com>
这是一篇为Lisp程序员写的 Python 简介(一些Python程序员告诉我,这篇文章对他们学习Lisp也有帮助,尽管这不是我的本意)。基本上,Python可以看作一个拥有“传统”语法(Lisp社区称之为“中缀”或者“m-lisp”语法)的Lisp方言。一个来自comp.lang.python的帖子说到“我一直不明白为什么LISP是一个不错的想法,直到我开始玩上了Python”。Python支持除了宏(macros)之外的所有Lisp的关键特征,并且你不会太想念宏因为Python拥有求值,运算符重载,和正则表达式解析,所以一些(不是所有)宏的使用场景都被涵盖了。
我深入学习Python的原因是,我正在考虑把Russel & Norvig的 人工智能教材 配套的 Lisp代码 转化成Java代码。一些教师和学生想要Java代码,因为:
,我可以定位于Java的JVM。
。这点是很重要的,因为一些同学抱怨说,他们在把书中的伪代码和网上的Lisp代码对应起来的过程中,有一段困难的时间(即使这个过程对Lisp程序员来说是显然的)。
从我的观点来看,Python的两个主要的缺点是:1)只有很少的编译时的错误分析(compile-time error analysis)和类型声明(type declaration),甚至比Lisp还少。2)运行时间比Lisp慢很多,通常相差10倍(有时100倍,有时1倍)。定性地说,Python和解释型的Lisp的速度差不多,但是很明显地慢于编译型的Lisp。基于这个原因,对于那些(或者会逐渐变为)计算密集性的应用(除非你愿意把速度瓶颈部分用c重写),我不会推荐使用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: yelse: 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 varClose 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相对于其他语言而言的执行速度如何。我很难得到和 你的 应用相关的基准测试数据,但是下面这个表可能对你有用:
测试 | 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倍。
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 ...
>>> 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程序 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('|')] return 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()) |
是有Yusuke Shinyama完成。
Peter Norvig