如何改进Python打包,或者为什么14个工具至少有12个是多余的
Python 领域中有一个让许多开发者头疼的问题。多年来,这个领域涌现了各种不同的解决方案,伴随着诸多争议、争论和解决尝试。许多人抱怨打包生态系统和工具让他们的生活更加困难。许多初学者对虚拟环境感到困惑。但情况非得如此吗?当前解决打包问题的方案是否有效?而主导大多数打包工具和标准的组织本身是否就是问题的一部分?
让我们一起探索 Python 及其他领域的打包技术。我们将从经典打包栈(涉及 setuptools 及其相关工具)、科学计算栈(使用 conda)以及一些现代/替代工具(如 Pipenv、Poetry、Hatch 或 PDM)开始。我们还将探讨其他领域(如Node.js和.NET)中常见的包装和依赖管理工作流程示例。我们还将展望一个可能的未来(采用PDM实现的无venv工作流程),并看看PyPA是否认同八千名用户的愿景与洞察。
丰富的工具集
Python 中有许多与打包相关的工具。这些工具的作者、传承和观点各不相同,尽管大多数现在都统一在 Python 打包权威机构(PyPA)的 umbrella 下。让我们来看看它们。

经典工具栈
经典的 Python 包装堆栈由多个半相关的工具组成。Setuptools 可能是该组中最古老的工具,它本身基于 distutils
(标准库的一部分,尽管它将在 Python 3.12 中被移除),负责安装单个包。它以前使用 setup.py
文件来完成其任务,这需要任意代码执行。随后,它添加了对非可执行元数据规范格式的支持:setup.cfg
,以及 pyproject.toml
(部分仍处于测试阶段)。然而,如今你不应直接使用 setup.py
文件,而应使用 pip。Pip 安装包通常来自 PyPI,但也可支持其他来源(如 Git 仓库或本地文件系统)。那么 pip 安装包的位置在哪里?默认情况下,pip 会全局安装并覆盖系统范围,这意味着可能导致通过 pip 安装的包与 apt(或其他系统包管理器)安装的包之间发生冲突。即使使用用户级安装(这是 pip 如今更倾向于尝试的方式),仍可能出现冲突,而且还可能出现这样的冲突:包 A 要求 X 版本 1.0.0,而包 B 期望 X 版本 2.0.0——但 A 和 B 完全无关,可以各自使用其首选的 X 版本。此时引入 venv
,它是 virtualenv
的标准库继承者,可创建一个轻量级虚拟环境供包使用。该虚拟环境可与系统包及不同环境隔离,但仍与系统 Python 存在某种关联(若系统 Python 消失,虚拟环境将无法正常工作)。
在典型的打包工作流中还会用到几个额外工具。wheel
包通过添加生成轮文件(无需运行 setup.py
即可直接安装)的功能,增强了 Setuptools。轮文件可以是纯 Python 格式(可在任何环境中安装),也可以包含针对特定操作系统和 Python 版本预编译的扩展模块(用 C 语言编写的内容)。甚至存在一个标准,允许为所有常见 Linux 发行版构建并分发单一轮文件。wheel
包应作为实现细节存在于 Setuptools 和/或 pip 内部,但用户若想在系统上生成 wheels,就需要了解它,因为 venv
生成的虚拟环境中并未安装 wheel
。普通用户若不维护自己的包,有时会被告知 pip 正在使用过时功能,因为 wheel
未安装,这会带来不佳的用户体验。包作者还需要 twine
,其唯一任务是将使用其他工具创建的源代码分发包或轮子上传到 PyPI(关于该工具的其他内容无需赘述)。
…以及一些扩展
多年来,基于经典堆栈的工具层出不穷。例如,pip-tools
可简化依赖项管理。虽然 pip freeze
可以生成一个包含环境中所有已安装包的文件,但无法指定所需依赖项并生成包含特定版本和传递依赖项的锁定文件(无需安装并冻结所有包),也无法在执行 pip freeze
时轻松跳过开发依赖项(如 IPython),且没有通过 pip 批量更新所有依赖项的工作流。pip-tools
添加了两个工具:pip-compile
,它接受包含您关注的包的 requirements.in
文件,并生成一个包含这些包的固定版本及其所有传递依赖关系的 requirements.txt
文件;以及 pip-sync
,它可以安装 requirements.txt
并移除其中未列出的内容。
另一个可能有用的工具是 virtualenvwrapper
,它可以帮助你在中央位置管理(创建和激活)虚拟环境。它有一些额外功能(如在每次创建虚拟环境时执行自定义操作的钩子),尽管对于基本使用,你可以用一个单行的 shell 函数来替代它。
与经典工具集并行工作的另一款工具是 pipx
,它用于创建和管理用于 Python 应用程序的虚拟环境。你只需告诉它执行 pipx install Nikola
,它就会在某个位置创建一个虚拟环境,将 Nikola 安装到其中,并在 ~/.local/bin
目录下放置一个用于启动 Nikola 的脚本。虽然你可以通过 venv 和一些符号链接手动完成这些操作,但 pipx 可以帮你搞定这一切,而且你无需记住虚拟环境的位置。
科学计算堆栈与conda
科学计算领域的Python社区多年来一直拥有自己的工具。conda工具可以管理环境和包。它不使用PyPI和轮子,而是使用来自conda通道的包(这些包是预先构建的,并且期望使用Anaconda分发的Python)。在过去,当没有轮子的时候,这是在Windows上安装东西最简单的方法;现在有了PyPI上的二进制轮子,这个问题不再那么严重——但Anaconda堆栈在科学界仍然很受欢迎。Conda 包可通过独立但与 conda
密切相关的 conda-build
工具进行构建。Conda 包与 pip
完全不兼容,也不遵循其他工具使用的打包标准。这好吗?不好,因为这使得整合两个世界变得更加困难,但也有好处,因为许多适用于科学包(及其 C/C++ 扩展模块、高性能数值库和其他内容)的问题并不适用于 Python 的其他用途,因此拥有一个独立的工具可以让专注于其他用途的人简化工作流程。
新工具
几年前,新的打包工具开始出现。过去曾推出过许多“新潮工具”,例如 setuptools 扩展了 distutils,随后 distribute 从 setuptools 分支出来,后来 distribute 又与 setuptools 合并……
最早的“新工具”是 Pipenv。Pipenv 的营销策略非常糟糕且具有误导性,它将 pip 和 venv 结合在一起,即 Pipenv 会创建一个 venv 并在此安装包(来自 Pipfile
或 Pipfile.lock
)。Pipenv 可以将 venv 放在项目文件夹中,或将其隐藏在项目文件夹的某个位置(后者是默认设置)。然而,Pipenv 并不处理与打包代码相关的任何包,因此它仅适用于开发不可安装的应用程序(例如 Django 站点)。如果你是库开发者,你仍然需要 setuptools。
第二个新工具是 Poetry。它以与 Pipenv 类似的方式管理环境和依赖项,但它还可以使用您的代码构建 .whl
文件,并可以将轮子和源分发上传到 PyPI。这意味着它基本上拥有其他工具的所有功能,只是您只需要一个工具。然而,Poetry 是有自己立场的,而它的立场有时与包装领域的其他部分不兼容。Poetry 使用 pyproject.toml
标准,但它并未遵循 PEP 621 中关于如何在 pyproject.toml
文件中表示元数据的标准规范,而是使用了自定义的 [tool.poetry]
表。这部分是因为 Poetry 发布于 PEP 621 之前,但该 PEP 已于两年前被采纳——最大的兼容性问题在于 Poetry 借鉴 Node.js 的 ~
和 ^
依赖版本标记,这些标记与 PEP 508(依赖规范标准)不兼容。Poetry 可打包 C 扩展模块,但为此使用了 setuptools 的基础设施(并需要自定义 build.py
脚本)。
另一个类似工具是 Hatch。该工具也可管理环境(支持每个项目多个环境,但不允许将环境放在项目目录中),并可管理包(但不支持锁定文件)。Hatch 还可用于打包项目(使用符合 PEP 621 的 pyproject.toml
文件)并上传至 PyPI。它不支持 C 扩展模块。
试图以更简洁方式重新设计 Setuptools 的工具是 Flit。它可通过 pyproject.toml
文件构建并安装包,并支持上传至 PyPI。但它不支持 C 扩展模块,且要求用户自行管理环境。
还有一个有趣(尽管不流行或不为人知)的工具。这个工具是 PDM。它可以管理 venvs(但默认使用更合理的 .venv
位置),管理依赖项,并使用符合标准的 pyproject.toml
。它还有一个名为 PEP 582 支持的有趣小功能,我们稍后会讨论。
工具泛滥与 Python 包管理权威机构
前文提到了 14(十四个!)种不同的工具。正如我们即将发现的,这至少多了 12 个。让我们尝试比较它们。
首先,让我们定义九个我们期望打包工具具备的功能:
- 管理环境
- 安装包
- 打包/开发应用程序
- 打包库
- 打包C扩展模块
- 以可编辑模式安装
- 锁定依赖项
- 支持pyproject.toml文件
- 上传至 PyPI
工具 | 维护者 | 适用场景 | 支持的功能数量 | 部分支持的功能数量 | 不支持的功能数量 |
---|---|---|---|---|---|
setuptools | PyPA | 使软件包可安装 | 4 | 2(pyproject.toml 部分处于测试阶段,仅支持基于 setuptools 的 sdists 安装) | 3 |
pip | PyPA | 安装包 | 2 | 1(仅手动锁定依赖项) | 6 |
venv | PyPA | 创建虚拟环境 | 1(创建环境) | 0 | 8 |
wheel | PyPA | 在setuptools中构建wheel包 | 0 | 1(在setuptools中构建wheel包) | 8 |
Twine | PyPA | 上传至 PyPI | 1(上传至 PyPI) | 0 | 8 |
pip-tools | Jazzband | 管理要求文件 | 2(锁定依赖项、安装包) | 0 | 7 |
virtualenvwrapper | Doug Hellmann | 管理虚拟环境 | 1 (管理环境) | 0 | 8 |
pipx | PyPA | 安装 Python 命令行工具 | 2 (安装包、可编辑安装) | 1 (管理环境) | 6 |
conda | Anaconda, Inc. | 环境和依赖项管理 | 3(环境管理、安装) | 4(手动锁定、打包需要 conda-build) | 2(pyproject.toml 和 PyPI) |
Pipenv | PyPA | 管理应用程序的依赖项 | 3(管理环境、安装和锁定) | 1(开发应用程序) | 5 |
Poetry | Sébastien Eustace 等 | 打包和管理依赖项 | 7 | 2(pyproject.toml、C 扩展) | 0 |
Flit | PyPA | 包装纯 Python 项目 | 5 | 1(仅安装 Flit 包) | 3 |
Hatch | PyPA | 包装和管理依赖项 | 7 | 0 | 2(C 扩展,锁定依赖项) |
PDM | Frost Ming | 包装和管理依赖项 | 8 | 0 | 1(C 扩展) |
展开表格以查看每个功能的详细支持信息
工具 | F1 (环境) | F2 (安装) | F3 (应用程序) | F4 (库) | F5 (扩展) | F6 (可编辑) | F7 (锁定) | F8 (pyproject.toml) | F9 (上传) |
---|---|---|---|---|---|---|---|---|---|
setuptools | 否 | 仅在创建包时使用,不建议直接使用 | 是 | 是 | 是 | 是 | 否 | 测试版 | 否(可构建 sdist) |
pip | 否 | 是 | 否 | 否 | 否 | 是 | 手动 | 不适用 | 否 |
venv | 仅创建环境 | 否 | 否 | 否 | 否 | 否 | 否 | 否 | 否 |
wheel | 否 | 否 | 否 | 否 | 否 | 否 | 否 | 否 | 否(可构建 wheel 包) |
Twine | 否 | 否 | 否 | 否 | 否 | 否 | 否 | 否 | 是 |
pip-tools | 否 | 是 | 否 | 否 | 否 | 否 | 是 | 否 | 否 |
virtualenvwrapper | 是 | 否 | 否 | 否 | 否 | 否 | 否 | 否 | 否 |
pipx | 某种程度上 | 是 | 否 | 否 | 否 | 是 | 否 | 否 | 否 |
conda | 是 | 是(通过 conda 频道) | 开发(conda-build 是独立工具) | 通过 conda-build | 通过 conda-build | 是 | 手动 | 否 | 否 |
Pipenv | 是 | 是 | 仅开发 | 否 | 否 | 否 | 是 | 否 | 否 |
Poetry | 是 | 是 | 是 | 是 | 部分支持(自定义 build.py 脚本) | 是 | 是 | 是,但使用自定义字段 | 是 |
Flit | 否 | 仅在创建包时 | 是 | 是 | 否 | 是 | 否 | 是 | 是 |
Hatch | 是 | 是 | 是 | 是 | 否 | 是 | 否 | 是 | 是 |
PDM | 是 | 是 | 是 | 是 | 否 | 是 | 是 | 是 | 是 |
您应特别关注表格中的“维护者”列。其中绝大多数由PyPA(Python打包权威机构)维护。更值得注意的是,拥有最多“是”值的两个工具(Poetry和PDM)并非由PyPA维护,而是由与PyPA完全独立且未参与工作组的其他人员维护。那么,如果工作组无法产出一个功能齐全的工具,它是否算成功?如果工作组有多个职责重叠的项目,它是否成功?工作组是否应将精力集中在如PEP 517等标准上,该标准为打包工具提供了一个通用API,同时又鼓励创建更多不兼容且相互竞争的工具?
最重要的是:初学者应该使用哪个工具?PyPA有一些指南和教程,其中一个是使用pip + venv,另一个是使用pipenv (为什么还要这样做?),以及另一个教程,让你可以在Hatchling(Hatch的构建后端)、setuptools、 Flit 和 PDM 进行选择,但并未解释它们之间的差异——且未使用任何环境工具,也未使用 Hatch/PDM 的构建和 PyPI 上传功能(而是选择使用 python -m build
和 twine
)。虚拟环境的概念对初学者来说可能非常令人困惑,而如果每个人对虚拟环境的看法都不一致,管理虚拟环境就会变得困难。
值得注意的是,PEP 20《Python 哲学》中明确指出:
应该有一种——而且最好只有一种——显而易见的方式来实现它。
Python打包显然没有遵循这一原则[1]。有14种方法,没有一种是显而易见的或唯一好的方法。总之,这简直是一团糟。为什么Python不能选择一种工具?竞争对手是怎么做的?我们稍后会探讨这个问题。但首先,让我们来谈谈这个显而易见的问题:Python虚拟环境。
Python真的需要虚拟环境吗?
Python 依赖虚拟环境来实现项目之间的隔离。虚拟环境(又称 virtualenvs 或 venvs)是包含指向系统安装的 Python 的符号链接以及自身的一组 site-packages 的文件夹。它们存在一些问题:
如何从虚拟环境中使用 Python?
有两种方法可以实现这一点。第一种是通过运行环境的 bin 目录中安装的 activate 脚本激活虚拟环境。另一种方法是直接从 venv 中运行 Python 可执行文件(或 bin 目录中的任何其他脚本)。[2]
直接激活虚拟环境对开发者来说更方便,但它也存在一些问题。有时,激活会失败,因为 shell 会缓存 $PATH
中项目的路径。此外,初学者通常被教导要使用 activate
并运行 python
,这可能导致他们混淆,并在脚本或 cron 任务中尝试使用 activate(但在这些环境中,你不应激活虚拟环境,而应直接使用 Python 可执行文件)。虚拟环境的激活是一个需要特别注意的状态,如果你忘记了它,或者它出现故障,你可能会弄乱你的用户级(甚至更糟,系统级)Python 包。
系统 Python 和虚拟环境之间有何关联?
虚拟环境与创建它所使用的 Python 版本(系统/全局/pyenv 安装的 Python)紧密相关。这在磁盘空间方面是有好处的(干净的虚拟环境占用空间不大),但这也使得环境更加脆弱。如果用于创建环境的 Python 被移除,虚拟环境将无法正常工作。如果你完全自行管理 Python,这种情况可能不会发生,但如果你依赖系统 Python,操作系统升级包可能会将 Python 3.10 替换为 Python 3.11。部分发行版(如 Ubuntu)仅会在新发行版发布时进行此类重大升级(因此可提前规划),部分发行版(如 Arch)采用滚动发布模式,常规系统升级可能包含新版本 Python,而部分工具(如 Homebrew)则通过使用包含补丁版本(如 3.x.y)的路径进一步加剧问题,导致虚拟环境更频繁地出现故障。
如何管理虚拟环境?
原始的 virtualenv 工具及其简化的标准库重写版本 venv,允许您将虚拟环境放置在文件系统的任何位置,只要您对该位置具有写入权限。这导致人们和工具发明了自己的标准。Virtualenvwrapper 将环境存储在中央位置,并且不关心其内容。Pipenv 和 poetry 允许您选择(中央位置或项目中的 .venv 目录),且环境与项目绑定(如果您在项目目录中,将使用项目特定的环境)。Hatch 将环境存储在中央位置,并允许每个项目拥有多个环境(但没有在项目之间共享环境的选项)。
Brett Cannon 最近进行了一项调查,结果显示社区在工作流程上存在分歧:部分人使用中央位置,部分人将环境放在项目目录中,部分人拥有多个不同 Python 版本的环境,部分人会在项目间复用虚拟环境……每个人都有不同的需求和观点。例如,我在使用Nikola时采用中央目录(~/virtualenvs)并复用环境(开发环境与4个Nikola站点共享同一环境)。但另一方面,在部署网络应用时,虚拟环境会存放在项目文件夹中,因为该虚拟环境需要被不同用户运行的进程使用(例如我本人、root用户或网络服务器的服务账户,这些账户可能禁用了交互式登录,或其主目录被设置为临时目录)。
因此:Python 是否需要虚拟环境?或许参考其他语言如何处理此问题能帮助我们为 Python 找到解决方案?
其他人是如何做的
我们将探讨两个生态系统。首先是JavaScript/Node.js(使用npm),然后我们将查看C#/.NET(使用dotnet CLI/MSBuild) 生态系统进行比较。我们将演示一个示例流程:创建项目、安装依赖项并运行程序。如果您熟悉这些生态系统并希望跳过示例,请继续阅读 Node 为何优于 Python? 如何改进 Python 打包? 和 这些生态系统的工具完美吗? 继续阅读。否则,请继续阅读。
JavaScript/Node.js(使用npm)
在 Node 生态系统中,处理包的工具有两种,即 npm 和 Yarn。npm 命令行工具随 Node 一起提供,因此我们将重点介绍它。
让我们创建一个项目:
$ mkdir mynpmproject
$ cd mynpmproject
$ npm init
…answer a few questions…
$ ls
package.json
我们有一个 package.json 文件,其中包含项目的一些元数据(名称、版本、描述、许可证)。让我们安装一个依赖项:
$ npm install --save is-even
added 5 packages, and audited 6 packages in 2s
found 0 vulnerabilities
is-even
包的存在本身就值得商榷;它包含四个依赖项更是令人质疑,而它依赖于 is-odd
则更糟糕。但这篇文章不是关于 is-even
或 Node 生态系统倾向于为一切使用小型包的趋势(但我之前写过一篇关于这个主题的文章 此处)。让我们看看文件系统中有什么:
$ ls
node_modules/ package.json package-lock.json
$ ls node_modules
is-buffer/ is-even/ is-number/ is-odd/ kind-of/
我们还来看看 package.json
文件:
{
"name": "mynpmproject",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"is-even": "^1.0.0"
}
}
我们的 package.json
文件现在列出了依赖项,并且我们还生成了一个锁定文件(package-lock.json
),该文件记录了本次安装所使用的所有依赖项版本。如果将此文件保存在仓库中,未来任何 npm install
操作都将使用该文件中列出的依赖项版本,确保一切与最初安装时完全一致(除非其中某个包被从注册表中移除)。
让我们尝试使用该模块编写一个简单的程序并运行它:
$ cat index.js
var isEven = require('is-even');
console.log(isEven(0));
$ node index.js
true
让我们尝试移除 is-odd
以演示该包设计有多糟糕:
$ rm -rf node_modules/is-odd
$ node index.js
node:internal/modules/cjs/loader:998
throw err;
^
Error: Cannot find module 'is-odd'
Require stack:
- /tmp/mynpmproject/node_modules/is-even/index.js
- /tmp/mynpmproject/index.js
at Module._resolveFilename (node:internal/modules/cjs/loader:995:15)
at Module._load (node:internal/modules/cjs/loader:841:27)
at Module.require (node:internal/modules/cjs/loader:1061:19)
at require (node:internal/modules/cjs/helpers:103:18)
at Object.<anonymous> (/tmp/mynpmproject/node_modules/is-even/index.js:10:13)
at Module._compile (node:internal/modules/cjs/loader:1159:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
at Module.load (node:internal/modules/cjs/loader:1037:32)
at Module._load (node:internal/modules/cjs/loader:878:12)
at Module.require (node:internal/modules/cjs/loader:1061:19) {
code: 'MODULE_NOT_FOUND',
requireStack: [
'/tmp/mynpmproject/node_modules/is-even/index.js',
'/tmp/mynpmproject/index.js'
]
}
Node.js v18.12.1
Node.js 为何优于 Python?
撇开设计糟糕的包不谈,我们可以看出与 Python 的一个重要区别在于,这里没有 虚拟环境,所有包都位于项目目录中。如果我们通过运行 npm install
来修复 node_modules
目录,就可以看到我可以从文件系统的其他位置运行脚本:
$ pwd
/tmp/mynpmproject
$ npm install
added 1 package, and audited 6 packages in 436ms
found 0 vulnerabilities
$ node /tmp/mynpmproject/index.js
true
$ cd ~
$ node /tmp/mynpmproject/index.js
true
如果你尝试用 Python 工具做同样的事情……
- 如果你使用手动管理的虚拟环境,你需要记得激活它,或者使用相应的 Python 版本。
- 如果你使用更高级的工具,它可能与当前工作目录绑定,并可能要求你切换到该目录,或者传递一个指向该目录的参数。
我还可以以 root
用户身份运行代码,也可以以无特权的 nginx
用户身份运行,无需任何特殊准备(如告诉 pipenv/poetry 将其虚拟环境放在项目目录中,或以其他用户身份运行它们):
$ su -
# node /tmp/mynpmproject/index.js
true
# sudo -u nginx node /tmp/mynpmproject/index.js
true
如果你尝试使用 Python 工具这样做……
- 如果你使用的是手动管理的虚拟环境,你可以使用其 Python 作为其他用户(假设它具有正确的权限)。
- 如果你的工具将虚拟环境放在项目目录中,这也行得通。
- 如果你的工具将虚拟环境放在用户主目录中的某个奇怪位置,其他用户将获得各自的虚拟环境。Fedora 系统中的
uwsgi
用户使用/run/uwsgi
作为其主目录,而/run
是临时目录(tmpfs),因此重启系统会迫使你重新安装相关组件。
我们甚至可以尝试更改项目名称:
$ cd /tmp
$ mv mynpmproject mynodeproject
$ node /tmp/mynodeproject/index.js
true
如果你尝试使用 Python 工具进行此操作…
- 如果你使用手动管理的 venv,且它位于中央目录中,一切正常。
- 如果你的工具将 venv 放置在项目目录中,那么 venv 将会损坏,你需要重新创建它(希望你有一个最近的
requirements.txt
!) - 如果你的工具将 venv 放置在你家目录中的某个奇怪位置,它可能会认为这是一个不同的项目,这意味着它会重新创建它,而你将在文件系统上的某个地方有一个未使用的虚拟环境。
其他打包主题
某些包可能包含可执行脚本(通过 bin
属性)。这些脚本可通过三种方式运行:
- 使用
npm install -g
全局安装,这会将脚本放置在全局位置(通常在$PATH
中,例如/usr/local/bin
)。 - 使用
npm install
本地安装,并通过npx
工具或手动在node_packages/.bin
中运行脚本。 - 完全不安装,但使用
npx
工具执行,该工具会将其安装到缓存中并运行。
此外,如果我们想发布我们的项目,只需运行 npm publish
(在使用 npm login
登录后)。
C#/.NET(使用 dotnet CLI/MSBuild)
在现代 .NET 中,唯一真正的工具是 dotnet CLI,它使用 MSBuild 完成大部分繁重的工作。(在经典的 .NET Framework 中,这些任务由 MSBuild 和 NuGet.exe 分担,但我们专注于现代工作流程。)
让我们创建一个项目:
$ mkdir mydotnetproject
$ cd mydotnetproject
$ dotnet new console
The template "Console App" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on /tmp/mydotnetproject/mydotnetproject.csproj...
Determining projects to restore...
Restored /tmp/mydotnetproject/mydotnetproject.csproj (in 92 ms).
Restore succeeded.
$ ls
mydotnetproject.csproj obj/ Program.cs
我们得到三样东西:一个定义项目部分属性的 mydotnetproject.csproj
文件;一个“Hello World”程序 Program.cs
,以及包含一些无需关注的文件的 obj/
目录。
让我们尝试添加一个依赖项。虽然这是一个毫无意义的示例,但比 JavaScript 的示例稍合理一些,我们将使用 AutoFixture
,它引入了对 Fare
的依赖。如果我们运行 dotnet add package AutoFixture
,会得到一些控制台输出,而我们的 mydotnetproject.csproj
现在看起来像这样:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
</ItemGroup>
</Project>
第一个 <PropertyGroup>
指定了我们的项目类型(Exe = 可运行的程序),指定了目标框架.NET 6.0 [3] 如何改进 Python 打包)),并启用了 C# 的几个可选功能。第二个 <ItemGroup>
是安装 AutoFixture 时自动插入的。
现在我们可以编写一个简单的 C# 程序。以下是新的 Program.cs
文件:
using AutoFixture;
var fixture = new Fixture();
var a = fixture.Create<int>();
var b = fixture.Create<int>();
var result = a + b == b + a;
Console.WriteLine(result ? "Math is working": "Math is broken");
(我们只需使用 C#/.NET 的内置随机数生成器即可,AutoFixture 在这里完全是小题大做——它本用于自动生成测试数据,支持任意类和其他数据结构,而我们这里只是需要两个随机整数。我在此示例中使用 AutoFixture,因为它简单易用且易于演示,而且它会引入一个传递依赖关系。)
现在,我们可以运行它:
$ dotnet run
Math is working
如果我们希望创建一个可以在项目外部运行、且可能在系统上未安装.NET的环境中运行的程序,可以使用dotnet publish。最基本的场景:
$ dotnet publish
$ ls bin/Debug/net6.0/publish
AutoFixture.dll* Fare.dll* mydotnetproject* mydotnetproject.deps.json mydotnetproject.dll mydotnetproject.pdb mydotnetproject.runtimeconfig.json
$ du -h bin/Debug/net6.0/publish
424K bin/Debug/net6.0/publish
$ bin/Debug/net6.0/publish/mydotnetproject
Math is working
你可以看到,我们有几个与项目相关的文件,以及 AutoFixture.dll
和 Fare.dll
,它们是我们的依赖项(Fare.dll
是 AutoFixture.dll
的依赖项)。现在,让我们尝试从发布的分发中移除 AutoFixture.dll
:
$ rm bin/Debug/net6.0/publish/AutoFixture.dll
$ bin/Debug/net6.0/publish/mydotnetproject
Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly 'AutoFixture, Version=4.17.0.0, Culture=neutral, PublicKeyToken=b24654c590009d4f'. The system cannot find the file specified.
File name: 'AutoFixture, Version=4.17.0.0, Culture=neutral, PublicKeyToken=b24654c590009d4f'
[1] 45060 IOT instruction (core dumped) bin/Debug/net6.0/publish/mydotnetproject
我们还可以尝试一个更高级的场景:
$ rm -rf bin obj # clean up, just in case
$ dotnet publish --sc -r linux-x64 -p:PublishSingleFile=true -o myoutput
Microsoft (R) Build Engine version 17.0.1+b177f8fa7 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
Restored /tmp/mydotnetproject/mydotnetproject.csproj (in 4.09 sec).
mydotnetproject -> /tmp/mydotnetproject/bin/Debug/net6.0/linux-x64/mydotnetproject.dll
mydotnetproject -> /tmp/mydotnetproject/myoutput/
$ ls myoutput
mydotnetproject* mydotnetproject.pdb
$ myoutput/mydotnetproject
Math is working
$ du -h myoutput/*
62M myoutput/mydotnetproject
12K myoutput/mydotnetproject.pdb
$ file -k myoutput/mydotnetproject
myoutput/mydotnetproject: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=47637c667797007d777f4322729d89e7fa53a870, for GNU/Linux 2.6.32, stripped, too many notes (256)\012- data
$ file -k myoutput/mydotnetproject.pdb
myoutput/mydotnetproject.pdb: Microsoft Roslyn C# debugging symbols version 1.0\012- data
我们有一个单一的输出文件,其中包含我们的程序、其依赖项以及 .NET 运行时的一部分。如果我们想使用 .NET 调试器运行二进制文件并查看相关源代码,还可以获取调试符号。(有方法可以缩小二进制文件的大小,并且我们可以将 dotnet publish
的大部分参数移至 .csproj 文件,但本文讨论的是 Python,而非 .NET,因此我不会过多关注这些内容。)
.NET比Python好在哪里?
我不会重复之前在讨论Node比Python好在哪里?时展示的相同示例,但:
- 你可以以任何用户身份从文件系统的任何位置运行已构建的 .NET 项目。
- 运行你的代码所需的只是输出目录(发布是可选的,但有助于获得更干净的输出、简化部署,并可能启用编译为原生代码)。
- 若以单一可执行文件模式发布,只需分发该可执行文件,用户甚至无需安装 .NET。
- 您无需管理运行环境,无需特殊工具运行代码,也无需考虑运行代码时的当前工作目录。
其他打包主题
默认情况下,锁定依赖项功能处于禁用状态,但如果您在 .csproj
文件的 <PropertyGroup>
中添加 <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
,即可启用该功能(并在输出中生成 packages.lock.json
文件)。
关于 命令行工具,.NET 同样支持这些工具。它们可以全局或本地安装,并可通过 $PATH 环境变量或 dotnet
命令访问。
若要将包发布到 NuGet.org 或其他仓库,建议查阅 完整文档 以获取更多细节,但简要步骤如下:
- 在
.csproj
文件中添加一些元数据(例如PackageId
和Version
) - 运行
dotnet pack
以生成.nupkg
文件 - 运行
dotnet nuget push
以上传.nupkg
文件(传入文件名和 API 密钥)
再次强调,所有操作均可通过单一的 dotnet
工具完成。 .NET 集成开发环境(尤其是 Visual Studio 和 Rider)确实提供了许多功能的友好图形界面版本。其中一些图形界面可能在后台以略微不同的方式实现,但这对用户是透明的(且后端仍基于 MSBuild 或其衍生版本)。我可以使用命令行创建的项目,通过 Rider 添加依赖项,然后从 VS 发布可执行文件,所有操作均可正常运行。尽管 XML 文件可能不如 TOML 时尚,但在本案例中仍易于处理。
其他语言和生态系统
虽然我们已经对两种语言的两种工具进行了深入探讨,但还有其他语言也值得一提。在Java领域,最常用的两种工具是Maven和Gradle。这两种工具均可用于管理依赖项并构建可执行或进一步分发的构建 artifacts(如JAR文件)。虽然还有其他支持构建Java项目的工具存在,但大多数人通常会选择其中一种。另一种基于JVM的语言Scala的社区更倾向于使用sbt(也可用于纯Java项目),但该社区中也有使用Maven或Gradle的用户。最后,两种近期颇受欢迎的新兴语言Go和Rust,其官方工具已与整个工具链深度集成。go
命令行工具可完成许多构建、依赖管理和打包任务。Rust 的 cargo
(随 Rust 标准发行版附带)负责依赖管理、构建、运行代码和测试,以及将你的代码发布到注册表。
这些生态系统的工具完美无缺吗?
并非总是如此,它们也存在不足。在 Node 生态系统中,包在安装时可能会执行任意代码,这可能构成安全风险(已知的一些例子包括一个 npm 包 在俄罗斯和白俄罗斯擦除硬盘,或另一个窃取~虚拟互联网货币~比特币的案例)。二进制包不会直接通过 npm 注册表分发,它们要么使用 node-gyp
构建,要么通过 node-pre-gyp
(一个第三方工具)下载预构建包。
在 .NET 生态系统中,工具还会创建一个包含临时文件的 obj
目录。这些临时文件与运行环境绑定,虽然工具通常会在环境发生变化时重新创建它们,但有时可能会失败并导致令人困惑的错误(通常可通过删除 bin
和 obj
目录解决)。如果包依赖于原生代码(该代码未作为共享库在目标操作系统上预先提供),则必须在 NuGet 包中包含所有支持平台的二进制构建,因为目前没有标准方法(https://github.com/NuGet/Home/issues/9631)允许从源代码构建。
你也可以发现其他语言工具的不足之处。有些人认为 Maven 糟糕,因为它使用 XML,而 Gradle 是更好的选择,另一些人则认为 Gradle 使用基于 Groovy 的 DSL 使事情比必要时更复杂,因此更倾向于使用 Maven。
PEP 582:Python打包的未来?
回顾之前在介绍 PDM 时提到的 PEP 582。该 PEP 定义了一个 __pypackages__
目录。Python 在查找导入时会考虑该目录。其行为与node_modules
类似。由于不会创建指向系统 Python 的符号链接,因此可解决虚拟环境迁移时的问题。由于包存储在项目目录中,多个系统用户共享同一项目目录不会产生冲突。在某些特定情况下,甚至可能实现不同计算机(但使用相同 Python 版本和操作系统)共享__pypackages__
目录。建议的 __pypackages__
目录结构包含 lib/python3.10/site-packages/
子文件夹,这仍使“在 Python 升级时重新安装”步骤成为必要,但不适用于次要版本升级。如果处理的是纯 Python 依赖树, mv __pypackages__/lib/python3.10 __pypackages__/lib/python3.11
可能直接生效。这种结构对二进制依赖项或仅在旧版 Python 上需要的依赖项有意义,因为它允许在同一项目目录下使用多个 Python 版本。PEP 并未提及在项目间共享 __pypackages__
目录,但您可能可以通过符号链接解决此问题(假设工具不关心目录是否为符号链接,而我认为它不应关心)。
尽管PEP 582是一个极具远见的提案,且能简化许多与包相关的流程,但它并未得到决策层的足够重视。该 PEP 于 2018 年 5 月提出,甚至有一个可用的实现,代码行数不到 50 行,但 进展有限 使其被 Python 核心接受并实现。然而,PDM 并不在意,它允许你在自己的机器上启用未来功能。
在自己的机器上启用未来功能
让我们在自己的机器上启用未来功能。这只需要一条简单的命令:
$ eval "$(pdm --pep582)"
之后,我们可以初始化项目并安装 requests 模块。让我们尝试:
$ mkdir mypdmproject
$ cd mypdmproject
$ pdm init
Creating a pyproject.toml for PDM...
Please enter the Python interpreter to use
0. /usr/bin/python (3.11)
1. /usr/bin/python3.11 (3.11)
2. /usr/bin/python2.7 (2.7)
Please select (0): 1
Using Python interpreter: /usr/bin/python3.11 (3.11)
Would you like to create a virtualenv with /usr/bin/python3.11? [y/n] (y): n
You are using the PEP 582 mode, no virtualenv is created.
For more info, please visit https://peps.python.org/pep-0582/
Is the project a library that will be uploaded to PyPI [y/n] (n): n
License(SPDX name) (MIT):
Author name (Chris Warrick):
Author email (…):
Python requires('*' to allow any) (>=3.11):
Changes are written to pyproject.toml.
$ ls
pyproject.toml
$ pdm add requests
Adding packages to default dependencies: requests
🔒 Lock successful
Changes are written to pdm.lock.
Changes are written to pyproject.toml.
Synchronizing working set with lock file: 5 to add, 0 to update, 0 to remove
✔ Install charset-normalizer 2.1.1 successful
✔ Install certifi 2022.12.7 successful
✔ Install idna 3.4 successful
✔ Install requests 2.28.1 successful
✔ Install urllib3 1.26.13 successful
🎉 All complete!
目前进展顺利(我并不喜欢终端中的表情符号,但这是我唯一的真实抱怨)。我们的 pyproject.toml
文件如下:
[tool.pdm]
[project]
name = ""
version = ""
description = ""
authors = [
{name = "Chris Warrick", email = "…"},
]
dependencies = [
"requests>=2.28.1",
]
requires-python = ">=3.11"
license = {text = "MIT"}
如果我们查看文件结构,会看到如下内容:
$ ls
pdm.lock __pypackages__/ pyproject.toml
$ ls __pypackages__
3.11/
$ ls __pypackages__/3.11
bin/ include/ lib/
$ ls __pypackages__/3.11/lib
certifi/ certifi-2022.12.7.dist-info/
idna/ idna-3.4.dist-info/
charset_normalizer/ charset_normalizer-2.1.1.dist-info/
requests/ requests-2.28.1.dist-info/
urllib3/ urllib3-1.26.13.dist-info/
我们将编写一个简单的 Python 程序(暂命名为 mypdmproject.py
),使用 requests
模块发送 HTTP 请求。它还会打印 requests.__file__
,以确保它没有使用某个随机的系统副本:[4]
import requests
print(requests.__file__)
r = requests.get("https://chriswarrick.com/")
print(r.text[:15])
$ python mypdmproject.py
/tmp/mypdmproject/__pypackages__/3.11/lib/requests/__init__.py
<!DOCTYPE html>
让我们终于尝试在其他语言中进行的测试。没有 urllib3,requests 就毫无用处,所以让我们移除它 [5] 并看看它的工作效果如何。
$ rm -rf __pypackages__/3.11/lib/urllib3*
$ python mypdmproject.py
Traceback (most recent call last):
File "/tmp/mypdmproject/mypdmproject.py", line 1, in <module>
import requests
File "/tmp/mypdmproject/__pypackages__/3.11/lib/requests/__init__.py", line 43, in <module>
import urllib3
ModuleNotFoundError: No module named 'urllib3'
最后,我们能否尝试使用不同的目录?或者不同的用户?
$ pdm install
Synchronizing working set with lock file: 1 to add, 0 to update, 0 to remove
✔ Install urllib3 1.26.13 successful
🎉 All complete!
$ pwd
/tmp/mypdmproject
$ cd ~
$ python /tmp/mypdmproject/mypdmproject.py
/tmp/mypdmproject/__pypackages__/3.11/lib/requests/__init__.py
<!DOCTYPE html>
# su -s /bin/bash -c 'eval "$(/tmp/pdmvenv/bin/pdm --pep582 bash)"; python /tmp/mypdmproject/mypdmproject.py' - nobody
su: warning: cannot change directory to /nonexistent: No such file or directory
/tmp/mypdmproject/__pypackages__/3.11/lib/requests/__init__.py
<!DOCTYPE html>
这看起来相当不错。一个独立项目成功实现了权威机构多年来未能完成的任务。
这是完美解决方案吗?
几乎是。我有两点不满。第一个是 pdm --pep582
技巧,但希望 PyPA 能尽快将其纳入 Python 核心。然而,另一个重要问题是与系统 site-packages
的分离不足。热衷于阅读脚注的读者 [6] 可能注意到,我在 PDM 实验中不得不使用 Docker 容器,因为 requests 通常存在于系统 site-packages
中(尤其是在使用系统 Python 时,因为某些随机包包含 requests,或者因为它未被 pip 打包)。[7] 这可能会以你意想不到的方式破坏系统,因为你可能会无意中导入并依赖系统范围内的包,或者混淆系统范围内的包和本地包(如果你没有安装额外的依赖项,但这些包在系统范围内存在,那么你可能会使用到未请求的额外包)。这是一个重要问题——一个好的解决方案是,如果使用了 __pypackages__
目录,则禁用系统 site-packages。
指导委员会否决该提案的部分
2023 年 3 月底,Python 指导委员会宣布 拒绝 PEP 582。指导委员会的决定中提到的理由包括PEP的局限性(__pypackages__
目录并不总是足够,且未明确规定其在特殊情况下的行为方式)。另一个论点是,可以通过“sys.path
的多种现有自定义机制(如 .pth
文件或 sitecustomize
模块)”获取 __pypackages__
——这些通常被视为权宜之计,而非真正的解决方案。尽管用户确实可以对 sys.path
进行任何操作(往往会带来灾难性后果),但制定共同标准的目的是鼓励工具添加对其的支持——如果你使用上述黑客手段,你的 IDE 可能无法识别这些包,或将其视为代码的一部分(试图对其进行索引并搜索其中的内容)。另一个被提及的拒绝原因是打包社区的分歧,这并不令人意外,尤其是在考虑到下一节的内容时。
PEP 582/__pypackages__
机制或许有一天会成为官方标准,从而让 Python 的打包方式更加易于使用。这可能需要有人挺身而出,撰写一份新的 PEP,以让更多人满意。或者 Python 可能会继续使用这些不兼容的工具,并在未来几年内再发明 10 种新的工具。(PDM 仍然存在,并且仍然支持 __pypackages__
,尽管其实现与现在被否决的 PEP 建议的不完全相同。) Python 当前的发展轨迹,从这一决策以及许多人仍被迫应对不必要的复杂虚拟环境需求来看,与 经典的洋葱头条 极为相似: “无法阻止此事发生”,这是唯一一个经常发生此类事情的编程社区的说法。
PyPA与现实:打包调查结果及PyPA反应
一段时间前,PSF 进行了一次关于打包的调查。超过 8000 人参与了调查。用户已发声:
- 大多数人认为打包过于复杂。
- 绝大多数人更倾向于使用单一工具。
- 大多数人认为多个工具的存在对 Python 包装生态系统无益。
- 几乎所有人更倾向于明确定义的官方工作流程。
- 超过 50% 的受访者认为其他生态系统的工具在管理依赖项和安装包方面更优。
此次调查后的下一步是包装社区讨论其结果并尝试制定新的包装策略。PSF 包装项目经理 Shamika Mohanan 发布的首篇引发讨论的帖子,也重点强调了用户希望统一包装工具并实现“唯一真实工具”的愿景。此次讨论向包装领域相关人员开放;许多参与者与 PyPA 有关,但我未看到 Poetry 或 PDM 团队的任何评论。
大多数讨论集中在二进制扩展上,包括如何通过使非setuptools工具能够构建二进制扩展来促进工具的普及。讨论还大量聚焦于科学界对包含原生代码的库的担忧,这些库主要基于C/C++,并试图用PyPA批准的新工具取代Conda。虽然有部分帖子提到了“统一工具”的概念,但这类观点显然属于少数。
部分PyPA成员提到了用户体验(UX)分析,并表示统一工具将重新导出现有工具的功能——这立即引发了一个问题:它应从哪些工具导出功能以及为何如此?是否只需执行pip install unified-packaging-tool
就能导入全部十四个工具?用户对现有工具的不满,以及许多人希望拥有类似npm/dotnet/cargo的工具,难道不足以决定统一工具的用户体验方向吗?
其中一些人还反对破坏现有工作流程。统一打包工具能适用于每个用户吗?当然不能。但是,是否存在如此多的基本工作流程?如果我们忽略那些接近无谓争论的问题,例如 src 与 no-src 的区别,或虚拟环境的位置,是否需要考虑如此多的工作流程?制作库的人和制作应用程序的人确实有不同的需求(例如,关于发布包或可接受的依赖版本)。使用C扩展(或使用类似Cython的扩展)的人可能有不同的需求,但他们的需求通常是纯Python项目需求的一个子集。科学界可能有更专业化的需求,与复杂的非Python部分相关,但我相信其中许多问题也可以通过统一工具解决,即使该工具在达到v1.0之前无法完全解决。此外,科学界可能更倾向于继续使用Conda,或使用其某种演进版本——该版本既能与统一打包工具保持一致,又能更好地满足科学家的需求,而不仅仅是满足非科学家的需求。
然后,人们开始讨论现有的工具以及哪一个才是未来的方向。Hatch 的维护者 Ofek Lev 表示,Hatch 可以提供“统一的用户体验”。但Poetry或PDM的维护者是否认同这一观点?从GitHub问题数量来看,Poetry似乎比Hatch活跃得多,值得注意的是,Hatch的“单点故障因素”为1(Ofek Lev负责了主分支576次提交中的542次)。BeeWare的Russell Keith-Magee指出,撇开工具不谈,PyPA在沟通方面做得并不好。Russell提到,PyPA的其中一个教程现在使用了Hatch,但无法确定PyPA是否认为Hatch是未来方向,人们是否应该迁移到Hatch,以及另一个最近的PyPA工具Flit是否现在变得无用?Russell还提出了关于集中努力的合理观点:人们应该专注于帮助Hatch支持扩展模块(据Hatch维护者称,这是最后一个需要setuptools的场景;其他参与者指出,你已经可以不使用setuptools构建原生代码),还是应该专注于改善setuptools与PEP 517的兼容性?
还有一些人就以各种方式统一事物发表了看法——其中许多人反对 统一 这些内容。也有一些理性声音,如Russell Keith-Magee的观点,或 西蒙·诺特利的观点,他们正确地指出,该讨论未能解决开发者在打包方面感到困惑的问题,以及他们不了解不同选择及其相互作用的方式。西蒙确实认同原生依赖在 Python 项目中很重要且常见(我也是这么认为的),但参与调查的用户另有想法——正如讨论开篇帖子所提,用户期待 Rust 的 cargo 那种简洁性,而调查结果也印证了这一点。70% 的调查参与者同时使用 npm
,因此许多 Python 用户已亲身体验过更简洁的工作流程。调查参与者还被要求根据重要性对几个重点领域进行排名。“让 Python 包管理更好地服务于常见用例和工作流程”在提供的选项中排名第一[8],共有 3248 名参与者选择。“支持更广泛的用例(例如边缘用例等)”被379人评为第一,但在2989人心中却是最不重要的。
安德森·布拉瓦尔赫里提到的另一个凸显打包人员与现实脱节的点是: 在安德森看来,一个新的统一工具将是对现有工具维护者所付出的维护工作的“不尊重”,也是对不得不适应打包混乱的用户的“不尊重”。这一观点完全荒谬。用Windows NT和~Mac OS X~ ~OS X~ macOS取代MS-DOS/Windows 9x和Classic Mac OS,是否对各自的设计师不敬,以及对那些不得不手动配置细节、弄清楚如何让所有软件和硬件在20世纪80年代必要的奇怪限制下运行、并且系统偶尔会崩溃的用户不敬?用汽车取代马匹是否对马匹不敬,以及对那些清理街道上马粪的人不敬?用更快、更安全、更高效且更易于使用的汽车取代福特T型车是否对亨利·福特不敬?技术来来去去,有时,获得改进意味着我们需要淘汰旧事物。这一观点也适用于科技领域之外——你可以列举许多世界上的变革案例,这些变革可能让某些人失去权力,但却极大改善了数百万人的生活(例如欧洲共产主义的崩溃)。回到当今科技世界,这种观点暗示安德森对他们编写的软件过于依恋——这是一种健康的做法吗?
没有人提到PEP 582或虚拟环境的复杂性。对于那些拥有多年经验的打包工具维护者来说,这些问题可能并不明显,但对于普通人来说,对于那些认为Linux发行版提供的Python已经足够好的人来说,尤其是对于那些将Python作为编程入门的人来说,这些问题确实存在。
我再次强调:这不仅仅是某个随意发表意见的克里斯的观点。认为Python打包需要简化和统一的观点,得到了参与调查的8774人中约一半人的认同。
但还有一个有趣的现象:此次讨论所使用的平台Discourse显示了链接被点击的次数。当然,这个计数可能并不总是准确,但如果我们假设它是准确的,那么截至2023年1月14日21:20 UTC,结果摘要链接仅被点击了14次。该讨论有28名参与者和2.2千次浏览量。如果我们相信链接点击计数器,一半的讨论参与者甚至没有 bother 阅读其他人认为的内容。
摘要
Python打包一直是一团糟,而且一直如此。有大量工具,大多彼此不兼容,且没有工具能解决所有问题(尤其是PyPA提供的工具)。PDM 非常接近理想状态,因为它可以消除管理虚拟环境的开销——这可能是 Python 包管理的未来,或者说 Node.js 包管理的 2010 年代(尽管它不会成为 Python 包管理的 2023 年,考虑到指导委员会的否决)。或许在几年后,Python开发者(更重要的是Python学习者!)只需通过pip install
(或pdm install
?)安装所需内容,无需担心与系统Python分离但又不完全独立的“虚拟环境”问题,也无需使用虚拟机。Python需要更少的工具,而非更多的工具。
此外,我认为PyPA必须被废除。战略讨论凸显了他们无法让Python打包符合用户期望的事实。PyPA 应该专注于打造一个优秀的工具,并推动 PEP 582 进入 Python。实现这一目标的好方法是将资源投入到 PDM 中。原生代码和二进制轮子的问题很重要,但纯 Python 工作流程或具有简单二进制依赖的工作流程更为常见,需要得到改进。这种改进必须立即进行。
在下方评论区、Hacker News 或 Reddit 上讨论。
脚注
[1]有趣的是,这句格言本身在“一种显而易见的做法”上也失败了。它使用了两种不同的破折号设置方式(破折号后有空格但前面没有,以及破折号前面有空格但后面没有),而这两种方式都不是正确的(大多数英语风格指南更倾向于不使用空格,但有些允许在两侧都使用空格)。
[2]对于这篇帖子略微偏向 Linux 的内容,我深表歉意;我提到的所有要点在 Windows 上同样适用,只是可能需要使用略微不同的名称和命令。
[3].NET 每年都会发布一个新的大版本,其中偶数版本为长期支持(LTS)版本。这些版本的变革性远不及 Python 2 到 3 的过渡,一旦你跟上现代 .NET 的步伐,将项目升级到新的大版本相对简单(可能只需更新版本号即可)。
[4]为了更加保险,我使用了干净的 python:latest
Docker 容器,因为 requests 通常存在于系统站点包中。
[5]这里有一个小注意事项,我还必须删除 dist-info
文件夹,以便 PDM 知道需要重新安装。
[6]没错,就是你!
[7]此外,为什么Python的标准库中没有一个好的HTTP客户端库?如果requests在2022年发布了四个版本,urllib3发布了六个版本,而且大多数更改都是次要的,那么“标准库是包的坟墓”这一论点是否仍然成立?
[8]我已移除“其他”选项,并将所有排名在其下方的选项向后推移一位,因为我们无法确定“其他”选项具体指什么,以及它与所列选项之间的关联性(为保护用户匿名性,自由回答部分已从公开结果表格中移除)。若受访者未对部分选项进行编号,则空白选项既不被视为第一位也不被视为最后一位。
本文文字及图片出自 How to improve Python packaging, or why fourteen tools are at least twelve too many
这篇帖子是基于对Python用户的调查、他们的反馈以及Python论坛上关于未来发展方向的当前讨论线程[0]而撰写的。我前几天读了这个讨论线程,内容很长,而且有很多不同的观点。
在本文末尾有一个有趣的观察:
> 用于讨论的Discourse平台显示了链接被点击的次数。当然,这个计数可能并不总是准确,但如果我们假设它是准确的,那么截至2023年1月14日21:20 UTC,结果摘要的链接仅被点击了14次。该讨论有28名参与者和2.2千次浏览量。如果相信链接点击计数器,那么一半的讨论参与者甚至没有 bother 阅读他人的观点。
此外,我的担忧是参与者过多。一个打包系统不可能满足90%以上的用例,但那些属于10%特殊用例的参与者可能占比较高。存在他们无法就解决所有问题的方案达成一致,最终导致项目无法启动的风险。
不如先推出某种东西,立下标杆,再在此基础上逐步完善。此外,它必须是“核心 Python”,据我所知,PyPA(Python 包管理机构)与核心 Python 开发团队有些脱节。
最终,他们需要一个处于核心地位的系统,作为“官方认可”的工具,并对此有非常明确的表述。
0: https://discuss.python.org/t/python-packaging-strategy-discu…
> 最终,他们需要一个处于核心地位的系统,即“官方认可”的工具,并对此有非常明确的说明。
我认为这很重要;否则,切换到新的打包工具后,可能会面临被忽视或缺乏资源的风险,这会让人们对切换到该工具产生抵触情绪。这就是为什么虚拟环境/pip 是最低公约数——每个人都知道,最坏的情况下,这些工具始终会继续工作。官方工具需要激发这种信心。
是的,关于pipenv被“官方推荐”的沟通存在一些问题,经仔细审查,似乎这些说法主要来自该工具的开发者,而非真正官方渠道。虽然后来这一说法有所收敛,但在此之前已产生影响,甚至有人至今仍错误地认为它是官方推荐的。
我真的很不愿以这种“圈内人”的负面态度看待这一切,但肯尼思·莱茨(Kenneth Reitz)的因素……在 Python 社区中是独特的,而且在这种情况下尤为危险。我对 Python 包管理体验感到极度厌倦。目前,我只关注如何尽快实现某种标准化和改进。正因如此,我希望明确指出,在我看来,过去五年我们偏离正轨,很大程度上是因为某人对个人名声的渴望,而社区显然需要齐心协力寻找解决方案。Pipenv被过早地吹捧为万能解决方案,正如你所说,这并非来自“官方渠道”,而那些动荡的岁月让 everyone 都有些疲惫不堪。
当我每12个月进行一次检查,看看Python依赖管理进展如何时,我确实看到Pipenv现在“隶属于”PyPA。然后我了解到,PyPA的认可或关联并不意味着什么,原因正如你和其他人所指出的。确实,PyPA自己的打包文档对推荐前端工具持谨慎态度,更不用说堆栈的其他部分了。
一个显著改进的 Python 依赖管理方案完全可以包含 Pipenv 作为前端工具,我对此并无异议。但忽视显而易见的问题可能导致重蹈覆辙。我们不希望像 JS 社区那样陷入名人开发者文化,却又缺乏看似无尽的努力和资源。
这并非轻视技术挑战,尤其是考虑到整合当前碎片化生态系统的难度。
> 来自那些属于10%的人群的代表性可能会高于平均水平
而“普通用户”的代表性则会不足。
Python 语言一直受益于其相对简单的特性,我认为这得益于像 GVR 这样的人对语言中像船上藤壶一样堆积的专用功能说“不”(这里指的是 C++)。
在新的编程语言中,核心设计团队对构建工具和依赖项/打包管理工具的看法更加明确。这无疑是对C++和Python生态系统混乱的回应,因为“生态系统会自行解决问题”显然行不通。
我从Python 1.6版本开始接触这门语言,它的简单性具有欺骗性,其实这门语言的丰富程度不亚于C++,只是初学者因为表面看起来简单而产生了误解。
当开始探索标准库时,你会发现元编程和运行时内部实现的细节,以及即使在小版本更新中语言规范的变动,这一切都会让对Python的认知发生翻天覆地的变化。
曾经,我总是仔细阅读每个版本的发布说明,但最终我失去了对变化的跟踪。
几年前,我参与了一个(公司战略性)实验项目,该项目使用Python。显然,它只在某些特定版本的Python上运行,这是因为集合的内部顺序发生了变化,而这一事实当然没有在工具中记录或检查。
这并非项目失败的原因,但试图在团队之间复制结果所浪费的数周时间无疑雪上加霜。
Python 非常易读,但我同意标准库绝非简单。
我常说,若非神来之笔,做出正确选择几乎不可能。更可能的是先做出选择,再努力使其正确。
我认为你说得对。冒着被视为“低俗否定”的风险,TFA 的长度本身就是问题的明显症状:委员会设计(PyPA(Python 包管理机构)让我想起了电影《巴西》。别让我开始说这个。)
– – – –
有人应该为打包做的事情,就像 pathlib 为文件 I/O 所做的那样:为它创建一个 Python 风格的模型/API。然后部署就变得简单、可脚本化、可测试、可重复等…
这可能是一个有趣的想法,但 PyPA 并没有真正形成设计委员会(至少,我没有参加过一个)。你可以清楚地看到 Python 包装标准是如何制定的:它们是通过 PEP[1] 完成的,与其他 Python 标准化努力完全相同。
事实上,大多数包装 PEP 都是以你所描述的方式开始的:一个工具或服务编写一个 Python 风格的 API 或模型,然后将其标准化,以便其他工具可以依赖它。TFA 的问题(这些问题确实存在!)主要源于在 Python 包装工具之间尚未进行严肃标准化努力之前的错误。
[1]: https://peps.python.org/topic/packaging/
> 别让我开始说这个.
谢谢,这是一个有趣的细节。你是否打算为[0]添加一个链接引用?
是的,已编辑。谢谢。
随着时间的推移,我逐渐开始欣赏“pip-tools”。由于它只是pip的一个简单扩展,我希望它能被合并到pip本身中;这似乎是解决Python打包问题最直接的方式。
我认为很多人会对 pip-tools 皱眉,因为它是一个更手动的工作流程,但我真的很喜欢这个方面。我使用过的其他包管理器一开始很方便,但最终我不得不与它们抗争,而它们不会让步。使用 pip-tools,到头来我才是掌控者。
此外,我非常喜欢最终生成的 requirements.txt 文件,它可以与任何普通的 Python+pip 环境兼容。
没错。我喜欢让每个可执行文件都通过符号链接指向一个 shell 脚本。
shell 脚本会检查是否已设置虚拟环境并安装了 requirements.txt 中的包(它会对文件进行快照,因为 virtualenv 没有可快速查询的数据库)。一旦环境设置完成,它会从虚拟环境中运行。
这样,当你更新 requirements.txt 文件时,它会重新编译(如果 txt 文件过时),安装新包,移除不再需要的包,并更新版本已更改的包(如果发现任何更改)。它还允许你轻松运行具有不同 requirements.txt 文件的工具,而不会发生冲突,因为每个工具都位于自己的虚拟环境中。
这使得在共享仓库中使用这些工具变得非常简单,因为无需担心包问题或记住要在正确环境设置前运行某些命令。你只需修改代码并像运行普通命令行工具一样执行,包会自动部署。它也支持离线快照/分发。事实上,我曾用此方法分发Pixel Buds生产线的支持工具,效果极佳。
你描述的是一种更糟糕的、自制的诗歌版本。
考虑到我在2015年就完成了这项工作,而那时诗歌甚至还不存在,这种说法未免太过刻薄和吝啬。此外,我可能错了,但简单查看后,似乎它仍然存在一个问题,即当可执行文件的依赖项发生变化时,它不会自动更新你的虚拟环境(例如,有人合并了使用新依赖项的代码,你仍然需要知道如何运行一些命令来更新你自己的本地虚拟环境)。
是的。虽然这一点通常被忽略,但如果你正在开发一个 Python 应用程序,这就是你所需要的全部。
我从事多仓库开发,我真的很不喜欢 Git 的子树功能,我使用的是 https://pypi.org/project/zc.buildout/。是的,我知道我可以使用[1]。但在多仓库环境中同时编辑时,我只能使用zc.buildout[2]。虽然还不完美,但它能完成任务。
[1]
[2]
啊,我好几年没用过 buildout 了(记得在用 Plone 时用过很多次)。
我曾用它做过个人项目,但几年前放弃了,因为其中某个部分似乎出了问题且被 abandon(可能是某个组件没更新到新版本的 TLS 之类的问题)。
不过我还是喜欢 buildout——它的 bin 目录是个不错的系统。
目前我基本上已经确定使用Poetry来处理大多数事情。
它仍然有很多粗糙的边缘,比如错误消息更接近堆栈跟踪而不是可操作的内容,而且解析速度慢得要命,但90%的时间它都能正常工作。
现在我还是在Docker中运行一切,这绕过了其他一些问题。
对于好奇的人来说,缓慢的原因在这里简要描述:https://python-poetry.org/docs/faq/#why-is-the-dependency-re…
我们目前也处于相同状况,但你使用Poetry+Docker时未解决的问题依然存在:PyPI生态系统不会因你的需求而停止量子波动。如果你过度固定依赖项,将无法获得安全更新;若固定不足,每次三级依赖项发生你未预期的变更时都会受到影响。哦对了,现在上游几乎每天都有木马程序出现。
是的,但这种权衡是完全独立的。
我们用安全换取速度(或者用行话来说是“速度”)。
我只是固定所有依赖项,每隔几周检查一次项目并更新依赖项(除非有重大 CVE 漏洞登上新闻)。
是否有任何开源软件包生态系统对这一特定问题集有良好的解决方案?
我知道Go语言历史上曾对每个依赖项的“最小可行版本号”做过一些有趣的尝试,但这是我能想到的唯一相关想法。
对于PyPi,至少可以相对轻松地自行托管一个PyPi实例并自行管理依赖项更新。
在 Python 生态系统(相较于 Node 等)中,大多数普通项目的首层+次层依赖总数相当有限(例如中位数为两位数到三位数)。每月或每季度管理此规模的升级并不显得过于繁琐。
在 JVM 生态系统中,你获得的是直接依赖项构建/发布时对应的版本,传递性依赖项绝不会被静默升级(尽管如果你想升级,只需一条命令即可)。依我之见,这是更好的默认行为;这意味着当低级别库中发现安全问题时,所有人都会发布一系列仅提升依赖项版本的更新,但这种一致的行为非常值得。
Poetry 似乎仍不支持 PEP621,而是要求使用其自定义的、供应商特定的 pyproject.toml 格式。
他们是否讨论过不支持它的原因?或者未来支持它的计划?
似乎通向 Python 通用包管理工具的最短路径是为 Poetry 添加此功能,而非从头构建新工具。
https://github.com/python-poetry/roadmap/issues/3
虽然文档不够完善,但PyPA工具在正确使用时确实能提供统一的体验。
这是一个PyPA项目(FD:我参与的项目),它使用单个pyproject.toml文件来处理所有打包相关事宜(甚至包括大部分非打包工具的配置)[1]. 有了这样的单一文件,你需要做的唯一事情就是启动本地开发环境:
(我们提供了一个
make dev
目标,它也能为你完成这项任务。)同样地,要构建发行版,你只需要运行
build
:[1]: https://github.com/pypa/pip-audit
最终,需要由“Python.org”来认可该工具,而非PyPA。在整个体系中,没有人知道PyPA是谁,也不清楚它是否是“唯一正确的途径”。
如果你访问Python.org并按照初学者指南[0]操作,会得到以下建议:
> 安装额外Python包有多种方法:
> 可通过标准Python distutils模式(python setup.py install)安装包。
> 许多包也可以通过setuptools扩展或pip包装器安装,详见https://pip.pypa.io/。
这已经过时了,而且会让初次接触Python的人感到困惑。为什么pip是次要的,而且没有提到venv?
PyPA需要获得Python核心团队的支持,将某一工具置于核心位置。它需要像Rust与Cargo一样,成为初学者首先学习使用的工具,并成为所有入门指南的核心内容。
这并非贬低PyPA的工作,你们都非常出色,我只是希望你们的工作能更加显而易见!
0: https://docs.python.org/3/using/mac.html#installing-addition…
未来已至,只是分布不均。还有一个问题是python.org不愿选择赢家,即不推广activestate、conda或pypa。
https://packaging.python.org/en/latest/tutorials/installing-…
https://packaging.python.org/en/latest/tutorials/packaging-p…
这简直是荒谬至极。Python 包装故事将一直混乱下去,直到这一特定不一致性得到解决。
在pypa文档中,你的简单工作流程从未被描述或提及。取而代之的是,它是一堆指向各种工具(如hatchling、flit、pdm等)的链接,基本上就是一个耸肩的姿态,传达着“我不知道,你自己搞清楚吧”的信息。这篇文章指出,当前 PyPA 的“指导”对实际终端用户(即不直接参与 PyPA 开发或未研究 Python 包管理数十年的人)来说过于混乱和模糊。
我同意这很混乱。不过,PyPA确实有一个官方教程,详细介绍了生成我建议的命令所需的具体步骤,因此我认为说“找不到相关文档”并不准确[1])。
编辑:不幸的是,上述教程很容易与这个教程[2]混淆,后者专门针对setuptools。因此,我能理解用户对文档的困惑,尤其是在这一点上!
[1]: https://packaging.python.org/en/latest/tutorials/packaging-p…
[2]: https://packaging.python.org/en/latest/guides/distributing-p…
不,我正在查看链接 1,并且特别对设置 pyproject.toml 时出现的庞大选项表有意见——它包含 hatchling、flit、pdm 等标签,但完全没有解释为什么我需要使用这些工具。这只会让人更困惑,就像被扔了一袋零件却没有说明要组装什么或如何组装一样。
说实话,整个pypa文档应该只有两段文字,而不是1000多字。它应该基本上是:“要打包你的Python应用程序,只需运行<官方Python工具来初始化包>,就这样,你完成了!”。所有决策都应该替我做出,就像npm、cargo等工具一样。我不应该需要在运行一个命令之外再思考其他事情。
这就是Python终端用户所需的。我们不需要成千上万的工具和庞大的文档,这些文档似乎缺乏明确的愿景或目标。我们需要高效完成任务,而花在打包和工具上的时间越少越好。
没错。缺乏能真正帮助用户决定使用什么工具的文档,对我来说直接意味着“这里正发生某种政治纷争,而我正被卷入其中”。
那次“政治纷争”实际上是对前PyPA成员的针对性骚扰,当时有人试图在packaging.python.org上表达观点。由于此事以该成员退出告终且无人接替,该项目至今仍保持中立。
听起来,现在负责管理Python的指导委员会或其他相关机构需要采取行动。如果有人因为试图改进打包流程而受到针对,那么指导委员会应该做出决定,并将所有反馈意见等直接反馈给他们,而不是让个人去记录最佳实践。
整个“让社区自行解决”的模式似乎已经失败,并导致了更多混乱,甚至对个人的攻击。指导委员会需要挺身而出,明确表示:“Python打包将以这种方式运作,点到为止。此事已定,不再讨论。关于此问题的讨论到此为止,决定已最终确定。所有其他Python打包工具现在均被视为非标准且不推荐使用。”
这是BDFL领导模式能实现的唯一好处,即在面对众多强烈意见时做出艰难决策。
编辑:我的兄弟评论有另一个答案,这让我意识到自己缺乏上下文。因此我删除了此评论,以避免在不了解情况时发表意见。
几周前,我试图帮助某人入门 Python。我感到尴尬的是,不得不掩盖初始配置对新手而言有多么复杂和令人困惑。没有官方认可的最佳实践可供参考。六个派系各自提供相互竞争的解决方案和指导。
Python.org 对“正确方式”的明确声明将产生深远影响。
据我所知,在Python 2时代,这已经是事实上的标准(只需将“python -m venv”替换为“virtualenv”)。所有这些新工具只是让事情变得更复杂,我不清楚它们带来了什么好处,似乎只是成功地让人们相信它很复杂,而实际上只是掩盖了正在发生的事情。
正确。当时大多数人只是使用
pip install -r requirements.txt
,根据需要使用不同的文件进行开发/测试/生产环境。我曾在一家使用buildout的公司工作,这似乎相对罕见,但buildout更灵活,允许你做一些事情,比如安装系统包或运行自己的脚本。我们用它来下载未提交到仓库但仍需共享的配置文件,帮助设置本地开发环境,以及其他一些用途。
这是我第一次听说PyPA,查看他们的官网[1]:如果一个打包权威机构的主页上存在损坏的徽标链接[2],这可不是个好兆头。
[1]: https://www.pypa.io/ [2]: https://pypi.org/static/images/logo-large.svg/
因此,您链接的项目中的 pyproject.toml 文件使用了 flit:
Python.org 在文档中默认使用 hatchling[1].
PyPA 提供了一个使用 setuptools 的示例仓库[2]. 该项目在 Python.org 的此页面上有链接[3].
Python.org 在打包建议中也推荐使用 setuptools,甚至在该页面上未提及 hatchling 或 flit[4].
难怪这么多开发者对此感到沮丧。过去 11 年我主要用 Python 进行工作和娱乐编程,但现在我对该使用哪些工具感到困惑。当我启动新副项目时,无法确定某个工具是否会在几年后仍存在且正常工作,因为有 10 个工具试图实现相同功能,而社区究竟支持哪个工具并不明确。
[1]: https://packaging.python.org/en/latest/tutorials/packaging-p…
[2]: https://github.com/pypa/sampleproject/blob/main/pyproject.to…
[3]: https://packaging.python.org/en/latest/guides/distributing-p…
[4]: https://packaging.python.org/en/latest/guides/tool-recommend…
这仍然大约是正常/合理命令数量的4倍,与例如“mvn install”相比。
该仓库似乎没有固定依赖版本。您如何在该工作流中集成这一点?
如果你想实现完全固定,可以使用`pip-compile`。我们在另一个项目中就是这样做的——我们使用GitHub Actions与`pip-compile`结合,为希望获得完全冻结的依赖项树的用户提供一个完全冻结的副本[1].
在 `pip-audit` 的上下文中,这种做法似乎不太合理:我们的依赖项大多采用语义化版本控制,我们更希望用户能够自动获得子依赖项的补丁和修复,而不是等待我们发布相应的修复版本。同样,我们期望用户将 `pip-audit` 安装到现有的虚拟环境中,这意味着过度的依赖固定会导致过于保守的依赖冲突错误。
[1]: https://github.com/sigstore/sigstore-python/tree/main/instal…
或者如果您不想安装其他内容,并且愿意仅使用版本号(而非像该链接中的 pip-compile 那样同时使用哈希值),“pip freeze” 命令已内置。
使用
pip freeze
的棘手之处在于,它导出的是你的_环境_
,而非_已解析的包集合_
:你的环境还包含pip
和setuptools
的版本、任何开发工具,以及可能的全球环境状态(如果环境具有全球访问权限且你忘记向pip freeze
传递--local
参数)。换句话说,它通常是 `pip-compile` 收集的解析结果的超集。这可能正是你想要的,也可能不是你想要的,或者不是你的用户所期望的!
默认情况下(至少在现在的 Python 3 中),它还会排除 pip、setuptools、distribute 和 wheel;你需要使用 “–all” 选项才能将它们包含在输出中。
哦,这是我第一次听说!我收回之前的说法。
(关于其他开发工具的论点,我认为仍然准确——例如,如果你安装了`black`,`pip freeze`会显示它。)
这是许多其他工具的巨大局限性。我对 poetry 有一些不满,但它至少在一点上做对了:区分代码所需的库和开发工具(pytest、black 等)。虽然其他工具可以通过一些权宜之计实现这一点,但明确区分这一点的价值是无法估量的。
使用 pip-tools,您可以使用 requirements.txt 文件用于生产环境,而使用 requirements-dev.txt 文件用于开发环境。后者会导入前者。
在哪里定义了
的格式/语法?我试图找出可能实现的方式以及如何在 setup.cfg 工具中理解它
我在文档中只找到一次提及,但没有更多信息。
PEP-508[0] 解释了“extras”的语法:
> 分发的可选组件可通过 extras 字段指定:
同时简要解释了其行为:
> 附加项与它们所属分发的依赖项进行联合。
关于 . 的解析由 pip 文档[1]解释:
> pip 在多个位置搜索包:在 PyPI 上(如果未通过 –no-index 禁用),在本地文件系统中,以及通过 –find-links 或 –index-url 指定的任何额外仓库中。搜索位置没有顺序。而是会逐一检查所有位置,并根据版本号(详见 PEP 440)选择与需求最匹配的包。
[0]: https://peps.python.org/pep-0508/#grammar
[1]: https://pip.pypa.io/en/stable/cli/pip_install/#finding-packa…
你可能已经执行过
这与前者相同,只是针对当前目录(.)和开发版本。
没错。关键区别在于前者在PEP 508中明确规定,而
.
及其变体是pip(以及可能其他工具)提供的便利功能。是的,但我缺少关于变体与 setup.cfg 中定义的各种要求(例如测试依赖项)如何交互的数据。
这是 pip 的特有功能;据我所知,它并未在任何 PEP 中定义。不过它应该在 pip 的文档中有所说明。
那我得深入研究一下,谢谢
由于 Python 的复杂性,我通过 Docker Compose 运行所有内容。不再出现因缺少 wheel 包或需要本地安装 X 和 Y C++ 工具链包等原因导致在某些开发者电脑上无法运行的问题。升级Python或Poetry版本后无需再修复损坏的配置,只需执行“docker compose build”即可启动运行。无需花费数天时间让新克隆的项目正常运行。
然后我通过Docker Compose将Pycharm指向该远程解释器。
虽非完美无缺,但开发体验提升了百倍。
当解决像这样基本问题的方法是“使用Docker”时,你就会意识到现代软件开发(至少在Python中)是多么的缺陷重重。
我不确定是否同意“严重缺陷”的结论。
Python的打包系统(以及GIL和其他问题)的缺陷,源于设计一种语言时所做的权衡,这种语言旨在优化尽可能多地复用和连接不同系统二进制文件的能力。
这样一种服务器脚本语言与运行环境紧密耦合并不令人意外。Docker只是确保跨机器服务器环境可重复的一种方式。
你认为它本可以做些什么不同的事?
可以做些什么不同的事:
– 默认使依赖项解析确定性。不幸的是整个生态系统必须接受这一点,但好处巨大。
– 停止通过添加更多必须在 Python 中运行的工具层来建造沙堡(因此这些工具将运行在某个随机且管理不善的 Python 环境中)。语言安装无法管理构建系统安装——如果有什么不同,构建系统安装应该管理语言安装。将 pip 添加到语言发行版中是一个如此倒退的决定,以至于它标志着我放弃了 Python 能够修复其问题的希望。
– 忽略 Linux 系统包管理器(如 apt 等),它们的模型存在根本性缺陷,若试图迎合它们,将使你的语言生态系统受到这种缺陷的感染。
公平地说,几乎每个生态系统都已进入“使用 Docker”的阶段,甚至 Go 二进制文件也会提供 Docker 镜像!
我尝试过几次,但总体上不喜欢这种体验,最终还是回去了。当你进入容器的 shell 以使用项目中的 Python 环境时,你机器上安装的工具(如 bashrc、rg、别名)无法使用。我最终更倾向于原生管理环境的麻烦,而不是放弃使用我的工具或在不同 shell 之间切换。
我私下里认为Docker的存在,正是因为Python的分发机制一团糟。
我也经常想,Docker的成功有多少要归功于Python的不足。
.deb 包……
说到这一点,为什么我们要使用语言特定的打包工具来构建包,而不是直接构建操作系统特定的包,其中依赖项由 apt 处理 deb 包或 dnf 处理 rpm 包?
这些是语言无关的,并且可以完成任务。
因为我有数十位同事,每个人使用不同的操作系统/发行版,正在开发一个需要锁定依赖项的点版本以匹配生产环境中运行的版本并同时更新它们的应用程序。使用发行版特定工具来实现这一点会让人发疯。
>> 为什么我们要尝试使用语言特定的打包工具来构建包,而不是直接构建操作系统特定的包,其中依赖项由 apt 用于 deb 包或 dnf 用于 rpm 包来处理?
>>
>> 这些工具与语言无关,能够完成任务。
> 因为我有数十位同事,每个人都使用不同的操作系统/发行版,正在开发一个应用程序,需要将依赖项的点版本锁定为生产环境中运行的版本
我假设生产环境不会运行数十种操作系统版本和发行版。对于开发,可以使用与生产环境相同操作系统版本和发行版的虚拟机或容器,并使用该操作系统的包格式来打包软件并在开发环境(容器或虚拟机)中安装。你是在测试软件是否能在生产环境中运行,而不是测试某人偏好的操作系统/发行版。
> 并同时更新它们
我对 apt 不太熟悉,但 dnf 具有版本锁定功能,可将依赖项锁定到特定版本。您可以在开发过程中更新并测试依赖项,然后在更新生产环境时修改版本锁定文件以拉取依赖项的更新版本。
对于上游发行版中缺少的、且我们需要确切版本的每个依赖项,我们都需要进行适当的打包。这里我们没有任何好处,没有人会为此付费。
一旦打包了依赖项,更新到新版本通常只需进行 minor 更改,除非是 major 版本变更。您获得的优势是能够轻松升级或降级特定依赖项,并验证已安装文件的完整性(据我所知,pip 并未提供此功能,但操作系统包管理器支持)。
这简直是海量的工作量,毕竟另一种选择是更新你的锁定文件,然后让所选语言的传递依赖项解析功能完成剩下的工作。需要说明的是,作为最终用户,我非常喜欢干净整洁的.deb包,如果我的客户群体是这类用户,我也会以这种方式发布。但如果我是链条末端的开发者,而我的客户是内部用户,我绝不会采取你提议的这种方式。
因为对于Windows和macOS(占终端用户电脑的97%)只需构建1到2个包,而对于主要Linux发行版(甚至不是全部)则需要构建数十个包
> 需要为主要Linux发行版构建数十个包 (甚至不是全部)
这是通常由每个Linux发行版的包维护者处理的事务,而非应用程序的开发者。一些开发者会维护自己构建的包的公共仓库,并指导最终用户将这些仓库添加到包管理器的配置中,但他们通常也会提供源代码存档(如果该软件是开源的)以及针对未构建包的发行版的构建说明。
以 Virtualbox[1] 为例,除了 Mac 和 Windows 之外,他们还为 Redhat、Ubuntu、Debian、OpenSUSE 和 Fedora 构建了包,并为运行没有预构建包的发行版的用户提供了源代码和构建说明[2]。事实上,最后一个选项是最灵活的,尽管它需要终端用户付出更多努力。
[1] https://www.virtualbox.org/wiki/Linux_Downloads
[2] https://www.virtualbox.org/wiki/Downloads
我刚刚查了一下,npm上有130万个包,PyPI上有60万个,NuGet上有10万个。虽然我确信其中大部分可能已经过时或无用,但这个数量级仍然比Linux发行版提供的包要大得多。(Ubuntu为6万个)
VirtualBox就是一个很好的例子:尽管它是Linux在某个领域(该领域Linux占据主导地位)的主要软件之一,
– 他们不得不自行分发Linux包——他们需要为Windows分发1个包,为macOS分发2个包,但为Linux分发12个包。
我不确定包维护者在决定哪些包应包含在操作系统仓库时会考虑哪些因素,但根据我对 Python 应用程序的经验,我们所需的大多数依赖项都可以在操作系统仓库或其他受支持的包仓库(如 RPM 的 epel 或 rpmfusion 仓库)中找到。我们无法找到的少数依赖项也不难打包并添加到我们的内部包仓库中。
但这同时也引发了对依赖项审核的担忧。如果引入的依赖项会牵扯到数十个其他依赖项(直接和间接),审核起来就会变得困难。PyPi和npm已经出现过恶意包被上传的问题。另一方面,我尚未发现操作系统包仓库中可用的 Python 包因依赖项数量过多而出现问题,且未听说过这些仓库发生过类似 PyPi 和 npm 的安全事件。
Linux 软件包维护者最不需要的就是将整个 PyPI 倾倒到他们身上进行打包和维护。
你有没有一个链接,指向一个携带自身依赖项并采用最先进的 Debian 打包技术的 Python 应用程序?
这取决于你对“最先进的”的定义,但使用dh-virtualenv是可能的:https://github.com/vincentbernat/pragmatic-debian-packages/t… (这无法成为官方包,因为它违反了Debian政策)
> 它自带依赖项
你是指链接到一个格式错误的.deb包吗?
虽然不难实现,但这不是正确的做法。
说实话,我更想做的事情比关心一些 Debian 包管理实践要多得多,我只是想分发一家公司的内部应用程序及其确切的依赖版本。但它必须正常工作,例如“虚拟环境的可移动标志一直处于实验阶段,从未真正工作过”;
因此,实际上我们又回到了使用 Docker 发布整个发行版(或者,像我们之前做的那样,放弃 Python 作为开发选项,但保留 .deb 包用于部署)。
不可能。Docker 并非为安全而设计。
嗯,它可能比使用 `pip install` 并让每个依赖项在主机上自行安装更安全……
实际上,如果你在生产环境中使用虚拟机,情况并非如此。
此外,即使你只依赖一个容器——仍然不行:其他容器引擎的攻击面更小。
我这里指的是本地开发环境。
值得注意的是,PEP 582(即 __pypackages__)模式曾是 PDM 的默认设置——并且曾是其核心功能——但在 2.0 版本发布时被改为可选功能,其解释是该 PEP 进展停滞,且编辑器和 IDE 对虚拟环境的支持已显著提升(https:// http://www.pythonbynight.com/blog/using-pdm-for-your-next-p…)。
如果你阅读了 Python 论坛上的讨论,反对 PEP 的一个论点是:“如果现有工具已经支持这一点,为什么我们需要一个 PEP 来实现它?”但假设一个 PEP 能够推动生态系统适应 __pypackages__,并解决上述问题(如更广泛的编辑器和 IDE 支持)。
就我个人而言(作为一名全职开发 Python 工具的人):我通常支持转向类似 node_modules 的模型。
这个工具论点相当薄弱。PDM是目前我能找到的唯一支持__pypackages__的工具,其使用的路径似乎与PEP描述略有不同,且依赖$PYTHONPATH/shell技巧(
pdm --pep582
会添加一个包含sitecustomize.py
的文件夹来实现该功能)。如何修改IDE(及其魔法功能)使用的$PYTHONPATH?我同意!
哦,另外,我认为我已经阅读了关于这个PEP的整个讨论线程,从我所能判断的,这个PEP缺乏愿意推动它向前发展的倡导者。
我猜问题的一部分在于,这个PEP处于一个奇怪的位置,因为它不需要PyPA来“批准”它,或者类似的事情——PEP是由Python指导委员会批准的。
根据Brett Cannon(指导委员会成员)的说法:
> 需要达成共识(但目前尚未达成),或者有人需要将此提交给指导委员会做出决定(尽管这是一个特殊的PEP,因为它还涉及打包,因此甚至不清楚谁将拥有最终决定权)。
https://discuss.python.org/t/pep-582-python-local-packages-d…
今天我正在准备一个简洁的项目,以便与他人分享。由于不确定对方已安装哪些工具,我查阅了 Python 的官方打包文档并进行了浏览。
其中一页默认使用“hatch”提供操作指南,而另一页则指出官方推荐使用 setuptools 配合 setup.cfg 文件,仅在 setup.py 中声明动态内容。与此同时,pyproject.toml 的支持正在其他地方推进,并且对许多 setup tools 功能的 beta 支持也在进行中。
有太多工具,而且关于哪些工具应该使用以及未来应该选择哪些工具,存在太多混淆。为什么我们不能像 Rust 一样,有一个工具可以构建、格式化、运行测试以及处理其他所有事情?
Pipenv 虽然糟糕,但不知何故获得了 PyCQA 的支持,而我甚至不确定 Poetry 是否在 Python 文档或 CQA 中被提及,但我一直在使用它,感觉很不错,但现在我甚至不确定自己选择它是否正确。
这种碎片化只会导致开发者困惑,无论是新手还是使用该语言超过10年的老手。
我的观点是,使用任何工具构建轮子并不难。你可以基本上按照指南操作,只需提供相同的信息,主要是格式不同。
问题是,编译扩展并非 Python 的次要用例,它们被广泛用于包中,即使用户并不经常使用它们。构建一个没有依赖关系的包仍然不难。但目前没有好的方法来分发你依赖的编译包作为独立的包——例如,Intel将MKL作为“Python”包发布,但它实际上只是C库[1]。当然,如果你是包维护者,你的依赖包维护者既将包上传到他们不使用的语言的包管理器,又恰好是你要的版本,这种可能性非常低。因此问题变成了“如何将可能需要的每一个依赖项都打包到一个 Wheel 中”,因为源代码安装对大多数 Python 用户来说并不熟悉。Conda 和 Spack 试图通过成为通用包管理器来解决这个问题,它们可以分发任何语言的包,让你在 C 扩展中依赖 FFTW、Eigen、SUNDIALS 或任何你需要的库,但在所有讨论中,我认为没有哪个提案真正解决了这个差距。
[1] https://pypi.org/project/mkl/
我有一个单一的、简单的脚本(不是包!),它有依赖项。实际上,我有几个这样的脚本,都放在/usr/local/bin目录下,这样我就可以随时执行它们。
我应该如何管理这些脚本的环境?我应该在共享系统Python中安装依赖项吗?我应该创建一个共享的虚拟环境吗?我应该把它存放在哪里?有没有工具可以帮你做出这个决定并管理它?
仅仅是因为Homebrew偶尔会更新我安装的Python并破坏一切,这让我对使用Python作为脚本语言感到犹豫。当我有一个专门用于项目的文件夹时(Poetry对此很适合),它相当可靠,但我对整个系统的脆弱性感到非常厌倦。
如果你不介意添加一个 pyproject.toml 文件,你可以使用 pipx[1] 来安装这些脚本。目录结构将如下所示:
pyproject.toml 文件将如下所示(使用 Poetry,但你可以使用其他工具来实现):
然后运行 `pipx install -e .`,可执行脚本将安装在 ~/.local/bin 目录下。
[1] https://pypa.github.io/pipx/
感谢您尝试提供一个可行的解决方案,它确实不错,但对我来说有一些缺点。pipx本身安装在Python环境中,因此当Brew破坏我的Python环境时,也会破坏pipx。每次Brew破坏我的Python环境时,我都需要为每个脚本重新执行安装步骤(或自己编写一个工具来完成此操作)。这并非完全不可接受,但与我当前的情况相比并没有好多少,因为我当前的情况基本上就是假设任何版本的`requests`或`fire`都是可接受的。由于Python本身会不断更新并破坏我机器上的基础Python环境,因此可行的解决方案需要考虑基础Python环境可能需要安装某些组件。
一个选项是将 Python 安装在专门用于此目的的独立位置,并且不将其包含在 PATH 中。这样它就不会被 brew 等包管理工具检测到。安装 Python + pipx 等完成后,甚至可以将整个位置设为只读。
这肯定可行。
我使用 `pip install –user pipx` 安装 pipx,但使用 Homebrew 时更好的选择是 `brew install pipx`。后者在执行 `brew upgrade` 时,应能保持 Python 和 pipx 的版本同步。
EDIT:再仔细想想,使用 `brew install pipx` 可以避免 pipx 在 Homebrew 升级时出现问题,但如果从 Python 3.N 升级到 3.N+1,你安装的脚本可能仍然会失败。
因此,我认为唯一的解决方案是保留所有使用的 Python 版本,假设你不想在升级 Python 时同时升级所有脚本。
我使用 pyenv 而不是 Homebrew 来管理 Python 版本,这似乎可以避免你遇到的问题,因为通过 pipx 安装的工具所使用的 Python 版本会一直保留,直到我明确卸载它。
令人惊讶的是,我刚发现另一篇探讨同一主题的首页帖子!对于 Python,它似乎建议使用 nix-shell。
1. https://dbohdan.com/scripts-with-dependencies
我的评论中附带了一个单文件脚本的示例,该脚本可以指定自己的依赖项:
https://news.ycombinator.com/item?id=34393630
将它们放在~/.local/bin目录下,并将其添加到系统路径中。然后使用pip install reqs并添加—user参数进行安装。
如果你想自动化这个过程,可以使用pipx,但这过于复杂且会失去简洁性。
人们之所以对简洁性发出严厉警告,是因为近年来系统管理员的技能水平大幅下降。对于罕见的冲突,只需将问题模块放入虚拟环境即可解决。除了大型工作项目外,我从未需要过。
如果你不想依赖系统或 brew 安装的 Python 运行所有脚本,可以使用 asdf-vm 管理多个 Python 环境。
—
在继续提出自己的建议之前,我需要声明:我曾为pip和pip-tools做出过小规模的代码贡献,并维护了一个基于Zsh的前端工具,用于管理pip-tools+venv,名为zpy。
—
建议:
– 每个代码文件夹都对应其默认的venv环境,并可映射到更多venv环境以支持不同Python运行时
– 每个代码文件夹都包含一个 requirements.in 文件,其中列出了顶级依赖项,以及一个作为锁定文件的 requirements.txt 文件
– 你可以为脚本添加一个 shebang 行,明确调用 venv 的 Python,并将该文件链接到 ~/.local/bin/,或者在 ~/.local/bin/ 中创建一个外部启动脚本。
—
以下是使用 zpy 函数实现的示例:
如果 Python 运行时更新破坏了您的虚拟环境,您可以使用 zpy 的 pipup 函数来修复问题。
我为每个脚本使用单独的目录和虚拟环境。要执行脚本,我使用 shell 脚本调用虚拟环境的 Python 解释器。这也是我使用 Python 脚本与 cron/systemd 配合的方式。
你也可以跳过 shell 脚本,直接在 .bashrc 中使用别名。
我做的事情有点像这样,但所有脚本都会在底层环境突然损坏时崩溃,例如 brew 在未经我同意的情况下更新 Python 并破坏所有现有环境。
我确信我可以为我的特定机器想出非常健壮的解决方案,但我希望有一个解决方案,可以让我将其作为 gist 共享,团队成员可以轻松使用,或者我可以在另一台机器上使用它而无需麻烦。换句话说,一个包含在脚本本身中的解决方案,以及一两个外部二进制文件,使它成为可能。
我明白了,虽然可能有点粗暴,但将它们运行在 Docker 容器中可能会提供你需要的隔离。你还可以构建并分享这些镜像给你的团队成员。
我实际上已经开始使用很多不同的 CLI 工具与 Docker,尤其是当工具不支持我的操作系统时。
我不明白为什么至今还没有一个好的解决方案。这似乎是一个显而易见的需求,多年来我一直听到Go被宣传为解决这个问题。我明白为什么Python一开始没有提供解决方案,但在20TB硬盘的时代,我愿意牺牲一些空间来换取安装的便利性。
尝试在 Nix 环境下构建 Python(即严格遵守规范,而非“差不多就行”)会让你深刻体会到 Python 包管理生态系统的混乱程度。
这实在令人遗憾,因为 Python 是一门卓越的脚本语言,是事实上的数值计算标准,而且在大部分 Bash 应用场景下,它可能比 Bash 更胜一筹。
Python打包的一个问题是,Python核心开发者将“Python打包”和“打包Python”视为两件不同的事情,但大多数人并不在意(或无法区分)两者的区别 🙂
我必须承认,我已经放弃了所有这些工具。相反,我只是做
然后在脚本开头添加
有人会说这很傻,但它就是管用。记得将 .lib 添加到 .gitignore 中。否则你会遇到很多麻烦。
作为实现 -t 标志并了解其可怕的边缘案例的人,我只能说,祝你好运。
我很想知道这些边缘案例是什么,因为我也想尝试使用 -t 标志。
请在挖自己的坑之前详细说明一下,谢谢 😀
我查了一下,似乎它无法正确处理命名空间包。由于添加它们到路径的方式,导致它们在路径中靠后,因此可能出现因路径中较早的包被优先使用而无法找到的情况。
我稍微调整了一下,挺有意思的。但使用pip list或pip freeze时,它们不会显示.lib目录下的包。你需要手动将它们添加到requirements.txt文件中吗?
我手动将它们添加到 requirements.txt 中,并尽量保持列表尽可能简短。虽然这使得项目无法实现 100% 可重复构建,但它迫使我仔细考虑每个依赖项,并且我可以通过 Git 日志查看每个依赖项被添加的原因。
有趣的是“pip help list”或“pip help freeze”的用法。 🙂 我发现可以添加–path .lib参数来列出/冻结这些依赖项。虽然多了一个步骤,但并不算太麻烦。
这与Node.js的做法类似。我基本上是借鉴了这个思路。
那么我们都应该使用PDM吗?因为它是目前所有选项中最好的?我以前在某个地方读到过,但从未使用过。我一直使用
venv + pip
,对我来说效果很好。最近,我时隔数年重新投入Python开发。在我的第一个项目[1]中,我基于一个已使用Poetry的现有库进行开发,因此自然选择了Poetry。尽管学习曲线有点陡峭,但我很快适应了它,但仍好奇它为何会出现,毕竟我记得其他工具(如virtualenv等)已经“足够好”。
最近,我不得不处理一个缺乏任何运行说明的项目,该项目包含一个 setup.py 文件、一个 Pipfile 以及其他内容。在尝试让它运行时,我搞得一团糟,以至于(无疑是因为我对这些工具缺乏经验)最终不得不删除所有虚拟环境,因为它们都无法正常工作……
因此,我如今完全支持“一站式工具”的理念——尽管PDM看起来很有前景,但目前它并未提供比Poetry更让我在意的新功能。
正如卡托所言,PyPA必须被消除。
1. http://github.com/grafana/pySigma-backend-loki
> 在Node世界中,处理包的工具有两种,即npm和Yarn。
更不用说pnpm、Bun或甚至Deno了。所有这些工具都处理包,并且做出不同的选择。
这并非吹毛求疵,但我认为这是一个合理的例子,说明为什么不必执着于存在唯一一种方式来完成某件事,甚至不必执着于使用单一工具而非多种替代方案;相反,我关心的是无需将数十种工具串联起来才能完成某件事,并且这些不同替代方案之间的 API 存在一定程度的兼容性和标准化。
嗯,不。JS包生态系统比Python生态系统稍好一些,但仅此而已。我认为以此为基础进行任何重构都是一个糟糕的例子。至少作者提到了.NET,它在构建和打包方面基本上做得不错。
我们还需要为用Python编写的桌面应用程序提供应用程序分发功能。我需要能够将Python应用程序打包成单一二进制文件。有像pyoxidizer https://pyoxidizer.readthedocs.io/en/stable/这样的工具。希望它能成为Python社区的标准。
你可能已经知道这一点,但 Nuitka[0] 对于构建可分发的 Python 应用程序非常出色。
[0]: https://nuitka.net
请注意,这篇文章长达8700字,它并没有深入探讨在Python上构建包有多糟糕;主要是关于使用它们有多糟糕。这是一篇优秀的文章,长度是必要的。我猜一篇关于创建包的文章需要两倍的长度才能达到同样的细节水平。
我并不认同文章中的所有观点,但我确实认同创建包(以及以可重复/可扩展的方式进行)存在一定的学习深度。我撰写了一本关于创建 Python 包的书籍,该书刚刚出版:https://pypackages.com
即使这本书也没有涵盖每个领域的所有选项,而且几乎完全跳过了conda,因为我没有使用它的个人经验。conda和科学界的工作为包的创建和消费两方面都增加了复杂性,而这正是我认为这篇文章在考虑“一刀切”解决方案在实践中如何运作时,可能没有涵盖所有细节的领域。
阅读这篇文章真的令人沮丧。我只是想要 Python 的 Cargo。目前只能使用 Poetry,但它有诸多怪癖,而且速度极慢……
很多人表示他们想要 Python 的 Cargo,但当要求他们使用 `cargo run` 运行程序时,他们立即退缩。他们希望
python myscript.py
仍然能神奇地工作,但正是这种神奇之处恰恰来自于此。直接运行python
就像手动调用rustc
(或者稍微自动化一点,比如一个make脚本);它确实能工作,但处于完全不同的抽象层级,与Cargo的抽象层级并不匹配。我并不是在暗示你就是那种人,但这或许能解释为什么“Cargo for Python”并未成为更广泛采用的工作流程。
这很有趣,因为诗歌的运作方式是“诗歌运行Python……”
回到正题,“Cargo for Python”应该成为官方标准,且是唯一的(没有竞争标准),有明确立场且快速(tm)。
如果这是大多数用户的期望,那么为什么`python foo.py`不能实现与`cargo run`相同的功能?
大多数工作流工具至少支持两种执行上下文,一种用于开发,另一种用于部署(如
cargo run
与直接执行编译后的二进制文件,npm run
与node foo.js
等)。分离的原因包括隐藏的构建步骤或设置运行时环境,通常在开发过程中希望某些操作自动化,但在程序部署时则不需要。更准确地说,python foo.py
无法替代cargo run
同时保持其当前功能;显然可以将其修改为在开发上下文中运行程序。问题在于它目前在多个上下文中被使用,而没有人希望自己的工作流程因此中断。我远离 Python 的主要原因。打包是一团糟!
这也是我离开它的原因之一!我关于这个主题的博客文章发布在这里,尽管它提出了类似的观点并进行了与 Node 的比较,但反响并不热烈。
Python并没有什么特别之处,值得忍受这种痛苦。我们有其他语言提供更好的开发者体验。
嗯,对于机器学习来说,情况并非如此。Python已经占据了主导地位。如果你计划从事核心算法开发或现有模型的微调,别无选择。
如果你有链接,我很乐意阅读你的博客文章?
https://cedwards.xyz/breaking-up-with-python/
请注意,这篇文章并非为读者群体撰写,更不用说为HN读者群体了。
感谢分享链接,我很快就会回来找你理论 😀
…读完后,我唯一想说的是“我完全同意你的痛点”。尤其是关于文档和包管理的部分。
我喜欢使用类型注释来提升 Python 代码库的可读性,即使你不使用类型检查器,但要在一个拥有大量现有代码库的组织中,让人们在新代码中一致使用类型注释,需要大量激励措施,或者基于 CI 的强制措施,而我目前还没有足够的组织影响力来直接推行这些措施… …至少现在还没有。
我认为他们不愿意改变是因为这会冒犯现有项目维护者的工作。如果维护所有打包软件的代价是牺牲功能性,那么Python打包系统就完了。
在前一家公司,我有机会与多个团队的Python开发者坐下来交流,其中一个有趣的问题是:假设Python 4是另一个规模相当于2到3的大型破坏性变更,什么功能让它值得你接受?
最常见且热情的回答是:打包和分发。
我认为说“大量Python开发者认为打包功能糟糕且需要修复”低估了当前的舆论。我认为有大量身处一线的Python开发者对这个问题已感到“彻底厌倦”,愿意接受导致不可忽视痛苦的极端措施,只要能解决问题。换句话说,人们正变得相当绝望和不满。
上游需要进行一些自我反思,并认识到他们所固守的任何阻碍解决方案出现的意识形态,很可能与现实需求脱节。
那么上游指的是谁?Pypa?Python指导委员会?Python本身在没有Pypa批准的情况下不会做太多事情。Pypa不会支持任何现有或新的工具,即使这些工具能很好地解决大多数主流场景。
似乎大多数人都认为Python的打包机制并不理想,但反过来,有没有一种语言,大多数人都认为其打包方法非常出色?我的意思是,我们应该以什么作为标杆来追求?
我不确定这是否算数,但在我看来,Java做得相当不错。一个JAR文件可以包含运行应用程序所需的所有内容,或作为库进行分发,只要安装了java命令,它就能正常工作。
同样,WAR文件使在运行时添加/删除/更新应用程序变得异常简单。
当然总有特殊情况,但在我看来,Java世界中的打包和分发一直都很轻松。
Python 同样具备类似的分发功能,因为它可以直接从 .zip 文件加载代码,并将这些文件视为目录——这与 JAR 文件非常相似。问题在于获取和管理这些依赖项的阶段。
加入一家使用 Python 代码库的公司后,我真怀念 Java 啊。
以一个例子来说,仓库中的所有包都进行了命名空间划分,且命名空间所有权会经过验证。[0]
因此,不会出现因拼写错误而占用现有包的情况,也不必担心有人抢先占用所有“好”的名称。
[0]: https://central.sonatype.org/publish/#individual-projects-op…
我认为这就是统一成功的样子:
假设有两个工具,一个叫pyup,一个叫pygo。
我必须不需要基础 Python 安装来实现上述任何功能。工具会不断修改系统 Python 并影响我的路径设置,因此我无法信任系统 Python,无论如何。pyup 和 pygo 应该作为独立的二进制文件调用 Python。示例.py:
当我第一次运行 ./example.py 时:
当我第二次运行 ./example.py 时,脚本正常运行且无错误。
如果我仍然需要在该基础上使用虚拟环境(如 virtualenv、poetry 或 conda),那么统一项目已失败。
Gentoo 的系统某种程度上兼具这两种特性。例如,你可以控制哪些包安装在哪个 Python 版本上,跨版本的依赖关系以及系统默认版本。不过它并未直接使用 PyPI 生态系统,因此所有内容都必须单独打包才能正常工作。
我认为C#/.NET生态系统中没有太多抱怨。当然,有替代工具(如F#的FAKE),但我相信大多数人对dotnet.exe/msbuild/nuget感到满意。
我认为 Go(如果你是从 `go mod` 时代开始使用它)和 Rust 在包管理方面有最好的实践。
`go mod` 的界面有点令人困惑,但我对它生成的依赖关系图有实际的信任。Cargo 似乎在界面和对底层运作的信任方面都做得很好。
在 Python 生态中,Poetry 表现尚可。与 `go mod` 或 Cargo 相比,它的速度确实很慢,但我通常能理解并信任其内部运作机制,且其界面对于新手来说也相对易于理解。
我是 Elixir 中的 `mix` 的忠实粉丝。https://hexdocs.pm/mix/Mix.html
没错。Mix 基本上是 Leiningen 和 Bundler 的混合体(双关语无意)。它运行良好。
这部分得益于Erlang在发布阶段解决了大量问题,而Hex团队则处理了剩下的部分。
但总体而言,“Yehuda Katz” 系(Bundler、Cargo、Yarn v1、mix,因为José曾受Yehuda指导)拥有值得借鉴的优秀工具。至少可以作为基础。
Ruby gems 和 Cargo 非常出色。此外,我发现我非常喜欢 Arch Linux 的 Pacman,尽管这是一种略微不同的用例,其中版本控制并未解决。
Cargo 是我认为的黄金标准,但它确实做了一些简化决策,可能不适合 Python:
– 依赖项和可执行文件在构建过程中可以执行任意代码。例如,Cargo 对如何构建 C 代码几乎一无所知,常见的工作流程是引入流行的 `cc` 库来处理此任务,并在你的 build.rs 中调用它。
– 基本上不存在“安装库”这样的事情,每个项目都从源代码构建其依赖项。这在 Cargo 级别(install 命令对库 crates 不起作用)和语言级别(除了“extern C”之外没有稳定的 ABI)都得到了体现。
– 与之相关的是,Cargo 工作流的最终产物是一个主要静态链接的二进制文件,因此没有与 virtualenv 相当的等价物。
> – 依赖项和可执行文件可以在构建过程中执行任意代码。例如,Cargo 对如何构建 C 代码几乎一无所知,常见的工作流程是引入流行的 `cc` 库来处理此任务,并在 build.rs 中调用它。
Python 打包涉及任意代码执行,即使在今天也是如此。虽然与 `setup.py` 相比,这一点不太明显,但如果你从源代码安装,包可以指定任何构建后端,而该构建后端可以执行任何操作。这可以通过建立一个构建后端的允许列表来缓解,但谁来负责审核这些工具,如何保证被允许的工具不会被恶意方接管,以及如何确保被允许的工具没有运行任意包提供的代码的机制?
我认为最终构建输出静态链接与虚拟环境无关。我只使用过 Python 的虚拟环境来避免全局安装依赖项。使用 Cargo 时无需担心这一点,因为其数据已按项目保存,且 Cargo 在执行命令时可自行处理。
R与CRAN。使用renv确保可重复性
Python.org网站上明显缺失的是针对典型紧凑但循环的Python开发迭代流程的分步操作指南,该指南应包含标准化的打包方法。
https://chriswarrick.com/blog/2023/01/15/how-to-improve-pyth…
这充分说明了 Node/JS/TS 的 NPM 相比 Python 的混乱局面要好得多。使用任何其他 JS/TS 开源项目都容易上手。即使你使用 Python 的 Poetry,你也会遇到其他人使用 pip-tools、pipenv、pdm、conda 等工具。
很高兴看到这个话题被讨论。我讨厌虚拟环境,原因正如文章中所提到的——需要额外跟踪状态,而且如果我没有跟踪好,可能会将包安装到错误的位置。
我总是强迫我的用户包正常工作。我通常只使用numpy/scipy/matplotlib,偶尔使用其他几个,所以这并不难,但某种类似npm的体验会很受欢迎。我知道很多人都在这些环境中挣扎。
作为一个几乎没有npm使用经验的人,你希望在venv或Python的包管理中看到npm的哪些优势?
我认为这篇文章描述得相当到位,它实际上突出了几个我之前不熟悉的npm功能。但就我自己的经验而言,npm将所有需要的包安装到项目本地node_modules文件夹中非常方便。这消除了对venv的需求。
我主要是希望看到venv被淘汰。我真的不喜欢它所需的工作流程。
在使用__pypackages__时无法排除系统包的解析,这是一个相当明显的疏漏,这将迫使人们继续使用venv直到问题得到解决。
conda 简直是个噩梦;它安装了大量软件包,甚至没有询问我是否需要它们。当我想要安装不同版本的 Python 时,它不断进行冲突解决,持续了数小时,最终我厌倦了并终止了它。不如直接使用 virtualenv 通过 requirements.txt 进行传统设置。
处理所有这些问题是我选择使用 Go 语言编写 CLI 工具的原因(尽管我对它的错误处理模板并不太喜欢);静态类型化加上生成单一二进制文件,无需任何环境设置即可运行。我了解各种 Python 工具也可以生成二进制文件,但它们有自己的特殊情况,而直接开箱即用且无需依赖项会更方便。
> 对于其他语言的工具,你也可能发现其不足之处。有些人认为Maven很糟糕,因为它使用XML,而Gradle才是正确的选择;另一些人则认为Gradle使用基于Groovy的DSL让事情变得比必要复杂,因此更倾向于使用Maven。
是的,但我从未遇到过像PIP或类似工具在使用Maven安装包时出现安装失败的情况。最坏的情况是,如果仓库配置不正确,它可能无法找到该包。
尽管这篇文章在某些方面存在误导,但其最大亮点在于详细比较了不同Python打包工具功能的表格。
一个缺失的细节是PDM支持不同构建后端的能力,这使得一些有趣的功能成为可能:例如,使用hatchling作为后端,可以利用hatch对动态版本化的支持,而PDM本身并不具备这一功能。我还没有尝试过,但如果通过使用setuptools作为后端来支持PDM的C扩展,我不会感到惊讶…
其中一个误导性观点是:
> 值得注意的是,PEP 20《Python 哲学》中提到:
> 应该有一种——而且最好是唯一一种——显而易见的方式来实现它。
> Python打包显然没有遵循这一点。有14种方式,没有一种是显而易见的或唯一好的。总之,这是一个无法挽救的混乱。为什么Python不能选择一个工具?
关于这一点,有几点评论:
1. PEP 20被称为“Python的禅意”,而不是“Python打包的禅意”,是有原因的。
1a. 即使它适用于生态系统而不仅仅是语言本身——PEP 20 是一份指南,而非神圣的法则。
2. 确实存在一种显而易见的方法,就在 Python 文档中。[0] 虽然它列出了多种不同的工具,但“实现某种功能的方法”与“实现该功能的工具”是两个完全不同的概念。
[0] https://packaging.python.org/en/latest/tutorials/packaging-p…
> 为什么 Python 不能选择一个工具?
有时我怀疑开发者只是出于个人利益创建新工具,他们希望自己的名字出现在某个知名项目中,因此不愿意与现有项目合作来实现他们的想法,并推动整个社区朝着一个共同的工作模式发展。
> 拥有多个竞争解决方案有什么害处?
开发者困惑和社区分裂。
我是 pigar[1] 的作者,我大量使用 Go 语言,Go 也有其问题,但我喜欢 `import “url”` 风格的导入语句,开发者可以先编写代码,然后使用 `go mod tidy` 同步依赖项。
要解决 Python 世界的的问题,Python 社区应简化工具并培养先声明依赖关系的习惯(这可能应强制执行)。
[1]: https://github.com/damnever/pigar
即使在撰写关于 Node.js 在处理包方面比 Python 优秀得多的内容时,我也忍不住要吐槽 Node.js,哈哈:
> 让我们尝试移除 is-odd 来演示这个包设计得多么糟糕:
你直接删除了 is-even 对 is-odd 的依赖,然后还敢惊讶它崩溃了?
对Node.js的“小包哲学”有许多批评,但这也是一个巨大的优势,很可能是JavaScript取得如此成功的原因之一:非常明确的小功能,做的事情一目了然。与其通过复制粘贴is-even并维护两个版本来重复自己,不如通过组合功能、基于现有内容进行构建,这完全合乎逻辑。当包的范围明确限定时,更容易理解其作用范围及功能边界。
这里同样存在对现有机制的强烈反对,但这种设计自有其合理性。除极少数例外情况(如left-pad引发的故意破坏行为)外,它通常运行良好。唯一例外是,管理包库确实颇具挑战。
我可能有偏见,但我确实更喜欢 Python 的包管理方式,而非 Node。即使是普通的虚拟环境也是如此。我无法告诉你有多少次删除 Node 包目录并重新安装就能解决奇怪的问题,但在 Python 方面,这种情况对我来说非常罕见。
此外,不得不翻阅大量质量参差不齐的 Node 包来寻找某种标准方法的情况发生得过于频繁。比如比较 Python 的 requests 和 axios。
我认为 Node 的包管理已经远远超越了 Python,甚至到了令人尴尬的地步。由于它相对较新,因此能够迅速采用最佳实践:
声明式包清单。Python 的生态系统仍然是一团糟,各种方法混杂在一起,而你必须运行
setup.py
脚本才能确定依赖关系,这简直是一场噩梦。正因如此,在 Node 中运行依赖关系解析和安装的速度比在 Python 中快一个数量级。简单到不能再简单的隔离环境:所有内容都位于 `node_modules` 目录中。你只需在项目目录中盲目运行 `npm install`,就不会出错。而在 Python 中,你需要自行管理虚拟环境,这本质上涉及路径操作和符号链接,且在切换环境时必须记得取消这些操作。虚拟环境的命名也没有默认规则,因此你需要自行确定标准并将其添加到 gitignore 中。每次运行 `pip install` 时,你都必须确认自己处于正确的环境中,否则可能会破坏错误的环境(甚至全局环境)
3. 开箱即用的全面锁定文件支持。调试 Python 依赖问题是一场噩梦。几乎没有办法在不使用第三方工具(如 pipdeptree)的情况下弄清楚为什么某个依赖项被安装。在 Node 中,只需运行 `npm install` 即可自动生成一个正确的锁定文件。
我从事全栈开发,两者的差异犹如天壤之别。在 Node 中,我几乎不用考虑依赖项管理。
坦率地说,这读起来像是你在使用过时的 Python 工具。
试试 Poetry [1],它具备你在这里列出的所有功能。正如 Node.js 在过去五年中取得了长足进步,Python 也是如此。尽管如此,Python 的发展方式要分散得多,这可能对 Python 来说是个缺点。
[1]: https://python-poetry.org/history/#100—2019-12-12,于2019年12月发布1.0版本。
哦,我用过Poetry。我指的是Python的原生环境。依赖管理生态系统非常碎片化,依赖于第三方工具自行制定标准,即便如此,它们也无法克服根本性限制。例如,Poetry很棒,但锁定/安装速度仍远慢于Node,因为缺乏声明式清单文件。
Poetry无法提供生态系统中不存在(或不总是存在)的东西,比如正确声明的依赖项[1],而这在其他大多数包管理器中是必不可少的。
如果你的项目依赖于大量仅使用
setup.py
定义依赖项的包,Poetry可能会变得异常缓慢。[1] https://python-poetry.org/docs/faq/#why-is-the-dependency-re…
我之前尝试过使用 venv,但终端(Iterm)会出现故障。
小型包有其存在的价值,但 is-even/is-odd 作为包来说过于简单。直接在代码中写入
x % 2 === 0
显然更简单,这是一种常见的编程惯例,而非安装并导入一个独立的包来实现。is-even使用is-odd可能会让用户感到困惑。例如,你可能调用isEven(0.5)并得到以下错误:RangeError: is-odd 期望一个整数。在 isOdd (/tmp/mynodeproject/node_modules/is-odd/index.js:17:11) 在 isEven (/tmp/mynodeproject/node_modules/is-even/index.js:13:11) 在 Object. <anonymous> (/tmp/mynodeproject/index.js:3:13)
(但演示的主要目的是展示依赖项解析以及它在何处查找包。)
isEven 出现在堆栈跟踪中——这不应让任何具备基础编程入门水平的人感到困惑。
它是否太小了?如果未来语言演进到支持 BigInt,我们会面临一堆未升级的库,每次都得费劲查找吗?
我认为关键在于认识到这完全是个人观点。许多人不喜欢有太多选择,也不喜欢依赖项增长得太快。这没问题,不断膨胀的包树确实带来了一些真正的痛苦。但有一种自负的态度,我认为这种态度经常出现,即我们嘲笑并贬低像 is-even 这样的包。但对我来说,这不是绝对的,而是口味和偏好的问题。对局外人来说这可能看起来很奇怪,但它实际上极为强大且有用,是 JavaScript 成功的重要组成部分,正是因为 npm 的出现,使得包管理和包发布变得容易,我们才养成了捕捉所有这些小而有用的事物并使其可用的行为。
也许将简单的东西内联是有好处的,但我不清楚这样做的真正好处是什么,或者 is-even 哪里出了问题。
进一步强调,认为小型模块是坏事的观念实在太过自大。而且没有证据支持。人们就是喜欢憎恨。黑暗面容易且方便,触手可及,通过贬低他人可以获得社交影响力,感到自己高人一等。去你的。
这并不容易,也不温顺,但目前尚不清楚除了轻微的不便之外,还产生了哪些负面影响。而其中大部分危害可以通过更合理的保护措施来缓解,而非简单地让所有模块访问一切。像WASI这样的系统终于开始通过内置保护机制来合理降低导入风险;这是运行时环境的缺陷,而非我们蓬勃发展的包生态系统本身存在问题。
目前仍不清楚哪些抗议是有价值的。
如果主要 Node 模块能花点时间重新发明轮子,那就太好了。
在我使用它的时候,我真的很欣赏 Hapi 相比于 Webpack 等工具,为 Node 模块添加的冗余代码非常少。
显然,两者解决的问题有天壤之别,但仍然……
每次有人问我对 Python 包管理有什么看法,我都会给他们看这个链接。
保存了。感谢分享!
关于系统包升级破坏虚拟环境的问题:硬链接能否解决?如果系统包管理器删除了某个文件,你的虚拟环境仍通过硬链接指向该文件,因此不会察觉变化。
这也会阻止你的虚拟环境中的 Python 在系统范围内的 Python 安装时收到任何更新。到那时,可能更容易直接使用你自己的 Python 安装,例如使用 pyenv。
当然,但安装新副本会抵消空间节省。此操作的目的是在不浪费过多空间的情况下实现预期效果。我并非单纯为了提升易用性而优化。
换句话说,我的建议是将符号链接迁移为硬链接。
是时候让GvR骑上BDFL小马进行(最后一次?)骑行了?
很高兴看到dotnet CLI被提及。这个工具确实让跨平台编译变得轻松。在Python中,我通常会放弃并转而使用Docker。
虽然这篇文章很有趣,但至少没有提到Nix,而我认为Nix解决了文中描述的许多痛点。
我感觉作者在维护 webpack 代码库的包时,没有经历过足够的痛苦。
我使用 pdm 至少有一年了,没有任何遗憾:它是 Go 工具,我们应该纷纷转向它!
我将站点用作可组合的迷你虚拟环境,但目前没有相应的工具支持
请允许我吐槽一下Python打包。
我正在调试一个非常奇怪的错误,该错误仅在我的应用通过`warnings.py`触发警告时发生。
我的代码导入了numpy,而numpy又试图导入自己的`warnings.py`,但由于Python环境优先级的问题,它加载了我的`warnings.py`,而这个文件不幸地与numpy的同名。
这种情况是如何通过Python的导入/包系统设计的?为什么导入的模块没有唯一的标识符?
简而言之:一个导入的包因模块名称冲突而依赖于我自己的代码,导致了意外行为。
warnings.py
是如何与其他代码配合工作的?我对项目中警告/错误的处理方式感到困惑,因为我把自己逼入了死胡同。顺便说一下,我认为处理这些问题有既定规范,但 numpy 似乎并未采用。如果我记得没错,任何以 __ 开头的模块都会被命名为 {package}.__{module}。
直接效仿 Go 的做法。
Python打包是一个已解决的问题:
https://python-poetry.org/
作为Poetry用户,嗯……不。哈哈
Poetry是较好的选择之一,但其非标准的pyproject.toml并不理想。PDM基本上就是Poetry,但采用了标准的元数据规范,并支持__pypackages__。
我想补充的是,PDM还支持多个依赖项组,且开发者修复问题速度极快。
如果你不得不与不使用 Poetry 的代码库合作(这种情况相当常见),问题就没解决。目前有 14 种工具可供选择,这些工具短期内不会消失。
Poetry 是 Python 打包领域最糟糕的现代解决方案之一,建议选择 Hatch 或 PDM,它们不仅功能更丰富且实现更完善,且代码库也并非一团糟。
是的,打包应该有一种统一的方式。