Lisp的读取-求值-输出循环与Python的有何不同?

69
我遇到了Richard Stallman的下面这句话:“当你启动Lisp系统时,它会进入一个读取-求值-输出(read-eval-print)循环。大多数其他语言都没有相应的读取,求值和输出功能。这是令人震惊的缺陷!” 现在,我在Lisp中编写代码很少,但我在Python中写了相当多的代码,最近也稍微写了一些Erlang代码。我的印象是这些语言也提供了读取-求值-输出循环,但Stallman不同意(至少对于Python):
“有人告诉我Python在根本上与Lisp相似,我浏览了Python文档后得出的结论是并非如此。当你开始使用Lisp时,它会执行“读取”,“求值”和“输出”,而这三个功能在Python中都是缺失的。”
Lisp的REPL和Python有根本技术差异吗?您可以举例说明Lisp REPL所能轻松实现的事情,在Python中难以做到的是什么?
4个回答

64

支持Stallman的观点,Python在以下几个方面与典型的Lisp系统不同:

  • Lisp中的read函数读取一个S表达式,它表示任意的数据结构,可以被视为数据,也可以作为代码进行评估。Python中最接近的东西读取单个字符串,如果您想让它有意义,就必须自己解析。

  • Lisp中的eval函数可执行任何Lisp代码。Python中的eval函数仅评估表达式,并需要exec语句运行语句。但是这两个函数都使用文本表示的Python源代码工作,如果要“eval” Python AST,则必须跨越一堆障碍。

  • Lisp中的print函数以与read接受的完全相同的形式编写S表达式。print在Python中打印出由您正在尝试打印的数据定义的内容,这肯定不总是可逆的。

Stallman的声明有些虚伪,因为显然Python确实有名为evalprint的函数,但它们所做的事情与他期望的不同(并且较差)。

在我看来,Python确实具有与Lisp相似的一些方面,我可以理解为什么人们可能会建议Stallman尝试Python。然而,正如Paul Graham在《Lisp有何不同》中所述,任何包括Lisp所有功能的编程语言,必须 Lisp。


2
事实上,RMS 可能更喜欢 print() 使用 repr() 而不是 str()。也就是说,print(repr(eval(raw_input("> ")))) 很接近 REPL。 - Frédéric Hamidi
1
@user4815162342:是的,这就是我所说的“跳过一堆障碍”的意思。 - Greg Hewgill
11
Python将数据和代码区分开来,而LISP不会。参见格林斯潘第十条规则 - Henk Langeveld
3
它是否如此呢?Python拥有一流的代码表示;而Lisp以字符序列的形式接受文本输入。 - Marcin
2
但是 Python 交互提示符并不读取 "单个字符串"。它读取一个完整的表达式(通常跨越多行),可以评估为数据或代码(表达式或语句)。由于在 Python 中函数是一等对象,所以相当于 eval 的是简单地运行该对象:name(),如果 name 是指函数。只有 print 确实具有不同的属性:打印 Python 表达式或函数通常不能给我们再次解析相同的东西。 - alexis
显示剩余4条评论

33

Stallman认为,与Lisp相比,Python的REPL没有实现显式的“reader”,导致其看起来不够完整。Reader是将文本输入流转换为内存的组件,就像语言中集成的XML解析器一样,用于源代码和数据。这不仅有助于编写宏(在理论上,Python可以使用ast模块实现),还有助于调试和内省。

假设你对如何实现incf特殊形式感兴趣,可以这样测试:

[4]> (macroexpand '(incf a))
(SETQ A (+ A 1)) ;

然而, incf 的作用不仅仅是增加符号值。当被要求增加哈希表条目时,它到底做了什么?让我们看一下:

[2]> (macroexpand '(incf (gethash htable key)))
(LET* ((#:G3069 HTABLE) (#:G3070 KEY) (#:G3071 (+ (GETHASH #:G3069 #:G3070) 1)))
 (SYSTEM::PUTHASH #:G3069 #:G3070 #:G3071)) ;

在这里,我们了解到incf调用一个特定于系统的puthash函数,这是这个Common Lisp系统的实现细节。请注意,“打印机”如何利用“阅读器”已知的功能,例如使用#:语法引入匿名符号,并在扩展表达式的范围内引用相同的符号。在Python中模拟这种检查会更冗长,而且不太容易访问。

除了在REPL上的明显用途外,有经验的Lisper还使用printread作为一种简单且易于使用的序列化工具,类似于XML或JSON。虽然Python有等效于Lisp的printstr函数,但它缺乏read的等效函数,最接近的等效函数是eval。当然,eval混淆了解析和评估这两个不同的概念,这导致了像这样的问题这样的解决方案,并且是Python论坛上反复出现的主题。这在Lisp中不会成为问题,因为阅读器和评估器是清晰分离的。

最后,读取器工具的高级功能使程序员能够以甚至宏无法实现的方式扩展语言。一个典型的例子是Mark Kantrowitz的infix,实现了完整功能的中缀语法作为读取宏。


22
在基于Lisp的系统中,通常在程序运行时从REPL(读取评估打印循环)中开发程序。因此,它集成了一堆工具:完成、编辑器、命令行解释器、调试器等。默认情况下是这样的。输入带有错误的表达式-您将进入另一个REPL级别,启用了一些调试命令。您实际上必须采取某些措施来消除这种行为。
您可以有两个不同的REPL概念含义:
1. 读取评估打印循环,就像在Lisp(或其他几种类似语言)中一样。它读取程序和数据,评估并打印结果数据。Python不是以这种方式工作的。Lisp的REPL允许您直接以元编程方式工作,编写生成(代码)的代码,检查扩展,转换实际代码等。Lisp具有read / eval / print作为顶级循环。Python具有类似于readstring / evaluate / printstring的顶级循环。
2. 命令行界面。交互式shell。例如,参见IPython。将其与Common Lisp的SLIME进行比较。
Python的默认shell在默认模式下不太适合交互式使用,功能不是很强大。
Python 2.7.2 (default, Jun 20 2012, 16:23:33) 
[GCC 4.2.1 Compatible Apple Clang 4.0 (tags/Apple/clang-418.0.60)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> a+2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>> 

您遇到了错误信息,仅此而已。

与 CLISP REPL 相比较:

rjmba:~ joswig$ clisp
  i i i i i i i       ooooo    o        ooooooo   ooooo   ooooo
  I I I I I I I      8     8   8           8     8     o  8    8
  I  \ `+' /  I      8         8           8     8        8    8
   \  `-+-'  /       8         8           8      ooooo   8oooo
    `-__|__-'        8         8           8           8  8
        |            8     o   8           8     o     8  8
  ------+------       ooooo    8oooooo  ooo8ooo   ooooo   8

Welcome to GNU CLISP 2.49 (2010-07-07) <http://clisp.cons.org/>

Copyright (c) Bruno Haible, Michael Stoll 1992, 1993
Copyright (c) Bruno Haible, Marcus Daniels 1994-1997
Copyright (c) Bruno Haible, Pierpaolo Bernardi, Sam Steingold 1998
Copyright (c) Bruno Haible, Sam Steingold 1999-2000
Copyright (c) Sam Steingold, Bruno Haible 2001-2010

Type :h and hit Enter for context help.

[1]> (+ a 2)

*** - SYSTEM::READ-EVAL-PRINT: variable A has no value
The following restarts are available:
USE-VALUE      :R1      Input a value to be used instead of A.
STORE-VALUE    :R2      Input a new value for A.
ABORT          :R3      Abort main loop
Break 1 [2]> 

CLISP使用Lisp的条件系统打断进入调试器REPL。它会呈现一些重启选项。在错误上下文中,新的REPL提供了扩展命令。
让我们使用重启:
Break 1 [2]> :r1
Use instead of A> 2
4
[3]> 

因此,您可以获得程序的交互式修复和执行运行...

4
是的,但Python的特点在于其解释器提示符确实促进了交互式开发。看起来RMS看了Python,正确地得出它实际上并不是Lisp,并宣布它不如Lisp。 - Marcin
5
@Marcin,是的。但默认的“解释器”提示不太适合交互式开发,只能像CLI一样使用,而不能像读取/评估/打印(REPL)那样。Stallman来自一个交互式提示功能更强大的世界,其中包括他自己的Emacs。 - Rainer Joswig
3
实际上,在我的经验中,我不会说Common Lisp repl本身更有帮助。 - Marcin
3
我考虑使用CLISP、CMUCL和SBCL交互式解释器。将IPython与SLIME进行比较就像将sed与Emacs进行比较,它们根本不是同一回事,无论如何,都不是这个问题的主题。 - Marcin
3
我的印象是,CLISP的REPL比Python默认提供的更强大。 - Rainer Joswig
显示剩余5条评论

7

Python的交互模式与从文件中读取代码的模式在几个关键方面有所不同,这可能与语言的文本表示方式有关。Python也不是同构的,这使我称其为“交互模式”而不是“读取-评估-打印循环”。除此之外,我认为这更多是程度上的差异,而不是本质上的差异。

现在,在Python代码文件中,您可以轻松插入空白行,这实际上接近于“本质上的差异”:

def foo(n):
  m = n + 1

  return m

如果你尝试将相同的代码粘贴到解释器中,它会认为该函数是“已关闭”的,并抱怨您在错误的缩进处有一个裸露的返回语句。这在(通用)Lisp中不会发生。
此外,在Common Lisp(CL)中有一些非常方便的便利变量,在Python中没有(至少我不知道)。 CL和Python都有“上一个表达式的值”(在CL中为 * ,在Python中为 _ ),但CL还具有 ** (上一个表达式的值)和 *** (之前那个的值)以及 + ++ +++ (表达式本身)。 CL还不区分表达式和语句(实质上,一切都是表达式),所有这些都有助于构建更丰富的REPL体验。
正如我在开头所说,这更多是程度上的差异而不是本质上的差异。但如果它们之间的差距只有一点点更大,那么它可能也是一种本质上的差异。

1
“read-eval-print loop”这个术语中隐含了什么样的同构性? - Marcin
2
@Marcin 这不是一个严格的要求,但我听到过“读取-求值-输出循环”这个术语只用于同构语言,其他语言倾向于使用“交互模式”或“解释器”(如果Python有一个REPL,Sinclair Basic也有)。 - Vatine
是的,我认为可以公正地说Sinclair Basic有一个REPL。 - Marcin
1
Sinclair Basic的交互提示不是REPL,因为它缺少打印部分。它只打印您要求打印的内容,而且它打印的内容通常无法读回。 - Marko Topolnik
1
@MarkoTopolnik:在这种情况下,Python也不是(在Sinclair Basic中,“3+4”不是有效语句(在Python中是,并导致写入7),“LET I=3+4”不打印任何内容,而“i=3+4”在Python中也是如此;Sinclair Basic最接近的是“PRINT 3+4”,并且会打印7)。 - Vatine
2
@Vatine 是的,这就是我们在这里讨论的问题:Python的交互式提示符不是REPL。此外,请注意Sinclair的提示符甚至更远离REPL:您无法重用它打印的任何内容。即使是TTY的概念也不存在,输出历史记录也不会被保留,就像在连续进纸打印机(原始TTY)上一样。 - Marko Topolnik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接