028-86261949

当前位置:首页 > 学科资讯 > Python 打包的现状:包的三种类型

Python 打包的现状:包的三种类型

2020/01/04 17:52 分类: 学科资讯 浏览:1

19.0 已经于 2019 年 1 月 22 日发布。在其功能列表中,最值得注意的是它现在支持 PEP-517,默认情况下是支持的,如果项目的根目录中有一个 pyproject.toml。该 PEP 于 2015 年创建,并于 2017 年被接受。尽管 pip 花了一段时间才实现它,但该版本及其后续问题却表明,很多人根本不熟悉它。

 

如果你想了解 Python 打包(packaging)生态的现状及将来如何演变,请继续阅读。我们希望,即使上述提到的 Python 增强提案,如今可能会引起一些不愉快,但从长远来看,我们将从中受益。

 

我大约在三年前加入了 Python 开源社区(尽管使用它已有 8 年之久)。从早期开始,我就听说 Python 打包有一点黑匣子的名声。它有很多未知的内容,人们通常只复制其它项目的构建配置文件,就使用上了。

 

在尝试更好地理解这个黑匣子,并对其进行改进的过程中,我已经成为了 virtualenv 和 tox 项目的维护者,偶尔也为 setuptools 和 pip 做些贡献。

 

我希望对这个主题进行详尽的(并希望是一个较高水平的)论述,并决定将其分为三个部分。在这第一篇文章中,我将对 Python 打包的工作方式及其所具有的打包类型进行大概介绍。在第二篇文章中,我将详细地介绍软件包的安装方式,以及 PEP-517/518 是如何尝试对其进行改进的。最后,我再专门写另一篇文章,以介绍在引入这些改进时,我们吸取的一些痛苦的教训。

 

事先声明,我将主要关注 Python 官方的打包系统(即 pip、setuptools,因此没有 conda 或特定于操作系统的打包程序)。

Marcus Cramer 摄/Unsplash--人们第一次凝视 Python 打包时的脸

 

一个示例项目

为了讲这个故事,我需要先讲讲如何分发 Python 软件包的故事;更具体地说,包的安装在过去是如何运作的,以及我们希望它在将来如何运作。

为了有一个具体的示例,让我介绍一下我的很棒的示例库:pugs 。这个库相当简单:它只生成一个名为 pugs 的包,仅包含一个名为 logic 的模块。关于 pugs,你猜对了,logic 被用于生成随机的引号。这是一个展现为源码树(source tree)的简单示例结构(可以在gaborbernat / pugs 【2】里获得):

pugs-project
├── README.rst
├── setup.cfg
├── setup.py
├── LICENSE.txt
├── src
│   └── pugs
│       ├── __init__.py
│       └── logic.py
├── tests
│   ├── test_init.py
│   └── test_logic.py
├── tox.ini
└── azure-pipelines.yml复制代码

这里有四类独特的内容:

我们的pugs 包在用户机器的解释器上能用,意味着什么?在理想情况下,一旦启动解释器,用户应该能够 import 它,并调用其中的函数:

  • 业务逻辑代码(src 文件夹中的内容)
  • 测试代码(tests 文件夹和 tox.ini)
  • 包代码和元数据(setup.py、setup.cfg、LICENSE.txt、README.rst--请注意,我们如今使用的是事实上的标准打包工具setuptools【3】)
  • 有助于项目管理和维护的文件:
  • - 持续集成(azure-pipelines.yml)
    • 版本控制(.git)
    • 项目管理(例如潜在的 .github 文件夹)
Python 3.7.2 (v3.7.2:9a3ffc0492, Dec 24 2018, 02:44:43)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pugs
>>> pugs.do_tell()
"An enlightened pug knows how to make the best of whatever he has to work with - A Pug's Guide to Dating -  Gemma Correll"复制代码

Ryan Antooa 摄/Unsplash--让我们开始吧,兴奋!

 

Python 包的可用性

Python 怎么知道什么可用或不可用?简短的答案是,它不知道。至少不在前期知道。相反,它将尝试加载,并动态地检查是否可用。

它从哪里加载?有许多可能的位置,但是在大多数情况下,我们说的是从文件系统的文件夹中加载。这个文件夹在哪里呢?对于给定的模块,可以打印该模块的表示(representation)来找出:

>>> import pugs
>>> pugs
<module 'pugs' from '/Users/bernat/Library/Python/3.7/lib/python/site-packages/pugs/__init__.py'>复制代码

你会发现文件夹的位置取决于:

  • 软件包的类型(三方库或者标准库的内置/aka部分)
  • 它是全局的或仅限于当前的用户(请参阅PEP-370【4】)
  • 以及它是系统 Python 还是一个虚拟环境

但是一般来说,对于给定的 Python 解释器,可以通过打印出 sys.path 变量的内容,来找到可能的目录列表,例如在我的 MacOS 上:

>>> import sys
>>> print('\n'.join(sys.path))
/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/Users/bernat/Library/Python/3.7/lib/python/site-packages
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages复制代码

对于第三方软件包,会是一些 site-packages 文件夹。在以上示例中,请注意哪些是在整个系统范围内,哪些仅属于一个特定的用户。这些包是如何被放在此文件夹中的?它一定是由某些安装程序放在那里的。

下图展示了大多数的运行情况:

  1. 开发者在文件夹(称为源码树)内编写一些 Python 代码。
  2. 然后,某些工具(例如 setuptools)将源码树打包以进行重新分发。
  3. 生成的软件包通过另一个工具(twine),上传到可以被终端用户计算机访问的中央存储仓(通常为pypi.org【5】)。
  4. 终端用户计算机使用一些安装程序来查找、下载和安装相关软件包。安装操作最终是在 site-packages 文件夹内,创建正确的目录结构和元数据。

Pinho/摄--在探索新鲜事物

 

Python 包的类型

在安装时,软件包必须生成至少两种类型的内容,以放入 site-packages 中:有关软件包内容的元数据文件夹,其中包含 {package}-{version} .dist-info 和业务逻辑文件。

/Users/bgabor8/Library/Python/3.7/lib/python/site-packages/pugs
├── __init__.py
├── __pycache__
│   ├── __init__.cpython-37.pyc
│   └── logic.cpython-37.pyc
└── logic.py

/Users/bgabor8/Library/Python/3.7/lib/python/site-packages/pugs-0.0.1.dist-info
├── INSTALLER
├── LICENSE.txt
├── METADATA
├── RECORD
├── WHEEL
├── top_level.txt
└── zip-safe复制代码

发行信息(dist-info)文件夹描述了该软件包:用于安装该软件包的安装程序、该软件包所附的许可证、在安装过程中创建的文件、顶层 Python 软件包是什么、该软件包暴露的入口等等。在PEP-427【6】 中可以找到每个文件的详细说明。

我们如何从源码树中获得这两种类型的内容呢?我们面前有两条截然不同的路径:

  1. 从我们的源码树生成此目录结构和元数据,将其压缩为单个文件,然后将其发布到中央软件包存储仓。在这种情况下,安装程序必须下载软件包并将其解压到 site-packages 文件夹中。我们将这种类型的包称为 wheel 包。
  2. 或者,你可以创建一个包含软件包源码的归档文件,构建所需的脚本和元数据,以生成可安装的(installable)目录结构,然后将其上传到中央存储仓。这称为源码分发或 sdist。在这种情况下,安装程序还有很多工作要做,它需要解压归档文件,运行构建器,然后再将其复制。

这两个方法的区别主要在于包的编译/构建操作发生在哪里:在开发者的计算机上还是在终端用户的计算机上。如果它发生在开发者的一边(例如在 wheel 的情况下),则安装过程非常轻巧。一切都已经在开发机器上完成了。用户机器的操作仅是简单的下载和解压。

在本例中,我们使用 setuptools 作为构建器(从源码树生成要放入 site-packages 文件夹中的内容)。因此,为了在用户机器上执行构建操作,我们需要确保在用户机器上有合适版本的 setuptools (如果你使用的是 40.6.0 版的功能,则必须确保用户具有该版本或大于该版本)。

要考虑的另一种情况是 Python 提供了从其内部访问 C/C++ 库的能力(在需要的地方获得额外的性能)。这样的软件包被称为 C 扩展包(C-extension packages),因为它们利用了 CPython 提供的 C 扩展 API。

此类扩展需要编译 C/C++ 功能,才能适用与其交互的 C/C++ 库和当前 Python 解释器的 C-API 库。在这些情况下,构建操作实际上涉及到调用一个二进制编译器,而不仅仅是像纯 Python 包(例如我们的 pugs 库)那样,生成元数据和文件夹结构。

如果在用户计算机上进行构建,则需要确保在构建时,有可用的正确的库和编译器。现在这是一项相对困难的工作,因为有些特定于平台的二进制文件,也是通过平台打包工具分发的。这些库的缺失或版本不匹配通常会在构建时触发隐秘的错误,使用户感到沮丧和困惑。

因此,如果可能的话,始终选择将 package 打包成 wheel。这将完全避免用户缺少正确的构建依赖项的问题(纯 Python 类型如 setuptools 或二进制类型的 C/C++ 编译器)。即使这些构建依赖项易于配置(例如,使用纯 Python 构建器--例如 setuptools),你完全可以避免此步骤,来节省安装的时间。

话虽如此,仍然有两种需要提供源码分发的情况(即使在你提供 wheel 的情况下):

  1. C 扩展的源码分发往往更易于审核,因为人们可以阅读源代码,从而在其内容上有更高的透明度:许多大型公司的环境出于此单一原因,更倾向于使用 wheel(它们通常会将此扩展到纯 Python wheel,主要是为了避免对哪些是纯 Python 和什么不是做分类)。
  2. 你可能无法为每个可能的平台都提供一个 wheel(在使用 C 扩展包的情况下,尤其如此),在这种情况下,源码分发可以让这些平台自行生成 wheel。

 

小结

源码树(source tree)、源码分发(source distribution)和 wheel 之间的区别:

  • 源码树——包含在开发者的机器/存储仓上可用的所有项目文件(业务逻辑、测试、打包数据、CI 文件、IDE 文件、SVC 等),例如,请参见上面的示例项目。
  • 源码分发——包含构建 wheel 所需的代码文件(业务逻辑+打包数据+通常还包括单元测试文件,用于校验构建;但是不包含开发者环境的内容,例如 CI/IDE/版本控制文件),格式:pugs-0.0 .1.tar.gz 。
  • wheel——包含包的元数据和源码文件,被放到 site packages 文件夹,格式:pugs-0.0.1-py2.py3-NONE-any.whl 。

Charles PH 摄/Unsplash--hmmm

英文 | The state of Python Packaging【1】

原作 | BERNAT GABOR

#标签:Python 打包,Python 包的类型