当我们编写了一个 Python 模块后,可能希望别人也能复用这个 Python 模块,因此我们会对这个 Python 模块进行打包发布。而对于其他想要使用这个 Python 模块的开发者而言,则需要下载安装这个 Python 包。整个过程涉及到 Python 的包管理机制(python distribution)。这篇文章是对 Python 包管理的一个简单总结,目的是对 Python 的包管理机制有一个较为清晰的理解。
Python 包管理工具一览
目前 Python 的包管理机制大致有如下工具:
- distutils:python 标准库的一部分,目前仍然是 Python 打包的标准工具之一。它可以用于简单的 Python 包发布,但是由于其特性不够丰富,目前已经被 setuptools 所取代。在 setup.py 可以使用 distutils 包
- setuptools:可以认为是 distutils 的增强版本,改进了 distutils 的各种限制。它提供了 easy_install 命令行工具、setuptools Python 包(可以在 setup.py 脚本中使用)、pkg_resources Python 包(可以在代码中用于检索 python distribution 中的数据文件)
- distribute:是 setuptools 的一个分支版本,已经 merge 回 setuptools 0.7 了,因此不再需要 distribute 了
- distutils2:其打算尝吸收 distutils, setuptools 和 distribute 的优点,然后成为 Python 标准库的一部分,但是这个目标并没有实现,目前已经是一个废弃项目
目前 Python 官方已经推荐使用 setuptools 来创建 python distribution,尽管 setuptools 并不是 Python 标准库的一部分。
Source Distribution 以及 Built Distribution
Python distribution
其实就是一个版本化的压缩文件,它包含了你的 Python 包。目前主要由两种 distribution:
- Source Distributions:通常称为源码发行版,简写为
sdist
。sdist 文件包含 Python 包的源代码(.py 文件,二进制模块的 .c/.cpp 文件)、数据文件、构建指令、但是不包含平台相关的二进制交付件。在包的使用端,才会针对相应的目标平台执行构建过程,从而得到一个 bdist。sdist 的优点是:创建出来的 sdist 可以在所有平台使用,缺点则是用户在下载 sdist 后需要自行构建 - Built Distributions:通常称为二进制发行版,简写为
bdist
,bdist distribution
中可能会包含.so
、.ddl
等二进制模块,因此它是平台相关的。bdist
的优点是可以在用户侧迅速安装,缺点则是需要为多个平台、版本提供不同的bdist
sdist 和 bdist 的本质区别是包的编译/构建发生在哪里:
- 对于 sdist,在客户侧上完成编译/构建
- 对于 bdist,在开发者机器上执行编译/构建
开发者通过运行 python setup.py sdist
生成源码发行版,通过 twine 将发行版上传到 PyPI(之前可以使用 python setup.py upload
,目前已经被废弃)。
即使 distribution 是纯 python 软件包,安装 sdist
也会在客户侧触发构建过程:从 setup.py
中生成安装元数据,然后安装软件包。而对于 bdist
包,安装过程则不会触发 构建
步骤。为了支持 bdist
(即在用户侧安装 Python 包时无需构建或编译过程),Python 提供了两种包格式:
- Egg 格式:2004 年由 setuptools 引入。类似于 Java 中的 jar 包,
.egg
是一个 Python 包的特定版本,其中包含了代码、资源文件、元数据文件。.egg
文件本质上也是zip
文件。使用easy_install
可以安装.egg
包。 - Wheel 格式:2012 年由 PEP 427 引入。Wheel 也是目前 Python 二进制发行版的标准。使用
pip
可以安装wheel
包。
关于 Egg 和 Wheel 的区别,可以参考这篇文章。
包安装
pip 已经成为了 Python 包管理工具事实上的标准,它被用来作为 easy_install 的替代品,但是它的大量功能仍然是基于 setuptools 组件开发的。相比于之前 setuptools,pip 提供了更多的功能:
- 在安装之前,所有依赖包都会自动下载
- 目前
Built Distribution
都是以wheel
(.whl)的形式发布。pip 工具可以处理 wheels,而 easy_install 不能 - 对版本控制系统的原生支持
- 可以非常简单地定义包的依赖,以及利用 requirements.txt 来快速安装依赖列表,从而更加方便地复制环境
- 提供对包的卸载功能
- 输出的错误消息更加实用
pip 和 easy_install 的主要区别可以参考这篇文章。
当安装一个源发行版时,pip 主要做了一下工作:
- 找到这个包
- 下载该包的 sdist 并解压
- 在文件夹内运行
python setup.py install
进行构建 + 安装
包安装后,软件包会生成两种类型的内容,以放入 site-packages 目录内:
- 有关软件包元数据文件夹,
Wheel
包使用.dist-info
目录,.egg
包使用.egg-info
目录 - 业务逻辑文件
pyproject.toml
在执行构建过程,需要思考一个问题,构建器本身也是有依赖的,为了解决由于构建器依赖所带来的的问题,PEP517 引入了 pyproject.toml
配置文件,其定义了构建操作所需要的内容。
有了 pyproject.toml
后,python setup.py install
现在可以:
- 创建临时文件夹
- 创建隔离环境(python -m virtualenv our_build_env)
- 在隔离环境中安装构建依赖项
- 通过执行
python_isolated setup.py bdist_wheel
生成用于安装的 wheel - 提取 wheel 到 Python 的 site packages 文件夹
在开发者机器上生成源发行版或 wheel 时,也可以执行同样的过程。
为了不把所有东西都绑定到 setuptools 和 distutils 上,PEP518 将构建器分成后端和前端。此时不再通过 setup.py 或命令与后端通信,而是使用 Python 模块和函数,所有后端都必须提供一个 Python 对象 API,至少实现 build_wheel 和 build_dist 两个方法,该 API 对象也是通过 pyproject.toml 提供的。此时执行构建的后端不再要求必须是 setuptools 了。
总结
下图简单地总结了整个 Python 打包的过程:
- Python 在工作目录下编写 Python 代码
- 通过 setuptools 工具将源码树进行打包
- 使用 twine 工具上传软件包到 PyPI
- 软件包的使用者通过 pip 下载安装软件包,根据软件包的类型(sdist or bdist)执行不同的安装过程(是否有构建过程)
- 软件包最终安装在 site-packages 文件夹内
Reference
Python 打包指南
distutils/setuptools/distribute的区别
Python Packaging: sdist vs bdist
setuptools/distribute/pip的对比
Python 打包现状:包的三种类型
Python 打包——过去、现在与未来