[译] Python 的打包现状(写于 2019 年)

Python 的打包现状(写于 2019 年)

在这篇文章中,我将会试着给你讲清楚 python 打包那些错综复杂的细节。我在过去的两个月中,使用天天晚上精力最好的黄金时段尽量多的收集相关信息、现在的解决方案,并搞清楚哪些是遗留的问题。html

含糊不清的 python 术语是致使混乱的第一个来源。在编程相关的语境中,“包”(package)这个词意味着一个能够安装的组件(好比能够是一个库)。可是在 python 中却不是这样,在这里,可安装组件的术语是“发行版”(distribution)。可是,除非必要(特别是在官方文档和 Python 加强提案中),不然根本没人真的去用“发行版”这个术语。顺便说一下,使用这个术语实际上是个很是糟糕的选择,由于“distribution”一词通常用来描述 Linux 的一个 brand。前端

这是一个你应该牢记于心的警告,由于 python 打包其实并不真的是关于 python 的,而是关于它的发行版。可是我仍是称之为打包。python

我不想花那么多时间去阅读。能不能给我个简短的版本?在 2019 年,我应该如何管理 python 包呢?linux

我假设你是一名想要开始研发一个 python 包程序员,步骤以下:android

  • 首先使用 Poetry 建立开发环境,并使用严格模式指定项目的直接依赖。这样就能够保证你的研发和测试环境老是能够被重复建立的。
  • 建立一个 pyproject.toml 文件,而后使用 poetry 做为后端建立源代码版和二进制发行版。
  • 下一步要指定抽象包依赖。注意应指定你能肯定的该包可运行的最低版本。这样就能够保证不会建立出无用的、会和其余包冲突的版本。

若是你真的想使用须要 setuptools 的老方法:ios

  • 建立 setup.py 文件,在文件中指定全部的抽象依赖,并在 install_requires 中指定这些依赖使用可工做的最低版本。
  • 建立 requirements.txt 文件,在其中指定严格、具体(即指定某个版本)、直接的依赖。接下来你将会须要使用这个文件生成实际的工做环境。
  • 使用命令 python -m venv 建立一个虚拟环境,激活该环境而后在该环境下使用 pip install -rrequirements.txt 命令安装依赖。用这个环境来开发。
  • 若是你须要用于测试的依赖(固然这也是很是有可能的事情),那么你须要建立一个 dev-requirements.txt 文件,并一样为其安装依赖。
  • 若是你须要将全部环境配置冻结(这是推荐的作法),执行 pip freeze >requirements-freeze.txt 而且之后也要用这个命令建立环境。

个人时间很充裕。请帮我解释清楚吧。git

首先我将阐述目前存在的问题,真的有不少问题。程序员

假设我想要用 python 建立某“项目”:它也许是一个独立程序,也许是一个库。这个项目的开发和使用须要包含如下“角色”:github

  • 开发者:负责写代码的人或者团队。
  • CI:测试这个项目的自动化过程。
  • 构建:从咱们的 git 仓库到其余人能够安装使用这个项目的自动或半自动过程。
  • 最终用户:最终使用这个项目的人或者团队。若是这个项目是一个库,那么最终用户也许是其余开发者;或者若是是一个应用,最终用户可能就是普通民众。又或者这个项目是某一种网络服务,那么最终用户就是云计算微服务。固然还有不少可能,你明白个人意思,不一一列举了。

咱们的目标就是让全部的用户或者设备对该项目满意,可是他们都有不一样的工做流和需求,而且有时候这些需求会有重叠的部分。另外,当项目发生更改、发布新版本、废除旧版本,或者几乎全部代码都要依赖其余代码来完成其任务的时候会产生问题。项目中一定存在依赖,而随着时间推移,这些依赖会发生变化,它们也许是必要的也许也不是,它们可能在很底层运行,因此咱们必须考虑在不一样操做系统甚至在一样的操做系统中它们均可能是不可移植的。这已经很是复杂了。macos

更糟糕的是,你的直接依赖也有各自的依赖集合。若是你的包直接依赖于 A 和 B,而它们两个都依赖于 C 又会怎样呢?你应该安装哪一个版本的 C?若是 A 但愿安装 C 的严格版本 2 而 B 则但愿安装 C 的严格版本 1,是否可能作到呢?

为了必定程度上整治这种混乱,人们设计出代码打包的方法,这样代码包就能够被复用、安装、版本化并给出一些描述性的元信息,例如:“已在 windows 64 位系统上打包”,或者“仅适用于 macos 系统”,或者“须要该版本或以上才可运行”。

好吧,如今我知道问题所在了。那么解决方案是什么呢?

第一步是定义一个集合了指定软件指定发布版本的可交付实体。这个可交付实体就是咱们所谓的(或者专业的 python 说法是发行版)。你能够用两种方式交付:

  • 源代码:将源代码打包为 zip 或者 tar.gz 格式的文件,而后由用户本身编译。
  • 二进制文件:由你编译代码,而后发布编译好的内容,用户能够直接使用,无需附加步骤。

两种方式均可能有用,一般状况下,两种都提供是不错的选择。固然,咱们须要可以正确完成打包的工具,尤为是为了完成以下的任务:

  • 建立可交付的包(也就是前文提到的构建
  • 将包发布在某处,这样其余人就能够获取到
  • 下载并安装包
  • 处理依赖。若是包 A 须要包 B 才能运行怎么办?若是包 A 需不须要包 B 取决于你如何使用 A?若是包 A 只在 windows 上被安装时才须要包 B?
  • 定义运行时间。如前文所述,一般状况下一个小小的软件也须要不少依赖才能运行,而且这些依赖最好和其余软件的依赖需求隔离开。无论是当你进行开发的时候仍是运行的时候,都应该这样。

能够说得更详细一些吗?我写代码以前,必需要作什么呢?

固然。在你写代码以前,一般你要完成以下步骤:

  1. 建立一个独立于系统 python 的 python 环境。这样你能够同步研发多个项目。并且若是不这样操做,A 项目的内容和 B 项目的内容可能会混在一块儿。
  2. 若是你想要规定项目的依赖,那么请牢记有两种方式能够完成:抽象方式,此时你只须要笼统地指出须要那些依赖(例如 numpy),以及具体方式,这时候你必需要规定版本号(例如 numpy 1.1.0)。至于为何会有这样的区分,后文会详细说明。若是你想要建立一个可运行的开发环境,须要具体地规定依赖。
  3. 如今你已经作完了须要作的,能够开始研发了。

我须要使用什么工具来完成这些吗?

这个很差说,由于工具很是多而且在不断变化。一个选择是你可使用 python 内建的 venv 建立独立的 python “虚拟环境”。而后使用 pip(也是 python 内建工具)来安装依赖的包。逐个输入并安装太麻烦了,因此人们一般会将具体依赖(硬编码的版本号)写入一个文件内而后通知 pip:“读取这个文件并安装文件中写明的全部包”。pip 就会照作了。这个文件就是人尽皆知的 requirements.txt,你可能已经在其余项目里见过了。

好吧,但是 pip 究竟是什么呢?

pip 是一个用来下载和安装包的程序。若是这些包也有依赖,那么 pip 也会安装这些子依赖的。

pip 是怎么作到的?

它会在远程服务 pypi 上,经过名称和版本号找到对应的包并下载、安装。若是这个包已是二进制文件,那么只须要安装它。若是是源代码,pip 就会进行编译而后再安装。可是 pip 作的还不止这些,由于这个包自己可能会有其余的依赖,因此它也会获取这些依赖,而且安装它们。

为何你说使用 requirements.txt 的方法只是一个“选择”?

由于这种方式会随着项目扩展而变得冗长并且复杂。对于不一样的平台,你须要手动管理直接依赖版本。例如,在 windows 系统你须要安装某个包,而在 linux 或其余系统你则须要另外的包,那结果是你就须要同时维护 win-requirements.txt、linux-requirements.txt 等等多个文件。

你还必须考虑到,一些依赖是你的软件运行所必需的;而其余只是用来运行测试,这些依赖只是开发者或者 CI 设备必需的,可是对于其余使用你的软件的人,其实并不须要,因此它们此时就不能做为项目的依赖了。所以,你就须要一个新的文件 dev-requirements.txt。

问题在于,requirements.txt 或许只会指定直接依赖,可是在实际应用的时候,你想要定制好建立环境所须要的全部依赖。为何要这样?比方说,若是你安装了直接依赖 A,而 A 又依赖于版本 1.1 的 C。可是有一天 C 发布了新版本 1.2,那么今后以后,当你建立环境的时候,pip 就会下载可能带有漏洞的 1.2 版本的 C。也就是突然间你的测试没法经过了,但你又不知道为何。

因此你就想在 requirements.txt 中同时指定依赖和这些依赖的子依赖。可是这样的话,你在文件中却没法区分出这两种依赖了,那么当某个依赖出现问题你想要调试它的时候,你就要找出文件中哪一个才是它的子依赖,以及…

如今你懂了。真的一团糟,你并不想去处理这样的乱局吧。

接下来你会面临的一个问题就是,pip 能够决定使用更加原始的方式来安装哪一个版本,这可能会让它本身运行到一个死胡同里,呈现给你的就是某个没法工做的环境或者是错误。记住这个例子:包 A 和 B 都依赖于 C。所以你须要一个更加复杂的过程,在这个过程里,基本上使用 pip 仅仅是为了下载已经定义好版本的包,而须要决定安装什么版本的权限则交给其余程序,这个程序要有全局的考量,并能做出更明智的版本断定。

好比说?请给我举个例子吧。

pipenv 就是一个例子。它将 venv、pip 和其余一些黑科技集合在一块儿,你只需给出直接依赖列表,它则会尽最大努力为你解决上文提到的混乱并给你交付一个可运行的环境。Poetry 是另一个例子。人们常常会讨论二者,而且因为人为和政策的缘由还会引发一些争执。可是大多数人更偏向于 Poetry。

一些公司如 Continuum 和 Enthought 都有他们本身的版本管理(即 conda 和 edm),它们一般均可以免因为平台不一样而附加的依赖版本的复杂性。在这里咱们就不展开讲了。我只想说,若是你想要用那些不少已经被编译好的依赖关系或者(这些依赖关系)依赖于编译好的库,好比说在科学计算的场景下这种需求就很常见,那么你最好用它们的系统来管理你的环境,这会为你免去很多麻烦。由于这原本就是它们拿手的。

那么 pipenv 和 Poetry 究竟哪一个更好用呢?

正如我刚才说的,人们更偏向于 Poetry。这两个我都尝试过,于我而言 Poetry 也要更好一些,它提供了更具兼容性、更优质的解决方案。

嗯好,因此至少咱们要去用 Poetry,它能够为咱们建立好环境,这样我就能够安装依赖并开始编程了。

没错。但我尚未谈论到构建。也就是,一旦你有了代码,你该如何建立发布版呢?

嗯是的,因此这就是 setup.py、setuptools 和 distutils 的用武之地了?

能够这么说,但也并不确切。最初状况下,当你想要建立一个源代码或者二进制发行版的时候,你须要使用一个名为 distutils 的标准库模块。方法是使用一个名为 setup.py 的 python 脚本,它能够魔法般的建立出你能够交付给他人的项目。这个脚本能够任意命名,但 setup.py 是标准的命名方式,其余的工具(好比普遍使用的 pip)就会只寻找以此命名的文件。而若是 pip 没有找到须要依赖的可构建版本,它将会下载源代码并构建它,简单来讲,只需运行 setup.py,而后咱们只能祈祷结果是好的了。

可是,distutils 并很差用,因此有些人找到了替代的方案,它能够作比 distutils 多得多的事。尽管挑战很大,混乱不少,发展之路漫长,可是 setuptools 要更好,每一个人均可以使用。现在 setuptools 仍是使用 setup.py 文件,给人一种其实它们并无变化、建立环境的过程也保持不变的假象。

为何说咱们只能祈祷结果是好的?

由于 pip 并不能保证它运行 setup.py 构建的包是真的能够运行的。它只是一个 python 脚本,也许会有本身的依赖,而你又没法在出现问题的时候修改它的依赖或者进行追踪。这是先有鸡仍是先有蛋的问题了。

可是在 setuptools.setup() 中有 setup_requires 选项啊

这个方法就是个坑,你基本不能使用它解决什么问题。这仍是个先有鸡仍是先有蛋的问题。PEP 518 对此进行了详细的讨论,最后结论就是它就是渣渣。别用了。

因此 setuptools 和 setup.py 究竟是不是构建发布的可选方法呢??

过去是的。但如今不必定是了,只是或许有时候还能够用。这要看你要发布的内容是什么了。如今的状况是,没人但愿 setuptools 是惟一一种能决定包如何发布的方法。问题的根源要更深刻一些,会涉及到一些技术型问题,可是若是你好奇,能够看一看 PEP 518。最重要的部分我在上文已经提到了:若是 pip 想要构建它下载的依赖,它该怎么肯定下载哪一个版本同时用来执行 setup 脚本呢?没错,它能够假设须要依靠 setuptools,但也只是假设。而你的环境中可能并不须要 setuptools,那么 pip 又该怎么作决策?在更多状况下,为何必须使用 setuptools 而不是其余的工具呢?

不少时候这决定了,任何想要写本身的包管理工具的人应该均可以这么作,所以你只须要另外一个配置工具来定义使用哪一个包系统以及你须要哪些依赖来构建项目。

使用 pyproject.toml?

正确。更确切的来讲,是一个能够在其中定义用来构建包的“后端”的子节。若是你想要使用一种不一样的构建后端,pip 就能够完成。而若是你不想这样,那么 pip 会假设你在使用工具 distutils 或者 setuptools,所以它就会退而寻找 setup.py 文件并执行,咱们祈祷它能构建成功吧。

setup.py 最终到底会不会消失?setuptools(在它以前是 distutils)用 setup.py 来描述如何生成构建。而其余工具或许会使用其余方法。或许,它们会依赖于为 pyproject.toml 添加一些内容而完成。

同时,你终于能够在 pyproject.toml 中规定用来执行构建的依赖了,这就解除了前文说得那种先有鸡仍是先有蛋的难题。

为何选择 toml 格式的文件?我都还历来没有据说过它。为何不用 JSON、INI 或者 YAML?

标准的 JSON 不容许写注释。可是人们真的很须要依赖注释传递关于项目的信息。你能够不按照规则来,但那也就不是 JSON 了。另外,JSON 其实有些反人类,写起来并让人以为不赏心悦目。

INI 则其实根本不是一种标准的写法,并且它在功能上有不少限制。

YAML 则可能会成为你项目潜在的安全威胁,它简直就像是病毒。

这样的话选择 toml 就能够理解了。可是,他们不能将 setuptools 包含在标准库中吗?

或许能够,但问题是标准库的发布周期真的超级长。distutils 的更新很是缓慢,这正激发了 setuptools 的应用和崛起。可是 setuptools 也不能保证知足全部需求。一些包或许会有一些特殊的需求。

好吧,那么我这么理解是否正确:我须要使用 Poetry 建立工做环境。使用 setup.py 和 setuptools,或者 pyproject.toml 构建包。

若是你想要使用 setuptools,你就须要 setup.py,可是你可能会遇到的问题是,其余用户也须要安装 setuptools 来构建你的包。

那么除了 setuptools 我还能使用什么其余的工具呢?

能够用 flit,或者 Poetry。

Poetry 不须要安装依赖吗?

须要,但它也能够用来构建。pipenv 就不行。

顺便说一下,若是我使用 setup.py 的话,为何我就必须写明依赖呢?我下载的 setup.py 与 pipenv、Poetry 和 requirements.txt 有什么关系呢?

这些都是运行包须要的抽象依赖,也是 pip 在决定下载和安装哪些版本的时候须要的依赖。这里你应当放宽对依赖版本的限制,由于若是你不这样…还记得我以前说过的 A 和 B 都依赖于 C 的例子吗?若是 A 要求:“我要 1.2.1 版本的 C”,可是 B 要求:“我要 1.2.2 版本的 C”,那该怎么办呢?

当要构建下载资源的源代码发行版的时候,pip 没有其余的选择。pip 并不能获取到你写在 requirements.txt 文件中的需求。它只会去运行 setup.py,而这会致使 pip 去使用 setuptools,而后再次调用 pip 来将抽象依赖解析为具体的可安装依赖。

那么 eggs、easy install、.egg-info directories、distribute、virtualenv(这个不等于 venv)、zc.buildout、bento 这些工具又怎么样呢?

忽略它们吧。它们要么是一些遗留工具或者其余工具的分支,要么是一些毫无结果的尝试。

那 Wheels 呢?

还记得我以前说的吗?pip 须要知道从 pypi 下载什么资源,从而才能下载正确的版本和操做系统。Wheel 就是一个包含了要下载资源的文件,而且有一些特殊的、规定好的字段,pip 安装依赖和子依赖的时候会使用它们来决策。

Wheels 的文件名包含了做为元数据的标签(例如 pep-0425),因此当某些资源(例如 CPython)被编译了,Wheels 能知道编译的版本、ABI 等等。文件名中的标签有一个标准层,元数据中特定的词都有特定的含义。

记住,要为二进制发行版构建 wheels。

那么 .pyz 怎么样呢?

忽略它就好,严格来说它和打包无关。但在其余某些方面它可能有用,若是你想知道更详细的信息,能够看 PEP-441。

那么 pyinstaller 怎么样呢?

Pyinstaller 是关于彻底不一样的另外一个话题了。你看,“打包”这个单词的问题是,它没有清楚的表述出它真正的含义。到目前位置,咱们讨论了关于:

  1. 建立一个能够开发库的环境
  2. 把你建立的项目构建为其余人也可使用的格式

可是这些一般是应用于库的。而关于发行应用,状况就不一样了。当你打包库的时候,你知道它将会是一个更大的项目体的一部分。而当你打包一个应用,那么这个应用就是那个更大的项目体

另外,若是你想为人们提供应用,那就应指定应用的平台。例如,你想要提供一个带图标的可执行文件,可是在 Windows、macOS 和 Linux 平台上,它们应当是有所不一样的。

当你想要建立一个独立可执行应用的时候,PyInstaller 是可使用的工具。它可以为你在用户桌面上建立出最终完成的应用。打包是关于管理你须要用来建立应用的依赖、库和工具的网络,而建立这个应用你可能会、也可能不会使用 pyinstaller。

注意无论怎样,使用这个方法的前提是,假设你的应用是比较简单而且是自包含的。若是应用在安装的时候须要作更复杂的事情,好比建立 Windows 登陆密码,那你就须要一个更合适的、更成熟的安装器,好比 NSIS。我不知道在 Python 世界中是否有像 NSIS 这样的东西。但不管如何,NSIS 都不知道你部署了什么。你固然可使用 pyinstaller 建立可执行应用,而后使用 NSIS 来部署它,而且还能够完成例如注册表修改或者文件系统修改这样的附加需求,让应用能够运做。

好的,可是我如何安装那些我已经有资源包的项目呢?使用 python setup.py?

不对。用 pip install .,由于这个命令能保证你以后还能够卸载应用,并且它整体上更好一些。pip 这时候会检查 pyproject.toml 并在后台运行构建。而若是 pip 没有找到 pyproject.toml 文件,它就只好退回到老方法,运行 setup.py 来尝试构建。

我很喜欢这篇文章,可是我仍是有些问题没有搞清楚

你能够本身开一个 issue。若是我知道答案,我将会立刻为你解答。若是我不知道,我会作一下研究并尽快给你回复。个人目标是这篇文章能让人们最终理解 python 打包。

有没有参考连接能让我更深刻的学习呢?

固然,请见:

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索