欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

《PyCon2018》系列一:Pipenv

程序员文章站 2022-03-12 18:48:16
俗话说,工欲善其事,必先利其器。我们写代码也是如此。在Python开发过程中,如何管理Python运行环境、package依赖关系是每个开发者都绕不过去的问题。在PyCon2018上,Kenneth Reitz介绍的Pipenv,就是用来解决这类问题的大杀器。 ......

前言

俗话说,工欲善其事,必先利其器。我们写代码也是如此。在python开发过程中,如何管理python运行环境、package依赖关系是每个开发者都绕不过去的问题。在pycon2018上,kenneth reitz介绍的pipenv,就是用来解决这类问题的大杀器。

为何需要pipenv?

要想明白kennenth reitz为何开发pipenv,还需要从python的package管理工具的发展历史说起。

python packaging 历史

distutils

早期的python提供了一个名为distutils的内置模块。借助这个模块,开发者可以为自己的package创建setup.py文件,再全部打包上传到网上。当用户想安装这个package时,需要先从网上把文件下载下来(通常是tar包之类的),解压,然后执行python setup.py install,即可将其安装到python的site-packages目录下。

pypi

pypi全称是python package index,可以理解成一个集中式的索引,开发者们可以把他们的package及其metadata上传到这上面。有了pypi之后,其他开发者就可以从这上面下载他们需要的package,然后执行python setup.py install进行安装。但即使这样,也还是存在着一些问题:

  • 整个过程需要人工介入,不方便自动化
  • package都是全局安装的,没法同时安装同一package的两个不同版本
  • 过程繁琐,用户体验差
setuptools

setuptools的出现,弥补了distutils存在的一些缺陷并提供了更加丰富的功能。setuptools可以看作是对distutils的一系列扩展,包括支持egg安装文件、自动化安装工具(easy_install)以及对distutils的monkey-patch。有了easy_install,用户想安装某个package的时候,只需要执行easy_install <package>,工具会自动把package及其依赖(默认从官方的pypi)下下来进行安装。与之前的package安装方式相比,easy_install有以下优点:

  • 更好的用户安装体验
  • 绝大多数package都来自pypi
  • 更适合自动化

至于缺点嘛,最主要的就是:没有easy_uninstall。也就是说,你只能用easy_install安装package,却没有相应的工具用来卸载。

pip

到2008年,pip以easy_install替代者的身份出现了。虽然pip大部分也是建立在setuptools的各个部件之上,但它提供了比easy_install更加强大的功能,尤其是引入了requirements files的概念,使得用户可以非常方便地复制python环境。我们可以在一个环境里执行pip freeze > requirements.txt,将当前环境的package信息全部导出,然后在新的环境里执行pip install -r requirements.txt,pip便会解析、下载并安装这些package。当我们不需要某个package时,还可以执行pip uninstall <package>将其卸载。直到现在,pip早已成为最受python开发者青睐的package管理工具了。

virtualenv

pip解决了单个环境下的(大部分)package管理问题,但是我们通常会在一台机器上同时开发多个项目,项目a需要python2.7以及flask0.9,项目b需要python3.6以及flask1.0,而项目c需要python3.6以及flask1.0.2。如此一来,我们就面临着两个方面的问题:

  • 对于项目a和b或者项目a和c,如何区分它们所使用的不同版本的python以及快速切换?
  • 对于项目b和c,由于它们都使用python3.6,安装的第三方package都会放到python3.6的site-packages目录下面,那么如何区分它们所需的不同版本的flask?

对于第一个问题,可以把所需要的python都装上,给它们指定不同的alias,在开发不同项目时使用不同的alias。这个方法可以工作,但是很繁琐,而且容易出错,如果开发者忘了使用alias或者使用了错误的alias,可能就会把package安装到错误版本的python下面。
对于第二个问题,单靠pip就更难解决了,因为同个版本python的所有第三方package都在site-packages下面,没法区分不同版本。

为了解决上述问题,我们需要一个新的工具,那就是virtualenv。virtualenv可以为每个项目创建一套隔离的python环境,从而保证系统里不同的python环境之间不会相互影响。在每个隔离的环境下面,再使用pip进行package管理。pip+virtualenv是目前比较主流的python开发流程。

更进一步

前面提到,pip+virtualenv的工作方式成为了主流并延续至今。但是这种方式也有一些不足:

  • 新人(尤其是不懂unix相关概念的新人)很难弄清virtualenv的抽象层是什么样的
  • virtualenv的工作流程比较繁琐,对人来说不够自然,尽管virtualenv-wrapper的出现一定程度上缓解了这个问题
  • pip的requirements.txt过于简单,没法表示具体的依赖关系
  • 需要使用两个工具(pip+virtualenv)才能完成工作,不够便捷

下面是在只安装了flask的环境中执行pip freeze导出的requirements.txt。可以看到,里面包含了flask本身及其依赖,每个package的版本都是确定的,但是没法看出它们之间的具体依赖关系是怎样的。试想,如果我们想使用一个开源项目,看到这样一个requirements.txt,我们可能会误以为这个项目直接依赖了这些packages,但实际上它只是直接依赖了flask。

$ cat requirements.txt
click==6.7
flask==0.12.2
itsdangerous==0.24
jinja2==2.10
markupsafe==1.0
werkzeug==0.14.1

另一种requirements.txt的写法就是,我们只给定需要直接依赖的package名称,像下面这样。使用这种方式,我们一眼就能看出项目直接依赖了哪些package。但是这里有个问题,即flask及其依赖的版本是不确定的。如果过段时间某个依赖发布了新版本,你去新环境部署的时候pip就会给你装上新的版本,可能会导致你的代码没法工作。

$ cat requirements.txt
flask

以上就是kenneth的演讲中举的例子,用来说明"what you want"和"what you need"之间的不匹配。

pipfile & pipfile.lock

为了解决"what you want"和"what you need"之间的不匹配问题,pipfile这个新的标准被提了出来。

pipfile被设计用来取代requirements.txt。其优点主要在于:

  • 采用toml语法,相比requirements.txt表达能力更强
  • 默认支持两组依赖:[packages]和[dev-packages],可以将多个requirements.txt的内容合并到一个文件,方便管理
  • 可以通过pipfile.lock对环境进行明确、详细地描述

pipfile大致是这么个样子:

[[source]]  # source这部分指定从哪里获取package
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]  # default环境下需要的package
flask = "*"  # *表示任意版本,默认会安装最新版本

[dev-packages]  # dev环境下需要的package

[requires]
python_version = "3.6"  # 指定python版本

通过对pipfile进行处理,可以生成json格式的pipfile.lock,包含了所有依赖及其具体的版本号,还有每个release的hash。比如下面:

{
    "_meta": {
        "hash": {
            "sha256": "8ec50e78e90ad609e540d41d1ed90f3fb880ffbdf6049b0a6b2f1a00158a3288"
        },
        "pipfile-spec": 6,
        "requires": {
            "python_version": "3.6"
        },
        "sources": [
            {
                "name": "pypi",
                "url": "https://pypi.org/simple",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "click": {
            "hashes": [
                "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
                "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
            ],
            "version": "==6.7"
        },
        "flask": {
            "hashes": [
                "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
                "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
            ],
            "index": "pypi",
            "version": "==1.0.2"
        },
        "itsdangerous": {
            "hashes": [
                "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
            ],
            "version": "==0.24"
        },
        "jinja2": {
            "hashes": [
                "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
                "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
            ],
            "version": "==2.10"
        },
        "markupsafe": {
            "hashes": [
                "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
            ],
            "version": "==1.0"
        },
        "werkzeug": {
            "hashes": [
                "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
                "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
            ],
            "version": "==0.14.1"
        }
    },
    "develop": {}
}

大家可以理解成,pipfile只描述了你想要的package是哪些,是抽象而宽泛的,比如上面pipfile的例子描述了我们需要flask这个package。而pipfile.lock则是对你在实际运行环境里需要的package以及它们所有依赖的描述,是具体而明确的,比如上面pipfile.lock的例子描述了flask以及其依赖的具体信息,这样当我们想在新环境里运行我们的项目时,就可以按照这些信息来安装所有依赖的package,确保环境的一致性。实际上,很多语言的package管理工具都支持类似pipfile.lock这样的lockfile,比如node.js的yarn和npm,php的composer,rust的cargo以及ruby的bundler。

pipenv

kenneth reitz开发的pipenv,将pipfile,pip和virtualenv整合到了一起,让我们只使用这一个工具就可以非常方便、流畅地管理自己的python环境。pipenv的主要优点:

  • 可以让你无缝使用pipfile和pipfile.lock,保证每个依赖的信息都是明确的
  • 提供简洁的命令帮你操作virtualenv
  • 提供其他辅助工具,比如pipenv graph,可以显示项目完整的依赖关系

现在pipenv已经是python官方推荐的工作流(package管理+virtual env管理)工具了。

pipenv用法简介

首先安装pipenv:

codehub@ubuntu:~/workspaces$ pip install pipenv

然后我们创建一个workspace并切换到该目录下(我这里是~/workspaces/pipenv_demo),创建一个新的环境:

codehub@ubuntu:~/workspaces$ mkdir pipenv_demo
codehub@ubuntu:~/workspaces$ cd pipenv_demo
codehub@ubuntu:~/workspaces/pipenv_demo$ pipenv install

如果要指定python版本,可以使用--python参数:

codehub@ubuntu:~/workspaces/pipenv_demo$ pipenv --python /usr/local/bin/python3 install

创建完后,目录下就会生成pipfile和pipfile.lock两个文件:

codehub@ubuntu:~/workspaces/pipenv_demo$ ls
pipfile  pipfile.lock

下一步,我们安装requests:

codehub@ubuntu:~/workspaces/pipenv_demo$ pipenv install requests

安装完毕之后,我们pipfile就会变成下面这个样子:

codehub@ubuntu:~/workspaces/pipenv_demo$ cat pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
requests = "*"

[dev-packages]

[requires]
python_version = "3.6"

而pipfile.lock则是这样:

codehub@ubuntu:~/workspaces/pipenv_demo$ cat pipfile.lock
{
    "_meta": {
        "hash": {
            "sha256": "8739d581819011fea34feca8cc077062d6bdfee39c7b37a8ed48c5e0a8b14837"
        },
        "pipfile-spec": 6,
        "requires": {
            "python_version": "3.6"
        },
        "sources": [
            {
                "name": "pypi",
                "url": "https://pypi.org/simple",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "certifi": {
            "hashes": [
                "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
                "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
            ],
            "version": "==2018.8.24"
        },
        "chardet": {
            "hashes": [
                "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
                "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
            ],
            "version": "==3.0.4"
        },
        "idna": {
            "hashes": [
                "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
                "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
            ],
            "version": "==2.7"
        },
        "requests": {
            "hashes": [
                "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
                "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
            ],
            "index": "pypi",
            "version": "==2.19.1"
        },
        "urllib3": {
            "hashes": [
                "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
                "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
            ],
            "markers": "python_version < '4' and python_version != '3.2.*' and python_version != '3.1.*' and python_version >= '2.6' and python_version != '3.3.*' and python_version != '3.0.*'",
            "version": "==1.23"
        }
    },
    "develop": {}
}

运行pipenv graph可以将环境中的完整依赖打印出来:

codehub@ubuntu:~/workspaces/pipenv_demo$ pipenv graph
requests==2.19.1
  - certifi [required: >=2017.4.17, installed: 2018.8.24]
  - chardet [required: >=3.0.2,<3.1.0, installed: 3.0.4]
  - idna [required: >=2.5,<2.8, installed: 2.7]
  - urllib3 [required: >=1.21.1,<1.24, installed: 1.23]

这个时候,如果我们直接运行python交互模式,尝试import requests会报错,因为还没有激活virtual env:

codehub@ubuntu:~/workspaces/pipenv_demo$ python
python 3.6.6 (default, aug 25 2018, 10:34:56)
[gcc 5.4.0 20160609] on linux
type "help", "copyright", "credits" or "license" for more information.
>>> import requests
traceback (most recent call last):
  file "<stdin>", line 1, in <module>
modulenotfounderror: no module named 'requests'

pipenv提供了一个非常好用的命令:pipenv shell,用于激活virtual env:

codehub@ubuntu:~/workspaces/pipenv_demo$ pipenv shell
launching subshell in virtual environmentâ¦
 . /home/codehub/.local/share/virtualenvs/pipenv_demo-b6h7sxri/bin/activate
codehub@ubuntu:~/workspaces/pipenv_demo$  . /home/codehub/.local/share/virtualenvs/pipenv_demo-b6h7sxri/bin/activate
(pipenv_demo-b6h7sxri) codehub@ubuntu:~/workspaces/pipenv_demo$

可以看到,当激活virtual env后,命令行提示符前面多了'(pipenv_demo-b6h7sxri)',这个就相当于我们virtual env的id,表示我们现在处于这个virtual env下。再次尝试在交互模式中import requests,成功:

(pipenv_demo-b6h7sxri) codehub@ubuntu:~/workspaces/pipenv_demo$ python
python 3.6.6 (default, aug 25 2018, 10:34:56)
[gcc 5.4.0 20160609] on linux
type "help", "copyright", "credits" or "license" for more information.
>>> import requests
>>> print(requests)
<module 'requests' from '/home/codehub/.local/share/virtualenvs/pipenv_demo-b6h7sxri/lib/python3.6/site-packages/requests/__init__.py'>

当不需要virtual env时,只需要运行exit即可:

(pipenv_demo-b6h7sxri) codehub@ubuntu:~/workspaces/pipenv_demo$ exit
codehub@ubuntu:~/workspaces/pipenv_demo$

通常我们需要把pipfile和pipfile.lock也加到版本管理中,以能保证同一个项目的不同开发者的python环境保持一致。比如我们新加入了一个项目,就可以把repo clone下来,直接运行pipenv install,pipenv会自动找到已存在的pipfile和pipfile.lock,并根据里面的信息来安装依赖,这样我们就能准确无误地复制其他人的环境了。

总结

就像kenneth reitz演讲标题所写的那样,pipenv是python依赖管理的未来。作为一名合格的python开发者,还是有必要学习下这个工具,提升自己的工作效率,也享受更好的工作体验。

参考

pipenv - the future of python dependency management