0%

Python 包管理小结

当我们编写了一个 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:通常称为二进制发行版,简写为 bdistbdist 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 打包——过去、现在与未来