Python解释器简介(4):动态语言

472 查看

这是Python解释器简介的第四部分。阅读第一部分第二部分第三部分。如果你喜欢这个系列的话,那就把它分享到Hacker School吧。我是那里的管理员。

当我开始研究python内部工作的时候,我一直很困惑为什么能够进行“编译”的python还是“动态语言”。通常,我们都将这两个词作为一对反义词——“动态语言”1包括Python、Ruby和 Javascript等,而“编译语言”则包括C、Java和Haskell等。

人们通常所说的“编译语言”是指能够编译出适用于x86、ARM等的指令2(作用于真正的机器)的语言。一种“解释性”语言不是根本就没有编译器3就是只编译成一个中间表示,比如字节码。字节码的指令不是作用于任何硬件的,而是虚拟机。Python就属于后者:Python的编译器将生成的字节码传递给Python解释器。4

Python解释器将通过虚拟机做许多工作使得字节码得以解释。至于虚拟机,我们会在第五部分讨论。

目前为止,我们对编译和解释的概念还是抽象的。通过下面这个例子我们能更加清晰:

这是一个函数以及它的字节码通过反汇编程序的结果。一旦我们定义了modulus函数,它就被编译了并且生成了一个不能被修改的代码对象。

这应该很容易推算。键入modulus(%)使编译器发出指令BINARY_MODULO。所以如果我们要计算一个余数的话,这个函数就能够发挥作用了。

这样看,它正常工作。但如果我们不传递数字给它呢?

慢着,这是怎么了?你以前也许见到过,但它通常是这么写的:

当BINARY_MODULO处理两个字符串的时候,它默认执行字符串插值而不是求余数。这就是动态类型的典型例子。编译器在生成modulus的代码对象的时候,它完全不知道x和 y是字符串、数字还是其它类型。它只是发出一些指令而已:加载一个名字,加载另一个名字,BINARY_MODULO这个两个对象,然后返回结果。至于弄清BINARY_MODULO真正指什么则是解释器的工作。

我想我们忽略了一些东西。我们的函数modulus能够计算余数或者格式化字符串,还有吗?如果我们定义一个能够响应__mod__的自定义对象的话,我们还能做很多。

虽然还是那个有着同样字节码的函数modulus,但是当它传递不同种对象的时候,作用并不相同。Modulus也可能报错——比如,如果我们使用一个不能执行__mod__的对象就会有TypeError。更不可思议的是,当__mod__被调用的时候,我们能写出一个造成SystemExit的自定义对象。__mod__函数能够对文件进行写入操作;改变一个全局变量或者删除对象的另外一个性质。我们几乎能做任何事。

这也是难以优化Python的原因之一:在编译代码对象和生成字节码的时候,你并不知道会有怎样的结果。编译器根本不关心结果如何。就像Russell Power 和 Alex Rubinsteyn 所说的:“我们能使解释性语言Python有多快?”,“由于不用声明类型信息,几乎每个指令都要像INVOKE_ARBITRARY_METHOD一样来执行。”

尽管“编译”和“解释”的定义在通常情况下是很难区分的,但是对于Python来说却很简单。编译工作就是生成代码对象,包括字节码。而翻译的工作就是翻译字节码,执行指令。Python保持“动态”特性的原因之一就是同样的字节码能够有不同的作用。更普遍的说法就是Python解释器的工作比编译器的多一些。

在第五部分,我们就会讨论虚拟机和解释器了。

注:

  1. 你通常听到的“解释语言”就是“动态语言”。
  2. 感谢David Nolen给出了这个定义。“语法分析”、“编译”和“解释”的定义很易混淆。
  3. 一些语言并不在R、Scheme、二进制文件中都进行编译。这与你使用的开发工具和你对“编译”的定义有关。
  4. 尽管我和以往一样是基于CPython 和Python 2.7来写本文的,但是本文大部分内容也适于其它开发工具。