Ocavue's Blog
Pipenv:Python 官方钦定的包管理工具 2018-09-19

今天给大家介绍的是 python 包管理工具 Pipenv。在 Python 的官方文档 (opens new window)中,有这么一段话:

Use Pipenv to manage library dependencies when developing Python applications ... Consider other tools such as pip when pipenv does not meet your use case."

也就是说 Python 官方认为 Pipenv 比 pip 更加适合一般的用户。作为一个第三方工具,能从官方获得如此高的评价实属不易。这篇文章就向大家简要介绍一下这个工具。

xkcd: Python Environment

↑ xkcd: Python Environment

# 背景知识

在进入主题之前,首先我们要区分 Pythpn 社区中几个名字非常类似的工具。这里推荐 stackoverflow 上 Flimm 的回答 (opens new window),讲的十分清楚了。摘抄翻译一下本文会讲到的几个工具:

  • PyPI 中的非标准库
    • virtualenv (opens new window) 是一个非常流行的、用来创建虚拟环境的工具。如果你不熟悉这个工具,我强烈推荐你去学习一下,因为它真的非常有用。接下来的回答中我都会拿他进行对比。 它的原理是在一个目录下(比如:env/)安装很多文件,然后在 PATH 环境变量的前面加上一个自定义的 bin 目录(比如:env/bin)。一个完整的 python 或者 python3 二进制文件会放入这个目录中,Python 被设计成先在相对它的路径处寻找库。它不是 Python 标准库的一部分,但是它隶属于 PyPA (Python Packaging Authority) 。激活了之后,你可以直接通过 pip 在虚拟环境中安装库。
    • pyenv (opens new window) 用来隔离 Python 版本。比如你可能想要在 Python 2.6、2.7、3.3、3.4 和 3.5 中测试你的代码,所以你需要一种在它们之间进行切换的方式。激活之后,它会在 PATH 前面加上 ~/.pyenv/shims,这里储存了一些 Python 命令程序 (python, pip) 。这些文件不是从系统已有的 Python 中复制出来的,而是专门用来根据环境变量 PYENV_VERSION.python-version 文件或者 ~/.pyenv/version 文件来决定使用哪个 Python 版本。pyenv 也简化了下载和安装多个 Python 版本的步骤,使用 pyenv install 命令即可。
    • pipenv (opens new window), 由 Kenneth Reitz 编写 (requests 的作者)(国内也称作 K 神——译者注),是这个列表当中最新的一个项目。它将 Pipfile, pipvirtualenv 结合到同一个命令行命令。 virtualenv 目录一般放在 ~/.local/share/virtualenvs/XXX, 其中的 XXX 是项目目录的 hash。这和 virtualenv 不一样,后者一般放到当前的工作目录。
  • 标准库
    • venv (opens new window) 是 Python 3 内置的一个包(从 Python 3.3 版开始——译者注)。你可以通过 python3 -m venv 来运行它。它的目标和实现方式都和 virtualenv 非常类似,只不过它不需要复制 Python 二进制文件(Windows 除外)。除非你要使用 Python2,不然你应该选择 venv。不过截止本文时,Python 社区更喜欢 virtualenvvenv 并没有什么声音。

venv 的心情如图

↑ venv 的心情如图

# 安装

官方文章的 Installing Pipenv (opens new window) 一章有好几种安装的方式。在不是很重要的机器上(比如自己的开发机)可以直接使用 pip install pipenv 进行安装。但是如果在生产环境上安装,就需要其他方式。这里我介绍一下 User Installs (opens new window) 的方式。这种方式虽然有点繁琐,但是胜在安全,不会影响已有的 Python 环境。

[superman@server ~ ]$ pip3 install --user pipenv
    100% |████████████████████████████████| 5.0MB 314kB/s
Successfully installed pipenv-2018.7.1
[superman@server ~ ]$ which pipenv
pipenv not found

注意此时由于没有修改 PATH(环境变量),所以还需要找到用户目录并修改并将下面的 bin 目录加入 PATH 中。

在 Linux 和 macOS 下,用户目录可以使用 python -m site --user-base 命令获得:

[superman@server ~ ]$ python3 -m site --user-base
/home/superman/.local
[superman@server ~ ]$ export PATH=$PATH:$(python3 -m site --user-base)/bin
[superman@server ~ ]$ which pipenv
/home/superman/.local/bin/pipenv

为了方便起见,大家可以修改 ~/.profile~/.bashrc 或者类型的文件,加入上面设置 PATH 的语言,让 shell 在每次登录的时候自动设置 PATH

同时为了避免不同的工具链之间的相互影响,通过 User Install 方式安装的「工具链」(比如 virtualenv、pyenv 和 tox)要尽可能少。

# 基本用法

使用 pipenv install <packagename> 安装包。

$ cd myproj
$ pipenv install requests

Using /usr/bin/python3 (3.5.2) to create virtualenv...
Virtualenv location: /root/.local/share/virtualenvs/myproj-y53uuyLx
Creating a Pipfile for this project..
Adding requests to Pipfile's [packages]...
Pipfile.lock not found, creating...
Updated Pipfile.lock (0b4483)!
Installing dependencies from Pipfile.lock (0b4483)...

在第一次执行 pipenv install 的时候,Pipenv 会做这么几件事情:

  1. 创建 virtualenv 环境
  2. 创建 Pipfile 文件
  3. 创建 Pipfile.lock 文件
  4. 安装依赖

其他常用命令:

  • pipenv install:安装 Pipfile[packages] 下面的包
  • pipenv install --dev:安装 Pipfile[packages][dev-packages] 下面的包
  • pipenv install <package>:在当前环境安装包,并将相关信息写入 Pipfile[packages]Pipfile.lock
  • pipenv install <package> --dev:在当前环境安装包,并将相关信息写入 Pipfile[dev-packages]Pipfile.lock
  • pipenv uninstall <package>: 在当前环境卸载包,并将相关信息从 PipfilePipfile.lock 中移除
  • pipenv lock:确认 Pipfile 中所有包已安装,并根据安装版本生成 Pipfile.lock
  • pipenv --rm:删除 virtualenv 环境
  • pipenv --venv:输出当前 virtualenv 环境的路径
  • pipenv shell:进入虚拟环境,类似 venv 和 virtualenv 的 source ./venv/bin/activate
  • pipenv run <command>: 使用虚拟环境跑一些命令,比如 pipenv run pip3 list

# Pipenv 解决了哪些问题?

# 虚拟环境和 pip 不能同时使用

venv 和 virtualenv 可以提供独立的 Python 环境,这是一个非常必要的功能。但是每次进入和退出环境都需要手动地执行命令,非常繁琐而且容易出错。

举个例子,projectAprojectB 各有一个 venv 环境(位置是两个项目下的 .venv 目录),我想在两个 venv 环境中安装不同版本的库,正确的做法是下面这样的:

$ cd projectA
$ . .venv/bin/active
(venv) $ pip3 install requests==2.0  # 安装在 projectA/.venv/
(venv) $ cd ../projectB
(venv) $ which pip3
/tmp/projectA/venv/bin/pip3
(venv) $ deactive
$ . .venv/bin/active
(venv) $ pip3 install requests==2.1  # 安装在 projectB/.venv/

可以看到,切换目录并不会自动地切换 venv 环境。这样不仅繁琐,而且容易造成失误,因为我可能没有注意到我所在的目录和使用的 venv/virtualenv 环境不同。

使用 Pipenv 的话,就能解决这个问题了,这也是诸如 npm 等其他包管理器的行为。

$ cd projectA
$ pipenv install requests==2.0  # 安装在 ~/.local/share/virtualenvs/projectA-OOSnJKux/
$ cd ../projectB
$ pipenv install requests==2.1  # 安装在 ~/.local/share/virtualenvs/projectB-XaGlAV6H/
$ cd projectA
$ npm install express-generator@4.0  # 安装在 projectA/node_modules/
$ cd ../projectB
$ npm install express-generator@4.2  # 安装在 projectB/node_modules/

扩展阅读:A Better Pip Workflow™ (opens new window)

# pip 不能精确地指定版本

今天在开发 Python 项目的时候,一般会使用一个 requirements.txt 文件来保存依赖信息,这也是标准的做法。而 requirements.txt 有两种使用方法:

第一种方式,只保存顶级依赖:

$ cat requirements.txt
requests[security]
flask
gunicorn==19.4.5

因为依赖及其子依赖版本并没有被固定下来,在执行 pip install -r requirements.txt 时会有不同的结果,可能会造成一些问题。

第二种方式,

$ cat requirements.txt
cffi==1.5.2
cryptography==1.2.2
enum34==1.1.2
Flask==0.10.1
gunicorn==19.4.5
idna==2.0
ipaddress==1.0.16
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
ndg-httpsclient==0.4.0
pyasn1==0.1.9
pycparser==2.14
pyOpenSSL==0.15.1
requests==2.9.1
six==1.10.0
Werkzeug==0.11.4

这种方式所有的依赖,包括项目的依赖、和依赖的依赖,而且每个依赖都精确到具体的版本,这也是当前 Python 社区的最佳实践。顺便一提这些信息可以使用 pip freeze 命令输出。但是使用这种方式,管理起来不方便,没有办法很好的分清楚哪些依赖是项目直接需要的,哪些依赖是子依赖。尤其是在升级依赖版本的时候,这种混乱会造成很多困难。

Pipenv 的解决方案是 PipfilePipfile.lock ,使用它们来替代老旧过时的 requirements.txt

# Pipfile

Pipfile 使用了 TOML (opens new window) 格式而不是 requirements.txt 的纯文本,其中储存了生产环境([packages])和开发环境([dev-packages])下所需要的顶级依赖。下面有个简单的例子

[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"

[packages]
requests = "*"

[dev-packages]
pytest = "*"

# Pipfile.lock

Pipfile.lock 实际上是一个 JSON 文件,保存着所有的依赖的版本和 hash 信息。这个文件应该提交到 git 仓库里(除非你同时面向多个 Python 版本开发),而且在任何情况下,都不应该手动地修改这个文件。

一个 Pipfile.lock 文件的例子:

{
    "_meta": {
        "hash": {
            "sha256": "8d14434df45e0ef884d6c3f6e8048ba72335637a8631cc44792f52fd20b6f97a"
        },
        "host-environment-markers": {
            "implementation_name": "cpython",
            "implementation_version": "3.6.1",
            "os_name": "posix",
            "platform_machine": "x86_64",
            "platform_python_implementation": "CPython",
            "platform_release": "16.7.0",
            "platform_system": "Darwin",
            "platform_version": "Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64",
            "python_full_version": "3.6.1",
            "python_version": "3.6",
            "sys_platform": "darwin"
        },
        "pipfile-spec": 5,
        "requires": {},
        "sources": [
            {
                "name": "pypi",
                "url": "https://pypi.python.org/simple",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "certifi": {
            "hashes": [
                "sha256:54a07c09c586b0e4c619f02a5e94e36619da8e2b053e20f594348c0611803704",
                "sha256:40523d2efb60523e113b44602298f0960e900388cf3bb6043f645cf57ea9e3f5"
            ],
            "version": "==2017.7.27.1"
        },
        "chardet": {
            "hashes": [
                "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691",
                "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
            ],
            "version": "==3.0.4"
        },
        "idna": {
            "hashes": [
                "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4",
                "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f"
            ],
            "version": "==2.6"
        },
        "requests": {
            "hashes": [
                "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
                "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
            ],
            "version": "==2.18.4"
        },
        "urllib3": {
            "hashes": [
                "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
                "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
            ],
            "version": "==1.22"
        }
    },
    "develop": {
        "py": {
            "hashes": [
                "sha256:2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a",
                "sha256:0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3"
            ],
            "version": "==1.4.34"
        },
        "pytest": {
            "hashes": [
                "sha256:b84f554f8ddc23add65c411bf112b2d88e2489fd45f753b1cae5936358bdf314",
                "sha256:f46e49e0340a532764991c498244a60e3a37d7424a532b3ff1a6a7653f1a403a"
            ],
            "version": "==3.2.2"
        }
    }
}

Pipfile.lock 默认使用 sha256 算法给每一个包进行 hash,这样可以保证在不安全的网络环境下也能下载到正确的包。

PipfilePipfile.lock 都应该提交到版本库中。

# Pipenv 的其他特性

# 为什么 Pipenv 使用 virtualenv 而不是标准库中的 venv?

根据这个 issue (opens new window) 中的讨论,Pipenv 不使用 venv 的原因有两个:一是 Kenneth Reitz 个人更喜欢 virtualenv,二是 Pipenv 用来管理 virtualenv 的库 Pew (opens new window) 目前还不支持 venv (opens new window)

# 将 virtualenv 环境的位置改为工程目录下

Pipenv 默认会将 virtualenv 目录创建在 ~/.local/share/virtualenvs/ 中,但是很多人希望 (opens new window)可以将 virtualenv 目录放在工程目录下。Pipenv 也提供了相应的功能,只要将环境变量中 PIPENV_VENV_IN_PROJECT 的值设为 true,Pipenv 就会在工程目录下的 .venv 目录创建 virtualenv 环境。不过 Pipenv 也的确提供了足够的命令用来操作 virtualenv,所以不把 virtualenv 目录放在工程目录下也是可以接受的。

$ export PIPENV_VENV_IN_PROJECT=true
$ cd projectC
$ pipenv install
Creating a virtualenv for this project...
Virtualenv location: /root/projectD/.venv
$ pipenv --venv
/root/projectC/.venv

2019-02-13 更新:另外一个简单的办法是在项目根目录下手动创建 .venv 目录,然后再执行 pipenv install。Pipenv 会自动在工程目录下创建虚拟环境。

# 使用不同版本的 python 程序

Pipfile 中有一个可选的 python_version 字段:

[requires]
python_version = "3.7"

Pipenv 会自动地根据这个字段寻找相应的 Python 版本。如果找不到对应版本,而且安装了 pyenv 的情况下,Pipenv 还会询问你是否要使用 pyenv 安装特定版本的 Python,可以说是非常贴心了。

$ pipenv install
Warning: Python 3.7 was not found on your system...
Would you like us to install CPython 3.7.0 with pyenv? [Y/n]: y
Installing CPython 3.7.0 with pyenv (this may take a few minutes)...

有空写一篇博客介绍一下 pyenv

# 自定义脚本快捷命令

Pipenv 支持在 Pipefile 中的 [scripts] 表情内添加自定义的脚本命令,并通过 pipenv run <shortcut name> 的方式在 virtualenv 环境中执行对应的命令,哪怕在之前并没有手动地激活 virtualenv 环境。

比如在 Pipefile 中添加下面的代码:

[scripts]
test = "python3 -m unittest discover -s ./tests"
dev = "python3 manage.py runserver 0.0.0.0:8000"

然后执行 pipenv run test 就相当于执行了 pipenv run python3 -m unittest discover -s ./testspipenv run dev 相当于 pipenv run python3 manage.py runserver 0.0.0.0:8000。非常方便的功能。

# 继续学习 Pipenv

这篇文章只是一个简要的概述,如果大家希望能够深入了解 Pipenv,可以去看官网的 Further Documentation Guides (opens new window) 章节,应该可以覆盖绝大多数的使用场景了。

# 总结

虽然 virtualenv 不是 Pipenv 发明的,Pipfile 也不是它第一个使用的。但是 Pipenv 能把这些工具集合在一起,再加上极其人性化的设计,是解决 Python 包管理问题的不二之选。Pipenv 完全能配得上 Python 官方给他的褒奖和推荐,推荐大家亲自去使用和感受。


知识共享协议

本文采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议 (opens new window)进行许可。