前言
俗話說,工欲善其事,必先利其器。我們寫代碼也是如此。在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