Python 动态性致打包部署难,现有解决方案各有优劣,未来或有原生方案
有没有想过,为何将 Python 应用程序及其依赖项打包成单个可执行文件如此困难?问题就出在 Python 的动态性上。要是 Python 开发者对这门语言有个共同抱怨,那往往就是:为何把 Python 程序部署成独立软件包这么难,而 C、C++、Rust、Go 甚至 Java 都能做到?难道用 Python 程序前,得先让每个人都安装 Python 运行时?为何解决这问题的变通方法都这么笨拙?
Python 吸引人的动态性,也是其应用程序难以打包和部署的原因。这并非做不到,但颇具挑战。打包后的 Python 应用程序会变成大软件包,至少有十几兆字节甚至更大。而且,创建这些软件包的工具也不够友好便捷。那么,Python 的动态性究竟是怎样导致这一问题的呢?
Python 动态性的利弊
说 Python 是“动态”语言,不只是指它通过解释器执行。还意味着,Python 应用程序行为的很多决策是在运行时做出的,而非提前确定。Python 的许多便利之处都源于这一设计选择。变量无需提前声明,不再使用时会自动进行垃圾回收。导入可以提前声明,也能在运行时生成——理论上,它们可以从任何地方导入代码。Python 代码甚至能在运行时生成和解释。
这些灵活行为是有代价的:很难预测 Python 程序在运行时会做什么。其中一个难点在于,Python 程序中的代码理论上可以被其他代码修改。可以导入一个库,重写其方法,甚至更改其字节码。很难对 Python 进行高性能优化,因为许多优化都依赖于提前了解代码的行为(尽管新的 JIT 等改进正在改善这一情况)。
这产生了两个重大后果:运行 Python 程序最可靠的方法是通过 Python 运行时实例,这样才能重现 Python 的所有动态行为。任何将 Python 应用程序转化为某种可再分发软件包的解决方案,都必须以某种形式包含运行时。而任何只包含“刚好足够”的 Python 运行时来运行程序的解决方案,都会违背 Python 动态性的承诺。
将 Python 应用程序打包成独立使用的软件包很困难,因为很难预测应用程序在运行时需要哪些 Python 功能。这并非不可能,但难度很大。这也意味着应用程序所需的任何第三方库都必须完整地与应用程序打包在一起。
第三方库:要么全有,要么全无
Python 应用程序需要通过 `pyproject.toml` 或 `requirements.txt` 明确声明运行所需的库。此外,Python 的动态性意味着无法假设这些库的哪些部分会被实际使用。在 C++ 或 Rust 的世界里,可以编译静态链接的二进制文件,省略程序中未调用的任何代码。但 Python 库不能这样工作;库的任何部分都可能在任何时候被任何代码调用。因此,整个库——包括其所有依赖项(如二进制文件)——都必须包含在内。
所以,任何将 Python 应用程序打包成独立可执行文件的尝试都必须包含其所有依赖项。结果可能是一个相当大的软件包,大到会让那些不想向用户交付 300MB 软件包的人望而却步。但 Python 的动态性要求包含所有内容。理论上,可以跟踪 Python 程序的调用路径并进行“摇树优化”——移除从未被调用的部分。但这只适用于程序的特定运行情况。要保证这对程序的任何运行情况(包括利用 Python 动态性的情况)都有效,几乎是不可能的。
可行的解决方案:完整打包
所有这些问题意味着,可靠地部署 Python 程序只有几种方法:
- 安装到现有的 Python 解释器中:这是最常见的情况,但需要设置一个解释器副本。这至少意味着要单独进行一个步骤,如果系统中已经存在 Python 版本,这个步骤会充满复杂性。这也是人们一开始就想避免的情况,因为他们希望应用程序尽可能易于重新分发。
- 将解释器与程序及其依赖项打包在一起:像 PyInstaller 和 Nuitka 等项目采用了这种方法。缺点是交付的软件包往往很大,而且创建它们需要了解这些项目的特性。但它们确实可行。
- 使用 Docker 等系统打包程序:Docker 容器有其自身的权衡。一方面,你可以获得运行程序所需的一切,包括任何系统级依赖项。另一方面,生成的容器可能非常大。当然,使用 Docker 意味着要采用一个额外的软件生态系统。
一些针对这个问题的新解决方案试图解决某个特定的痛点,以此让整个问题变得更容易接受。例如,PyApp 使用 Rust 构建一个自解压二进制文件,用于安装所需的 Python 发行版、应用程序及其所有依赖项。它有两个主要缺点:需要 Rust 编译器为项目进行构建,并且项目必须是使用 `pyproject.toml` 标准的可安装软件包。第一个要求可能是更大的障碍;目前大多数 Python 项目都需要某种形式的 `pyproject.toml`。
另一个解决方案是自己编写的 pydeploy。它也要求相关项目可以通过 `pip install` 进行安装。除此之外,pydeploy 只需要 Python 的标准库就能生成一个包含 Python 运行时的自包含交付物。目前它的主要缺点是只适用于 Microsoft Windows,但理论上它可以在任何操作系统上运行。
未来可期
最近针对 Python 提出的所有重大更改,如新的原生 JIT 以及完全并发或多线程,都是为了增强 Python 作为动态语言的性能。任何旨在改变这种动态性的提议,本质上都意味着创建一种对其行为有不同预期的新语言。虽然有人尝试推出解决 Python 某些局限性的变体(如 Mojo),但原始的 Python 语言尽管有其局限性,仍然具有巨大的影响力。
随着这门语言的不断发展,未来有可能看到一个“完美的”、Python 原生的解决方案,来解决独立 Python 应用程序的分发问题。在此期间,现有的解决方案可能不够完美,但至少有办法应对。
关键词:Python、编程语言、软件开发