为什么有这么多蟒蛇? Python 实现比较

已发表: 2022-03-11

Python是惊人的。

令人惊讶的是,这是一个相当模棱两可的说法。 我所说的“Python”是什么意思? 我的意思是 Python 是抽象接口吗? 我的意思是 CPython,常见的 Python实现(不要与名称相似的 Cython 混淆)? 还是我的意思完全不同? 也许我是在间接地指代 Jython、IronPython 或 PyPy。 或者,也许我真的走得太远了,我在谈论 RPython 或 RubyPython(它们是非常非常不同的东西)。

虽然上面提到的技术是通用名称和通用引用的,但其中一些用于完全不同的目的(或者,至少以完全不同的方式运行)。

在我使用 Python 接口的整个过程中,我遇到了大量的 .*ython 工具。 但直到最近我才花时间了解它们是什么、它们是如何工作的以及它们为什么是必要的(以它们自己的方式)。

在本教程中,我将从头开始,逐步介绍各种 Python 实现,最后对 PyPy 进行全面介绍,我相信这是该语言的未来。

这一切都始于对“Python”实际上是什么的理解。

如果您对机器代码、虚拟机等有很好的了解,请随意跳过。

“Python 是解释的还是编译的?”

对于 Python 初学者来说,这是一个常见的困惑点。

进行比较时首先要意识到“Python”是一个接口。 有一个关于 Python该做什么以及它应该如何表现的规范(与任何接口一样)。 并且有多种实现(与任何接口一样)。

要意识到的第二件事是“解释”和“编译”是实现的属性,而不是接口

所以这个问题本身并不是很好形成的。

Python 是解释的还是编译的? 这个问题真的不是很好。

也就是说,对于最常见的 Python 实现(CPython:用 C 编写,通常简称为“Python”,如果您不知道我在说什么,那么您肯定会使用什么),答案是:解释, 有一些编译。 CPython 将 * Python 源代码编译成字节码,然后解释这个字节码,边执行边执行。

*注意:这不是传统意义上的“编译”。 通常,我们会说“编译”是采用高级语言并将其转换为机器代码。 但它是一种“汇编”。

让我们更仔细地看一下这个答案,因为它将帮助我们理解文章后面出现的一些概念。

字节码与机器码

理解字节码与机器码(也称为本机代码)之间的区别非常重要,也许最好通过示例来说明:

  • C 编译为机器代码,然后直接在您的处理器上运行。 每条指令都会指示你的 CPU 移动东西。
  • Java 编译为字节码,然后在 Java 虚拟机 (JVM) 上运行,JVM 是执行程序的计算机的抽象。 然后,每条指令都由与您的计算机交互的 JVM 处理。

简而言之:机器码要快得多,但字节码更便携、更安全

机器码在不同的机器上看起来不同,但字节码在所有机器上看起来都是一样的。 有人可能会说机器代码已针对您的设置进行了优化。

回到CPython实现,工具链流程如下:

  1. CPython 将您的 Python 源代码编译成字节码。
  2. 然后在 CPython 虚拟机上执行该字节码。
初学者通常认为 Python 是因为 .pyc 文件而编译的。 这是有道理的:.pyc 文件是编译后的字节码,然后会被解释。 因此,如果您之前运行过 Python 代码并且手边有 .pyc 文件,那么第二次运行速度会更快,因为它不必重新编译字节码。

替代 VM:Jython、IronPython 等

正如我之前提到的,Python 有几个实现。 同样,如前所述,最常见的是 CPython,但为了本比较指南,还应提及其他一些。 这是一个用 C 编写的 Python 实现,被认为是“默认”实现。

但是其他 Python 实现呢? 其中一个比较突出的是 Jython,它是一种使用 JVM 编写的 Java 的 Python 实现。 CPython 生成字节码以在 CPython VM 上运行,而 Jython 生成Java 字节码以在 JVM 上运行(这与编译 Java 程序时生成的东西相同)。

此 Python 实现图中描述了 Jython 对 Java 字节码的使用。

“你为什么要使用替代实现?”,你可能会问。 好吧,一方面,这些不同的 Python 实现与不同的技术堆栈很好地配合

CPython 使得为您的 Python 代码编写 C 扩展非常容易,因为它最终由 C 解释器执行。 另一方面,Jython 使得使用其他 Java 程序变得非常容易:您可以毫不费力地导入任何Java 类,从您的 Jython 程序中调用和利用您的 Java 类。 (顺便说一句:如果您没有仔细考虑过,这实际上是疯了。我们现在可以混合和混合不同的语言并将它们全部编译成相同的内容。(正如 Rostin 所提到的,程序混合 Fortran 和 C 代码已经存在了一段时间。所以,当然,这不一定是新的。但它仍然很酷。))

例如,这是有效的 Jython 代码:

 [Java HotSpot(TM) 64-Bit Server VM (Apple Inc.)] on java1.6.0_51 >>> from java.util import HashSet >>> s = HashSet(5) >>> s.add("Foo") >>> s.add("Bar") >>> s [Foo, Bar]

IronPython 是另一种流行的 Python 实现,完全用 C# 编写并针对 .NET 堆栈。 特别是,它运行在您可能称之为 .NET 虚拟机上,即 Microsoft 的公共语言运行时 (CLR),与 JVM 相当。

您可能会说Jython : Java :: IronPython : C# 。 它们在相同的虚拟机上运行,​​您可以从 IronPython 代码中导入 C# 类,从 Jython 代码中导入 Java 类等。

完全有可能在不接触非 CPython Python 实现的情况下生存下来。 但是切换也有一些优势,其中大部分取决于您的技术堆栈。 使用大量基于 JVM 的语言? Jython 可能适合您。 关于 .NET 堆栈? 也许您应该尝试 IronPython(也许您已经尝试过)。

这个 Python 比较图表展示了 Python 实现之间的差异。

顺便说一句:虽然这不是使用不同实现的理由,但请注意,这些实现在行为上确实有所不同,而不仅仅是它们如何处理您的 Python 源代码。 然而,这些差异通常很小,并且随着时间的推移而消失或出现,因为这些实现正在积极开发中。 例如,IronPython 默认使用 Unicode 字符串; 然而,CPython 2.x 版本默认为 ASCII(非 ASCII 字符出现 UnicodeEncodeError 失败),但 3.x 版本默认支持 Unicode 字符串。

即时编译:PyPy 和未来

所以我们有一个用 C 语言编写的 Python 实现,一个用 Java 编写,一个用 C# 编写。 下一个合乎逻辑的步骤:用...... Python 编写的 Python 实现。 (受过教育的读者会注意到这有点误导。)

这就是事情可能会变得混乱的地方。 首先,让我们讨论一下即时 (JIT) 编译。

JIT:为什么和如何

回想一下,本机机器代码比字节码快得多。 好吧,如果我们可以编译一些字节码然后将其作为本机代码运行呢? 我们必须付出一些代价来编译字节码(即时间),但如果最终结果更快,那就太好了! 这就是 JIT 编译的动机,这是一种混合了解释器和编译器优点的混合技术。 基本上,JIT 希望利用编译来加速解释系统。

例如,JIT 采用的一种常见方法:

  1. 识别频繁执行的字节码。
  2. 将其编译为本机机器码。
  3. 缓存结果。
  4. 每当设置运行相同的字节码时,取而代之的是获取预编译的机器码并获得好处(即速度提升)。

这就是 PyPy 实现的全部内容:将 JIT 引入 Python(参见附录了解以前的工作)。 当然,还有其他目标:PyPy 的目标是跨平台、内存轻量和无堆栈支持。 但JIT确实是它的卖点。 作为一系列时间测试的平均值,据说可以将性能提高 6.27 倍。 有关细分,请参阅 PyPy 速度中心的此图表:

使用 PyPy 实现将 JIT 引入 Python 接口可以提高性能。

PyPy 很难理解

PyPy 潜力巨大,在这一点上它与 CPython 高度兼容(因此它可以运行 Flask、Django 等)。

但是围绕 PyPy 存在很多混淆(例如,请参阅这个创建 PyPyPy 的荒谬提议……)。 在我看来,这主要是因为 PyPy 实际上是两件事:

  1. 用 RPython(不是 Python(我之前撒过谎))编写的 Python 解释器。 RPython 是具有静态类型的 Python 子集。 在 Python 中,对类型进行严格的推理“几乎是不可能的”(为什么这么难?考虑一下以下事实:

     x = random.choice([1, "foo"])

    将是有效的 Python 代码(归功于 Ademan)。 x的类型是什么? 当类型甚至没有严格执行时,我们如何推断变量的类型?)。 使用 RPython,您牺牲了一些灵活性,但反而使推理内存管理和诸如此类的事情变得更加容易,从而可以进行优化。

  2. 为各种目标编译 RPython 代码并添加 JIT 的编译器。 默认平台是 C,即 RPython-to-C 编译器,但您也可以针对 JVM 和其他平台。

仅为了在此 Python 比较指南中清晰起见,我将它们称为 PyPy (1) 和 PyPy (2)。

为什么你需要这两件事,为什么在同一个屋檐下? 可以这样想:PyPy (1) 是用 RPython 编写的解释器。 因此它接收用户的 Python 代码并将其编译为字节码。 但是解释器本身(用 RPython 编写)必须由另一个 Python 实现解释才能运行,对吧?

好吧,我们可以只使用 CPython 来运行解释器。 但这不会很快。

相反,我们的想法是我们使用 PyPy (2)(称为 RPython 工具链)将 PyPy 的解释器编译为另一个平台(例如,C、JVM 或 CLI)的代码以在我们的机器上运行,在 JIT 中添加为好吧。 这很神奇:PyPy 动态地将 JIT 添加到解释器中,生成自己的编译器! (同样,这很疯狂:我们正在编译一个解释器,添加另一个单独的独立编译器。

最后,结果是一个独立的可执行文件,它解释 Python 源代码并利用 JIT 优化。 这正是我们想要的! 这是一个拗口,但也许这张图会有所帮助:

此图说明了 PyPy 实现的美妙之处,包括解释器、编译器和带有 JIT 的可执行文件。

重申一下,PyPy 的真正美妙之处在于,我们可以用 RPython 为自己编写一堆不同的 Python 解释器,而不必担心 JIT。 然后 PyPy 将使用 RPython 工具链/PyPy (2) 为我们实现 JIT

事实上,如果我们变得更抽象,理论上你可以为任何语言编写一个解释器,将其提供给 PyPy,然后获得该语言的 JIT。 这是因为 PyPy 专注于优化实际的解释器,而不是它所解释的语言的细节。

从理论上讲,您可以为任何语言编写解释器,将其提供给 PyPy,并获得该语言的 JIT。

作为一个简短的题外话,我想提一下 JIT 本身绝对令人着迷。 它使用一种称为跟踪的技术,其执行方式如下:

  1. 运行解释器并解释所有内容(不添加 JIT)。
  2. 对解释的代码进行一些简单的分析。
  3. 确定您之前执行的操作。
  4. 将这些代码位编译为机器代码。

更重要的是,这篇论文很容易获得并且非常有趣。

总结一下:我们使用 PyPy 的 RPython-to-C(或其他目标平台)编译器来编译 PyPy 的 RPython 实现的解释器。

包起来

在对 Python 实现进行了长时间的比较之后,我不得不问自己:为什么这么棒? 为什么这个疯狂的想法值得追求? 我认为 Alex Gaynor 在他的博客上说得很好:“[PyPy 是未来],因为 [它] 提供了更快的速度、更大的灵活性,并且是 Python 发展的更好平台。”

简而言之:

  • 它很快,因为它将源代码编译为本机代码(使用 JIT)。
  • 它很灵活,因为它只需要很少的额外工作就可以将 JIT 添加到您的解释器中。
  • 它很灵活(再次)因为你可以用 RPython 编写你的解释器,它比 C 更容易扩展(事实上,它很容易,有一个编写你自己的解释器的教程)。

附录:您可能听说过的其他 Python 名称

  • Python 3000 (Py3k):Python 3.0 的替代命名,这是一个主要的、向后不兼容的 Python 版本,于 2008 年推出。Py3k 团队预测,要完全采用这个新版本大约需要五年时间。 尽管大多数(警告:传闻)Python 开发人员继续使用 Python 2.x,但人们越来越意识到 Py3k。

  • Cython:Python 的超集,包括调用 C 函数的绑定。
    • 目标:允许您为 Python 代码编写 C 扩展。
    • 还允许您将静态类型添加到现有 Python 代码中,使其能够被编译并达到类似 C 的性能。
    • 这类似于 PyPy,但不一样。 在这种情况下,您在将用户代码传递给编译器之前强制输入用户代码。 使用 PyPy,您可以编写普通的旧 Python,编译器会处理任何优化。

  • Numba:一种“即时专业化编译器”,可将 JIT 添加到带注释的 Python 代码中。 用最基本的术语来说,你给它一些提示,它会加速你的部分代码。 Numba 是 Anaconda 发行版的一部分,这是一组用于数据分析和管理的软件包。

  • IPython:与其他讨论的内容非常不同。 Python的计算环境。 交互式支持 GUI 工具包和浏览器体验等。

  • Psyco:一个 Python 扩展模块,也是早期的 Python JIT 工作之一。 然而,它后来被标记为“无人维护和死亡”。 事实上,Psyco 的首席开发人员 Armin Rigo 现在正在研究 PyPy。

Python 语言绑定

  • RubyPython:Ruby 和 Python VM 之间的桥梁。 允许您将 Python 代码嵌入到您的 Ruby 代码中。 您定义 Python 的启动和停止位置,RubyPython 在 VM 之间编组数据。

  • PyObjc:Python 和 Objective-C 之间的语言绑定,充当它们之间的桥梁。 实际上,这意味着您可以使用 Python 代码中的 Objective-C 库(包括创建 OS X 应用程序所需的一切),以及 Objective-C 代码中的 Python 模块。 在这种情况下,用 C 编写 CPython 很方便,C 是 Objective-C 的一个子集。

  • PyQt:PyObjc 为您提供 OS X GUI 组件的绑定,PyQt 为 Qt 应用程序框架提供相同的功能,让您创建丰富的图形界面、访问 SQL 数据库等。另一个旨在将 Python 的简单性引入其他框架的工具。

JavaScript 框架

  • pyjs (Pyjamas):一个用 Python 创建 Web 和桌面应用程序的框架。 包括 Python 到 JavaScript 编译器、小部件集和一些其他工具。

  • Brython:一个用 JavaScript 编写的 Python VM,允许在浏览器中执行 Py3k 代码。