《PyCon2018》系列一: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
下一篇: ps怎么给图片中的人物添加外发光效果?