《PyCon2018》系列一:Pipen

发布时间:2019-03-04 15:58:43编辑:auto阅读(2148)

    前言

    俗话说,工欲善其事,必先利其器。我们写代码也是如此。在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

关键字