第 9 章 Python 封装及其运用

本章将对程序包的使用方法和运用技巧进行介绍。

在第 3 章中,我们已经介绍了 Python 项目的结构。本章内容可视为其后续,为各位讲解包的使用方法,包括可用于试运行环境和正式环境的部署、为执行测试的部署等方面。只要包的使用方法和封装方法得当,测试及部署的自动化也会简单很多。另外,根据实际情况,我们往往需要满足一些常规处理之外的条件。加深对封装的理解,有助于各位应对这些特殊情况,免除为其单独花费工夫的麻烦。

本章还将进一步深入地讲解 pip 的用法。此外,我们还将学习一些其在用法上的组合技巧,帮助我们活用 pip。

9.1 使用程序包

本部分将介绍一些能应用于部署及自动测试的 pip 功能。

9.1.1 程序包的版本指定

有些时候,我们需要安装一些特定版本的程序包,比如想查看某版本下的运行情况,或者需要用到某个版本之前的最后一版。

指定程序包版本的方法有几个。以 pip install colander 为例,colander 是用于模式定义和校验的 Python 程序包。我们用几种不同方式来安装指定版本(LIST 9.1)。

LIST 9.1 指定版本的方法

  1. $ pip install -U colander # 1.0 : 最新的稳定版
  2. $ pip install -U colander==1.0" # 1.0 : 指定版本
  3. $ pip install -U "colander<1.0" # 0.9.9 : 1.0 版以前的最后一个稳定版
  4. $ pip install -U "colander<1.0" --pre # 1.0b1 : 1.0 版以前的最后一个版本
  5. $ pip install -U colander==1.0b1 # 1.0b1 : 指定版本
  6. $ pip install -U "colander<=1.0" # 1.0 : 1.0 版以前的最后一个稳定版(包括 1.0 版)
  7. $ pip install -U "colander>=0.9,<0.9.9" # 0.9.8 : 0.9 版(含)以后 0.9.9 版以前的最后一个稳定版

可见,指定版本的方法并不唯一。某些指定方法需要用双引号“"”括起来。这是防止不等号“<”“>”被 shell 解释为重定向。

为了便于理解,我们这里指定了 -U(—upgrade)选项,因此即便指定程序包已存在于计算机中,系统仍会再安装一遍。

另外,pip 不会安装 alpha 版、beta 版等预发布版本的程序包。至于某版本是否为预发布版本,可以看版本号是否像 1.3a1、1.3b1 这样后面跟着 a1、b1(在 PEP 4401 中有相关定义)。要安装上述预发布版本时,需要明确指定版本号,或者附加 —pre 选项。

1https://www.python.orgdevpeps/pep-0440

专栏 版本命名规范与大小关系

版本的命名规范(结构)与大小关系在 PEP 440 中有相关规定。

版本号要遵循 [N!]N(.N)[{a|b|c}N][.postN][.devN] 的结构。N 可以为正整数或0。(.N) 可以重复出现任意次。被 [] 括起来的部分可以省略。a、b、c 分别代表 alpha、beta、rc。

各版本号的先后顺序自然不能根据字符串排序来定,其编号方式及排序大致如 LIST 9.2 所示。

LIST 9.2 版本的编号方式及排序

  1. 0.9.1
  2. 0.10
  3. 1.0a1.dev1
  4. 1.0a1
  5. 1.0b1
  6. 1.0b2
  7. 1.0c1
  8. 1.0
  9. 1.0-post1
  10. 1.0.1

实际上,版本号这东西我们很少会在命令行指定。大多是在 setup.py 中指定版本时才会用到它。

举个例子,假设我们为 Python 2.7 开发的 myapp 程序依赖于名叫 securelib 的外部库。然而 securelib 从 1.0 版之后才开始提供我们所需的功能,所以必须指定安装 1.0 以后的版本(LIST 9.3)。现阶段的情况是,myapp 的 setup.py 中并没有给 install_requires 指定版本号。这种情况下,如果计算机中已经装有 securelib,那么其版本将不会被更新。一旦使用者环境中安装的是不提供所需功能的旧版本,程序将无法正常运行。

LIST 9.3 在 setup.py 中指定依赖库为 1.0 以后的版本

  1. setup(
  2. name='myapp',
  3. ...
  4. install_requires=[
  5. 'securelib>=1.0',
  6. ],
  7. )

这里如果指定 securelib==1.0 又会带来别的问题。因为这样一来,只要我们使用 myapp,securelib 的版本就会被限制在 1.0。就算 securelib 因为安全问题发布了 1.0.1,myapp 的使用者也只能继续用 securelib-1.0。为避免这类问题,要尽量避免在 setup.py 中指定固定版本。

接下来了解一下另一种情况,就是指定某版本之前的版本。比如 securelib-3.0 发布,其使用的 API 发生了重大变更。此时如果安装 myapp 时安装了 3.0,那么 myapp 将无法正常工作。为防止这个情况发生,可以给 myapp 追加一条指定,指定其使用 securelib-3.0 之前的版本(LIST 9.4)。

LIST 9.4 在 setup.py 中指定依赖库为 1.0 以后、3.0 以前的版本

  1. setup(
  2. name='myapp',
  3. ...
  4. install_requires=[
  5. 'securelib>=1.0,<3.0',
  6. ],
  7. )

做过上述修改后,就能让使用者安装正确版本的程序包,保证 myapp 正常运行了。

NOTE

本例中,myapp 选择使用旧版本的securelib。然而,如果程序一直依赖于旧版本的 securelib,将无法使用今后推出的安全更新。所以根据 API 的变更重新修改、测试 myapp,让其支持 securelib-3.0 才是比较好的选择。

9.1.2 从非 PyPI 服务器安装程序包

pip 搜索程序包时默认引用 PyPI 的 URL2。需要引用 PyPI 以外的服务器时,有两个选项可供使用。

2https://pypi.python.org/simple

-i(—index-url)选项可以指定其他兼容服务器来代替 PyPI。这些服务器被称为 index 服务器,PEP 3013、PEP 4384 中对它们的规格有着严格规定。举个例子,当我们想从 PyPI 的测试服务器 5 安装程序包时,可以像下面这样执行。

3https://www.python.orgdevpeps/pep-0301

4https://www.python.orgdevpeps/pep-0438

5https://testpypi.python.org/

  1. $ pip install bpbook -i https://testpypi.python.org/simple

同样地,我们可以在公司内搭建替代 PyPI 的 index 服务器,用 pip 从该服务器上进行安装。PyPI 上公开了多种搭建 PyPI 替代服务器的方法,其中 devpi6 可以给每个用户设置多个索引,同时具备 PyPI 镜像功能,非常好用。

6https://pypi.python.org/pypi/devpi

另一个用起来更方便的选项是 find-links。-f(—find-links)选项用来指定包含目标程序包链接的页面。该选项指定的页面所链接的程序包会优先于 index 服务器。如果在指定页面没有找到目标程序包,计算机则会引用 index 服务器内的资源进行安装。

  1. $ pip install -f https://bitbucket.org/shimizukawa/logfilter/downloads logfilter
  2. Downloading/unpacking logfilter
  3. Downloading logfilter-0.9.2.zip
  4. Running setup.py egg_info for package logfilter

像上面例子中这样,指定 -f 选项后,计算机便会检查该页面内的链接,识别程序包名称,自动获取并安装最新版本。不过,pip install 只能识别符合规定的正确包名。规定大致如下。

  1. { 包名}-{ 版本号}(-{ 平台})?.{ 扩展名}

指定平台的部分可以省略。如果一个程序包指定了平台(如 win32),那证明它在其他平台上是无法运行的。因此,pip 只会寻找并安装适合当前平台的程序包。

专栏 安装未存放于PyPI 的程序包

有些程序包虽然被添加到了PyPI,但实际的程序包文件却存放在其他地方。这种程序包无法用 pip install 程序包名的方式进行安装,所以必须指定 —allow-external 选项。

  1. $ pip install allow-external 程序包名

另外,如果程序包被存放在 http 环境而非 https 环境下,pip 会认为程序包有可能在通讯中被篡改,因此会中断安装。在迫不得已一定要使用这类程序包的情况下,请指定 —allowunverified 选项。

  1. $ pip install allowunverified 程序包名

专栏 已删除的 PyPI 镜像规格

PEP 3817 对 PyPI 镜像服务器规格作了规定。但随着 CDN 等技术的应用,以及 PyPI 服务器逐渐稳定,镜像服务器规格的删除在 PEP 4648 中被提出,并最终于 2014 年春完成删除。因此,如今已不需要用 —index-url 选项引用镜像服务器。另外,曾经的 —use-mirror 选项也于pip-1.5起被删除。

曾提供服务的 a.pypi.python.org、b.……之类的镜像服务器的域名早已废止。现在使用旧版本 pip,通过环境变量或设置将镜像引用设为有效后,pip 有可能不正常工作。因此请各位避免使用那些会引用镜像服务器的选项。

7https://www.python.orgdevpeps/pep-0381

8https://www.python.orgdevpeps/pep-0464

9.1.3 程序包的发布格式

如今上传到 PyPI 的程序包大多采用 sdist 格式。sdist 是包含程序包元数据及构建方法的存档格式。它会在每次安装时读取各环境的设置,存在 C 扩展时进行构建,然后检查所需的 Python 程序包并复制到 sitepackages。另外,sdist 中其实有许多文件并不会被安装。这些工作在每个平台上实施一次就足够了。相对地,二进制包的特点是仅包含已构建完毕的 C 扩展和 Python 程序包,仅解压程序包的存档即可完成安装。

Python 的二进制包长期以来使用着 setuptools 实现的 egg 格式。但是 pip 并不支持 egg 格式的程序包,这导致该格式在普及的道路上一直停滞不前。随着 Python 封装的规范化,PEP 4279 提出了一个能克服 egg 缺点的 wheel 格式。加之 pip 也开始支持 wheel 格式,所以其很快在部署等方面得到应用。

9https://www.python.orgdevpeps/pep-0427

已上传到 PyPI 的 wheel 格式程序包可以通过 pip 直接安装。接下来,我们以安装 Django 为例来学习一下。

  1. $ pip install django
  2. Downloading/unpacking django
  3. Downloading Django-1.7.1-py2.py3-none-any.whl (7.4MB): 7.4MB downloaded
  4. Storing download in cache at varpip-chache/https%3A%2F%2Fpypi.python.org%2Fpackages%2Fpy2.py3%2FD%2FDjango%2FDjango-1.7.1-py2.py3-none-any.whl
  5. Installing collected packages: django
  6. Successfully installed django
  7. Cleaning up...

可以看到计算机从 PyPI 下载了 wheel 格式的程序包。pip 是从 1.5 版以后开始正式支持 wheel 格式的。这里我们将 pip 的版本降回 1.4,然后再安装 Django 试试。

  1. $ pip install django
  2. Downloading/unpacking django
  3. Downloading Django-1.7.1.tar.gz (7.5MB): 7.5MB downloaded
  4. Storing download in cache at homeaodag/.pip/eggs/https%3A%2F%2Fpypi.python.org%2Fpackages%2Fsource%2FD%2FDjango%2FDjango-1.7.1.tar.gz
  5. Running setup.py egg_info for package django
  6. warning: no previously-included files matching '__pycache__' found under directory ''
  7. warning: no previously-included files matching '.py[co]' found under directory '*'
  8. Installing collected packages: django
  9. Running setup.py install for django
  10. warning: no previously-included files matching '__pycache__' found under directory ''
  11. warning: no previously-included files matching '.py[co]' found under directory '*'
  12. changing mode of build/scripts-2.7/django-admin.py from 644 to 755
  13. changing mode of homeaodag/works/bpbook2015/djangoenv/bin/django-admin.py to 755
  14. Installing django-admin script to homeaodag/works/bpbook2015/djangoenv/bin
  15. Successfully installed django
  16. Cleaning up...

可以看到,这种情况下的安装是通过 tar.gz 存档的 sdist 进行的。另外,相较于通过 wheel 进行安装,通过 sdist 进行安装时,下载后的处理要多出很多。

对于 Django 这种不包含 C 扩展的程序包而言,上面的差距还算可以接受。可是一旦换成 Pillow 这种包含 C 扩展,且 C 扩展需要大量依赖库的程序包时,差距就无法忽视了。现在 PyPI 上提供了支持各种 Python 版本及 CPU 的 wheel 格式 Windows 版 Pillow 程序包,即便是在难以准备编译器的 Windows 环境下,也只需要通过 pip 进行安装即可开始使用 Pillow 了。

专栏 wheel 程序包的文件名

PEP 42710 中规定了 wheel 程序包的命名规则。自此,wheel 程序包依赖于哪个版本的 Python 以及哪个OS,仅凭包名即可一目了然。

其命名与解释规则如下。

  1. {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl

LIST 9.5 是一些已上传到PyPI 的程序包的文件名。

LIST 9.5 wheel 程序包文件名示例

  1. bpmappers-0.7-py2-none-any.whl
  2. Django-1.7.1-py2.py3-none-any.whl
  3. MarkupSafe-0.23-cp27-none-linux_x86_64.whl
  4. Pillow-2.6.1-cp32-cp32m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.whl

10https://www.python.orgdevpeps/pep-0427

9.1.4 生成wheelhouse 的方法

wheel 格式的程序包方便好用,但我们前面也说了,PyPI 上大部分程序包都是 sdist 格式。为方便今后在 CI 工具上和部署时能有 wheel 可用,我们来学习一下如何根据 sdist 生成 wheel。pip 可以通过 wheel 命令将 PyPI 上只提供了 sdist 的程序包转换成 wheel 格式并保存在本地计算机中。本地的 wheel 格式程序包默认保存在 wheelhouse 目录下,也正因为如此,我们习惯将所有保存 wheel 格式程序包的目录都称为 wheelhouse(与实际目录名称无关)。使用 wheel 命令前,需要先安装 wheel 包。

  1. $ pip install wheel

接下来,我们以生成 Pillow 的 wheel 为例实际生成一个 wheelhouse。

  1. $ pip wheel pillow
  2. Downloading/unpacking pillow
  3. Downloading Pillow-2.6.1.tar.gz (7.3MB): 7.3MB downloaded
  4. Running setup.py egg_info for package pillow
  5. ...
  6. Building wheels for collected packages: pillow
  7. Running setup.py bdist_wheel for pillow
  8. Destination directory: homeaodag/works/bpbook2015/wheelhouse
  9. Successfully built pillow
  10. Cleaning up...
  1. $ ls wheelhouse/ -l
  2. -rw-r--r-- 1 bp bp 714085 11 10 19:31 Pillow-2.6.1-cp27-none-linux_x86_64.whl

可见,pip 和 wheel 组合使用之后,可以将只提供了 sdist 的程序包转换为 wheel 格式保存。

NOTE

生成并保存在 wheelhouse 里的 wheel 依赖于执行 wheel 命令的平台。库中包含 C 扩展的 wheel 不具备跨平台通用性。比如正式服务器使用 Linux 但开发者使用 OS X,就需要准备一个与正式服务器同平台的 CI 服务器,在上面执行生成 wheelhouse 的任务。

9.1.5 从 wheelhouse 安装

现在我们来学习如何安装 wheelhouse 内的程序包。最简单的方法就是用 pip install 直接指定 wheelhouse 目录下的文件。

  1. $ pip install wheelhouse/Pillow-2.6.1-cp27-none-linux_x86_64.whl
  2. Unpacking ./wheelhouse/Pillow-2.6.1-cp27-none-linux_x86_64.whl
  3. Installing collected packages: Pillow
  4. Successfully installed Pillow
  5. Cleaning up...

由于 C 扩展的编译等处理在这个阶段早已完成,所以安装会十分迅速。不过,wheel 格式程序包的文件名通常比较复杂,每次安装都手动输入实在麻烦。

其实,只要用 -f(—find-links)指定 wheelhouse 目录,程序包名部分就可以只写 Pillow 了。

  1. $ pip install -f wheelhouse pillow
  2. Downloading/unpacking pillow
  3. Installing collected packages: pillow
  4. Successfully installed pillow
  5. Cleaning up...

保险起见,我们添上 —no-index 选项以防 wheelhouse 以外的目录被引用。

  1. $ pip install --no-index -f wheelhouse pillow
  2. Ignoring indexes: https://pypi.python.org/simple/
  3. Downloading/unpacking pillow
  4. Installing collected packages: pillow
  5. Successfully installed pillow
  6. Cleaning up...

事先在 wheelhouse 目录下生成的 wheel 格式程序包都可以通过上述步骤进行安装。只要将所有依赖库全都放到 wheelhouse 下,我们就可以脱机完成环境搭建。另外,由于安装时所需的处理极少,所以非常适合需要多次重复搭建相同环境的情况。为部署和 CI 工具搭建环境时,请务必记得运用 wheelhouse 目录。

9.2 巧用程序包

在需要多次重复安装的时候,wheel 格式的程序包显得十分便捷。接下来我们学习一下如何将其巧用到 CI 和部署中去。

9.2.1 私密发布

有时候我们需要用到未在 PyPI 上公开的程序包,比如不能在 PyPI 上公开的公司内部库,或者一些经过修改但尚未正式发布的库等。

这种时候,可以借助版本库服务器提供的功能做私密发布。私密发布一般用来做程序包公开到 PyPI 前的测试,以及给一些不想添加到 PyPI 但仍需安排版本号的程序包做内部公开。前面我们学习了直接从版本库进行安装的方法,使用者可以通过这个方法获取目标版本的程序包,其过程与程序包作者本人的意图基本无关。

Github 和 Bitbucket 会以标签名或分支名为单位提供 zip 或其他格式的源码文件。同时,通过使用 pip 指定包含程序包名称的完整 URL,可以绕过索引服务器直接安装程序包。将这两个组合在一起,我们就能安装已私密发布但尚未在 PyPI 发布的程序包了。

举个例子,有个名为 logfilter 的工具,其源码在 Bitbucket 中公开管理。这个工具并未在 PyPI 上公开程序包,因此不能通过 pip install logfilter 进行安装。但是,如果我们像 LIST 9.6 这样直接指定 URL,就能够完成安装了。

LIST 9.6 安装已私密发布的程序包

  1. $ pip install https://bitbucket.org/shimizukawa/logfilter/get/logfilter-0.9.2.zip

9.2.2 巧用 requirements.txt

pip freeze 命令可以查看通过 pip 安装的依赖库的内容。如果将这些内容保存在文件中,再用 pip install 的 -r 选项指定这个文件,在该环境中安装的库就可以在其他环境中被还原。

  1. $ pip freeze > requirements.txt
  2. $ pip install -r requirements.txt

多数项目都将依赖库的列表保存在了 requirements.txt 文件中。不过,requirements.txt 能做的事还远不止于此。

requirements.txt 内部可以引用其他 requirements.txt。比方说我们要把只在开发时才需要的库和执行时需要的库分开管理。此时,我们需要把测试运行器和配置器工具写在 tests-require.txt 文件里,把执行时需要的库写在 requirements.txt 文件里。这种情况下,只要准备一个如 LIST 9.7 所示的 dev-requires.txt,就能通过一条 pip install -r dev-requires.txt 完成开发者环境的准备工作。

LIST 9.7 dev-requires.txt

  1. -r requirements.txt
  2. -r tests-require.txt

另外,建议将 —allow-external 和 —allowunverified 等常用选项也一并写在 dev-requires.txt 中。这样可以将选项与 requirements.txt 分离,免得在使用 pip freeze > requirements.txt 自动生成文件时丢失选项。

9.2.3 requirements.txt 层级化

接下来,我们考虑对 requirements.txt 进行分割。首先,requirements.txt 是许多工具默认识别的文件名,所以这个文件名的重要性最高,要用来保存执行时所需程序库的列表。接下来考虑开发者使用的工具。这部分可以按用途分为测试工具、文档生成、模式迁移等多个类别。我们为每个用途分别准备一个文件。

LIST 9.8 ~ LIST 9.12 表示的就是一个文件分割的例子。

LIST 9.8 requirements.txt

  1. pyramid==1.5.2
  2. sqlalchemy==0.9.8
  3. psycopg2==2.5.4

LIST 9.9 tests-require.txt

  1. pytest==2.6.4
  2. pytest-cov==1.8.1
  3. coverage==3.7.1
  4. webtest==2.0.6

LIST 9.10 docs-require.txt

  1. Sphinx==1.3b2

LIST 9.11 db-requires.txt

  1. alembic==0.6.7
  2. psycopg2==2.5.4

LIST 9.12 dev-requires.txt

  1. --no-index
  2. -f http://devpi/+myproject/simple
  3. -r requirements.txt
  4. -r tests-require.txt
  5. -r docs-require.txt
  6. -r db-requires.txt

这样设置下来之后,就不会在执行时安装一些没用的库了。同时,tests-require.txt 等还可以拿到其他项目之中重复利用。

9.2.4 为部署和 CI+tox 准备的 requiremests

前面我们学习过,用 pip 能够将依赖库以 wheel 格式保存在 wheelhouse 里。为方便将它们运用到 CI 和部署当中,我们要简化这些依赖库的安装流程。

用 pip 管理程序包的情况下,依赖库会记录在 requirements.txt 中。这里我们跟前面一样,在 requirements.txt 里只保存依赖库的列表。

然后,为了在搭建环境时只从 wheelhouse 获取这些库,我们像 LIST 9.13 这样编写 dev-requires.txt,将库的获取位置限定为 wheelhouse。

LIST 9.13 dev-requires.txt

  1. -f wheelhoouse
  2. --no-index
  3. -r requirements.txt

有了这些文件之后,就可以用下述方法从 wheelhouse 进行安装了。

  1. $ pip install -r dev-requires.txt

接下来,我们把它应用到我们在 8.4.1 节学习过的测试环境工具 tox 上。tox 可以在 virtualenv 中为每个测试环境搭建一个虚拟环境,如果能通过 wheel 格式给各个环境安装依赖库,那么必将提升搭建环境的效率。

tox 可以在 tox.ini 文件中指定安装到环境的库(LIST 9.14)。由于这部分是直接交给 pip 处理的,所以指定的东西不一定非要是库,还可以是 requirements.txt 等文件。

LIST 9.14 tox.ini

  1. [testenv]
  2. deps = -rdev-requires.txt

这样设置下来之后,计算机就会根据 dev-requires.txt 从本地的 wheelhouse 安装依赖库。这里有一点需要注意,tox 不会查看 dev-requires.txt、requirements.txt 这类文件的内容,所以如果我们对这些文件进行了编辑,需要手动执行带 —recreate 选项的 tox 命令。

  1. $ tox --recreate

虽然难以避免上述这类麻烦,但对于某些需要频繁重置环境的 CI 工具而言,它仍是一个非常省时省力的方法。特别是使用 travis.ci 等对执行时间有限制的工具时,缩短搭建环境的时间显得十分重要。

9.2.5 通过 requirements.txt 指定库的版本

我们在用 pip freeze 生成的 requirements.txt 文件中指定了库的版本。通过 requirements.txt 指定版本的方法有很多种,这里我们学习直接指定版本的方法。对于程序库而言,即便只是修复 Bug,也可能导致其运行出现变化。因此,开发、测试、正式环境要保证使用同版本的库。CI 工具可以用 tox 的 —recreate 选项来严格按照 requirements.txt 中描述的库重新搭建环境。另外,想知道环境中是否安装了未包含在 requirements.txt 中的库时,可以通过 pip freeze 指定 requirements.txt 进行查看(LIST 9.15)。

LIST 9.15 查看是否安装了未包含在 requirements.txt 中的库

  1. $ pip freeze -r requirements.txt

随着程序库版本不断更新,wheelhouse 内的文件会越积越多。我们可以通过下述方法将未包含在 requirements.txt 中的库转移到其他目录。

  1. $ mv wheelhouse wheelhouse.old
  2. $ pip wheel -r requirements.txt -f wheelhouse.old

对正式服务器而言,直接从当前活动环境中撤走程序库会带来很高的风险。这时就需要花些心思来规避风险了,比如在 virtualenv 上重新搭建一个环境,然后切换 nginx 等反向代理的连接。

9.3 小结

本章介绍了程序包的使用方法和使用技巧。正如本章所说的,巧用 pip 可以为测试和部署的自动化减轻很多负担。希望各位能以本章介绍的方法为参考,研究出适合各自项目的结构。