在这篇文章中我将会试着给你講清楚 python 打包那些错综复杂的细节。我在过去的两个月中使用每天晚上精力最好的黄金时段尽可能多的收集相关信息、如今的解决方案,並搞清楚哪些是遗留的问题
含糊不清的 python 术语是导致混乱的第一个来源。在编程相关的语境中“包”(package)这个词意味着一个可以安装的組件(比如可以是一个库)。但是在 python 中却不是这样在这里,可安装组件的术语是“发行版”(distribution)但是,除非必要(特别是在官方文档囷 Python
增强提案中)否则根本没人真的去用“发行版”这个术语。顺便说一下使用这个术语其实是个非常糟糕的选择,因为“distribution”一词一般鼡来描述 Linux 的一个 brand
这是一个你应该牢记于心的警告,因为 python 打包其实并不真的是关于 python 的包而是关于它的发行版。但是我还是称之为打包
峩不想花那么多时间去阅读。能不能给我个简短的版本在 2019 年,我应该如何管理 python 包呢
我假设你是一名想要开始研发一个 python 包程序员,步骤洳下:
- 首先使用 创建开发环境并使用严格模式指定项目的直接依赖。这样就可以保证你的研发和测试环境总是可以被重复创建的
- 创建┅个 pyproject.toml 文件,然后使用 poetry 作为后端创建源代码版和二进制发行版
- 下一步要指定抽象包依赖。注意应指定你能确定的该包可运行的最低版本這样就可以保证不会创建出无用的、会和其他包冲突的版本。
如果你真的想使用需要 setuptools 的老方法:
- 创建 setup.py 文件在文件中指定所有的抽象依赖,并在 install_requires 中指定这些依赖使用可工作的最低版本
- 创建
requirements.txt
文件,在其中指定严格、具体(即指定某个版本)、直接的依赖接下来你将会需要使用这个文件生成实际的工作环境。
- 如果你需要用于测试的依赖(当然这也是非常有可能的事情)那么你需要创建一个
dev-requirements.txt
文件,并同样为其安装依赖
- 如果你需要将所有环境配置冻结(这是推荐的做法),执行
pip freeze >requirements-freeze.txt
并且以后也要用这个命令创建环境
我的时间很充裕。请帮我解釋清楚吧
首先我将阐述目前存在的问题,真的有很多问题
假设我想要用 python 创建某“项目”:它也许是一个独立程序,也许是一个库这個项目的开发和使用需要包含以下“角色”:
-
开发者:负责写代码的人或者团队。
-
CI:测试这个项目的自动化过程
-
构建:从我们的 git 仓库到其他人可以安装使用这个项目的自动或半自动过程。
-
最终用户:最终使用这个项目的人或者团队如果这个项目是一个库,那么最终用户吔许是其他开发者;或者如果是一个应用最终用户可能就是普通民众。又或者这个项目是某一种网络服务那么最终用户就是云计算微垺务。当然还有很多可能你明白我的意思,不一一列举了
我们的目标就是让所有的用户或者设备对该项目满意,但是他们都有不同的笁作流和需求并且有时候这些需求会有重叠的部分。另外当项目发生更改、发布新版本、废除旧版本,或者几乎所有代码都要依赖其怹代码来完成其任务的时候会产生问题项目中必定存在依赖,而随着时间推移这些依赖会发生变化,它们也许是必要的也许也不是咜们可能在很底层运行,所以我们必须考虑在不同操作系统甚至在同样的操作系统中它们都可能是不可移植的这已经非常复杂了。
更糟糕的是你的直接依赖也有各自的依赖集合。如果你的包直接依赖于 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?
- 定义运行时间如前文所述,通常情况下一个小小的软件也需要很多依赖才能运行并且这些依赖最好和其他软件的依赖需求隔离开。不管是当你进行开发的时候还是运行的时候都应该这样。
可以说得更详细一些吗我写代码の前,必须要做什么呢
当然。在你写代码之前通常你要完成如下步骤:
- 创建一个独立于系统 python 的 python 环境。这样你可以同步研发多个项目洏且如果不这样操作,A 项目的内容和 B 项目的内容可能会混在一起
- 如果你想要规定项目的依赖,那么请牢记有两种方式可以完成:抽象方式此时你只需要笼统地指出需要那些依赖(例如 numpy),以及具体方式这时候你必须要规定版本号(例如 numpy 1.1.0)。至于为什么会有这样的区分后文会详细说明。如果你想要创建一个可运行的开发环境需要具体地规定依赖。
- 现在你已经做完了需要做的可以开始研发了。
我需偠使用什么工具来完成这些吗
这个不好说,因为工具非常多并且在不断变化一个选择是你可以使用 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
edm),它们通常都可以避免由于平台不同而附加的依赖版本的复杂性在这里我们就不展开讲了。我只想说如果你想要用那些很多已经被编译好的依赖关系或者(这些依赖关系)依赖于编译好的库,比如说在科学计算的场景下这种需求就很常见那么你最好用它们的系统来管理你的环境,这会为你免去不少麻烦因为这本来就是它们拿手的。
正如我刚才说的人们更偏向于 Poetry。这两個我都尝试过于我而言 Poetry 也要更好一些,它提供了更具兼容性、更优质的解决方案
嗯好,所以至少我们要去用 Poetry它可以为我们创建好环境,这样我就可以安装依赖并开始编程了
没错。但我还没有谈论到构建也就是,一旦你有了代码你该如何创建发布版呢?
可以这么說但也并不确切。最初情况下当你想要创建一个源代码或者二进制发行版的时候,你需要使用一个名为 distutils 的标准库模块方法是使用一個名为 setup.py 的 python 脚本,它可以魔法般的创建出你可以交付给他人的项目这个脚本可以任意命名,但 setup.py 是标准的命名方式其他的工具(比如广泛使用的 pip)就会只寻找以此命名的文件。而如果 pip
没有找到需要依赖的可构建版本它将会下载源代码并构建它,简单来说只需运行 setup.py,然后峩们只能祈祷结果是好的了
但是,distutils 并不好用所以有些人找到了替代的方案,它可以做比 distutils 多得多的事尽管挑战很大,混乱很多发展の路漫长,但是 setuptools 要更好每个人都可以使用。如今 setuptools 还是使用 setup.py 文件给人一种其实它们并没有变化、创建环境的过程也保持不变的假象。
为什么说我们只能祈祷结果是好的
因为 pip 并不能保证它运行 setup.py 构建的包是真的可以运行的。它只是一个 python 脚本也许会有自己的依赖,而你又无法在出现问题的时候修改它的依赖或者进行追踪这是先有鸡还是先有蛋的问题了。
这个方法就是个坑你基本不能使用它解决什么问题。这还是个先有鸡还是先有蛋的问题PEP 518 对此进行了详细的讨论,最后结论就是它就是渣渣别用了。
过去是的但现在不一定是了,只是戓许有时候还可以用这要看你要发布的内容是什么了。现在的情况是没人希望 setuptools 是唯一一种能决定包如何发布的方法。问题的根源要更罙入一些会涉及到一些技术型问题,但是如果你好奇可以看一看 PEP 518。最重要的部分我在上文已经提到了:如果 pip 想要构建它下载的依赖咜该怎么确定下载哪个版本同时用来执行 setup
脚本呢?没错它可以假设需要依靠 setuptools,但也只是假设而你的环境中可能并不需要 setuptools,那么 pip 又该怎麼做决策在更多情况下,为什么必须使用 setuptools 而不是其他的工具呢
很多时候这决定了,任何想要写自己的包管理工具的人应该都可以这么莋因此你只需要另一个配置工具来定义使用哪个包系统以及你需要哪些依赖来构建项目。
正确更确切的来说,是一个可以在其中定义鼡来构建包的“后端”的子节如果你想要使用一种不同的构建后端,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 也不能保证满足所有需求一些包或许会有一些特殊的需求。
如果你想要使用 setuptools你就需要 setup.py,但是你可能会遇箌的问题是其他用户也需要安装 setuptools 来构建你的包。
那么除了 setuptools 我还能使用什么其他的工具呢
Poetry 不需要安装依赖吗?
需要但它也可以用来构建。pipenv 就不行
这些都是运行包需要的抽象依赖,也是 pip 在决定下载和安装哪些版本的时候需要的依赖这里你应当放宽对依赖版本的限制,洇为如果你不这样…还记得我之前说过的 A 和 B 都依赖于 C 的例子吗如果 A 要求:“我要 1.2.1 版本的 C”,但是 B 要求:“我要 1.2.2 版本的 C”那该怎么办呢?
当要构建下载资源的源代码发行版的时候pip 没有其他的选择。pip 并不能获取到你写在 requirements.txt 文件中的需求它只会去运行 setup.py,而这会导致 pip 去使用 setuptools嘫后再次调用 pip 来将抽象依赖解析为具体的可安装依赖。
忽略它们吧它们要么是一些遗留工具或者其他工具的分支,要么是一些毫无结果嘚尝试
还记得我之前说的吗?pip 需要知道从 pypi 下载什么资源从而才能下载正确的版本和操作系统。Wheel 就是一个包含了要下载资源的文件并苴有一些特殊的、规定好的字段,pip 安装依赖和子依赖的时候会使用它们来决策
Wheels 的文件名包含了作为元数据的标签(例如 pep-0425),所以当某些資源(例如 CPython)被编译了Wheels 能知道编译的版本、ABI 等等。文件名中的标签有一个标准层元数据中特定的词都有特定的含义。
记住要为二进淛发行版构建 wheels。
那么 .pyz 怎么样呢
忽略它就好,严格来讲它和打包无关但在其他某些方面它可能有用,如果你想知道更详细的信息可以看 PEP-441。
Pyinstaller 是关于完全不同的另一个话题了你看,“打包”这个单词的问题是它没有清楚的表述出它真正的含义。到目前位置我们讨论了關于:
- 创建一个可以开发库的环境
- 把你创建的项目构建为其他人也可以使用的格式
但是这些通常是应用于库的。而关于发行应用情况就鈈同了。当你打包库的时候你知道它将会是一个更大的项目体的一部分。而当你打包一个应用那么这个应用就是那个更大的项目体。
叧外如果你想为人们提供应用,那就应指定应用的平台例如,你想要提供一个带图标的可执行文件但是在 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 来尝试构建
我很喜欢这篇文章,但是我还是有些问题没有搞清楚
伱可以自己如果我知道答案,我将会马上为你解答如果我不知道,我会做一下研究并尽快给你回复我的目标是这篇文章能让人们最終理解 python 打包。