俗话说,工欲善其事,必先利其器。我们写代码也是如此。在Python开发过程中,如何管理Python运行环境、package依赖关系是每个开发者都绕不过去的问题。在PyCon2018上,Kenneth Reitz介绍的Pipenv,就是用来解决这类问题的大杀器。
要想明白Kennenth Reitz为何开发Pipenv,还需要从Python的package管理工具的发展历史说起。
早期的Python提供了一个名为distutils的内置模块。借助这个模块,开发者可以为自己的package创建setup.py文件,再全部打包上传到网上。当用户想安装这个package时,需要先从网上把文件下载下来(通常是tar包之类的),解压,然后执行python setup.py install,即可将其安装到Python的site-packages目录下。
PyPI全称是Python Package Index,可以理解成一个集中式的索引,开发者们可以把他们的package及其metadata上传到这上面。有了PyPI之后,其他开发者就可以从这上面下载他们需要的package,然后执行python setup.py install进行安装。但即使这样,也还是存在着一些问题:
Setuptools的出现,弥补了distutils存在的一些缺陷并提供了更加丰富的功能。Setuptools可以看作是对distutils的一系列扩展,包括支持egg安装文件、自动化安装工具(easy_install)以及对distutils的monkey-patch。有了easy_install,用户想安装某个package的时候,只需要执行easy_install <package>,工具会自动把package及其依赖(默认从官方的PyPI)下下来进行安装。与之前的package安装方式相比,easy_install有以下优点:
至于缺点嘛,最主要的就是:没有easy_uninstall。也就是说,你只能用easy_install安装package,却没有相应的工具用来卸载。
到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管理工具了。
pip解决了单个环境下的(大部分)package管理问题,但是我们通常会在一台机器上同时开发多个项目,项目A需要Python2.7以及Flask0.9,项目B需要Python3.6以及Flask1.0,而项目C需要Python3.6以及Flask1.0.2。如此一来,我们就面临着两个方面的问题:
对于第一个问题,可以把所需要的Python都装上,给它们指定不同的alias,在开发不同项目时使用不同的alias。这个方法可以工作,但是很繁琐,而且容易出错,如果开发者忘了使用alias或者使用了错误的alias,可能就会把package安装到错误版本的Python下面。 对于第二个问题,单靠pip就更难解决了,因为同个版本Python的所有第三方package都在site-packages下面,没法区分不同版本。
为了解决上述问题,我们需要一个新的工具,那就是virtualenv。virtualenv可以为每个项目创建一套隔离的Python环境,从而保证系统里不同的Python环境之间不会相互影响。在每个隔离的环境下面,再使用pip进行package管理。pip+virtualenv是目前比较主流的Python开发流程。
前面提到,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"之间的不匹配。
为了解决"what you want"和"what you need"之间的不匹配问题,Pipfile这个新的标准被提了出来。
Pipfile被设计用来取代requirements.txt。其优点主要在于:
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。
Kenneth Reitz开发的Pipenv,将Pipfile,pip和virtualenv整合到了一起,让我们只使用这一个工具就可以非常方便、流畅地管理自己的Python环境。Pipenv的主要优点:
现在Pipenv已经是Python官方推荐的工作流(package管理+virtual env管理)工具了。
首先安装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