Python 打包——過去、現在與未來

英文 | Python packaging - Past, Present, Future【1】

原作 | BERNAT GABOR

譯者 | 豌豆花下貓

你是否想過在運行 pip install 時究竟發生了什麼?這篇文章將給你一個關於過去所涉及的步驟的詳細綜述,以及它是如何隨著 PEP-517 和 PEP-518 的採用而改變的。

在前一篇文章中,我描述瞭如何做到安裝三種類型的內容:源碼樹(source tree)、源發行版(source distribution)和 wheel。只有最後兩種類型會被上傳到 PyPi 中央存儲倉,但你也可以獲得源碼樹(例如,通過為 pip 加入 git 協議)。與其它類型相比,wheel 的優點是不需要在用戶機器上進行任何構建操作;只需要下載和提取。

構建 Python 包

現在可以獨立出構建的環境(用戶或開發者的機器),但你仍然需要構建包(sdist 或 wheel)。為了做到這一點,你需要一些適當的構建器。在過去,對第三方包的需求很早就表現出來了。

遵循內置電池的原則,在 2000 年的 Python 1.6 中,distutils【2】包被添加進 Python 標準庫中。它引入了包含構建邏輯的setup.py 文件的概念,並通過python setup.py 命令觸發。

它允許用戶將代碼打包成庫,但沒有聲明(declaration)及自動安裝依賴庫等功能。而且,它的升級週期直接與核心解釋器的發佈週期綁定。

setuptools 於 2004 年創建,它構建在 distutils 之上,並擴展了其它優秀的特性。它很快變得非常流行,以至於大多數 Python 安裝包開始將其與核心解釋器一起提供。

在那個時候,所有的包都是源發行版。wheel 分發方式出現得很晚,是在 2014 年。distutils 是在只有少數非常精通打包的人的時候創建的。因此它是非常靈活和命令式的(imperative),你寫一個 Python 腳本,可以修改包生成過程中的每一步。

但這樣做的缺點是,它一點也不容易學習和理解。隨著 Python 的流行,這開始成為一個越來越嚴重的問題,因為有越來越多的用戶對 Python 內部的工作原理不是很精通。

Python 打包——過去、現在與未來

Charles PH 攝/Unsplash--ehhh

構建依賴項

關於安裝一個源發行版,pip 主要做了以下工作:

  1. 找到這個包
  2. 下載源發行版並提取它
  3. 在提取的文件夾上運行python setup.py install(進行構建+安裝)

開發者運行python setup.py sdist 生成分發包,運行python setup.py upload 上傳到中央存儲倉(上傳命令在 2013 年被棄用了,因為有 twine【3】工具,更主要是因為 upload 使用了不安全的 HTTP 連接,而且上傳命令會做一次新的構建,也就不允許最終用戶在實際上傳之前檢測(inspect)生成的包)。

當 pip 運行python setup.py install時,它使用 Python 解釋器來安裝包。因此,構建操作可以訪問該解釋器中已經存在的所有三方包。最值得注意的是,它完全使用了安裝在主機 Python 解釋器上的 setuptools 版本。如果一個包使用了 setuptools 的新版本特性,那麼完成安裝的唯一方法就是首先更新已安裝的 setuptools。

如果新版本包含了能破壞其它包的 bug,就會導致出問題。在用戶無法更改已安裝包的系統上,這尤其麻煩。當構建器(例如 setuptools)希望使用其它輔助包(例如 cython)時,這也是個問題。

如果缺少構建器的輔助,通常會拋出導包失敗的錯誤:

<code>File"setup_build.py",line99,inrunfromCython.BuildimportcythonizeImportError:NomodulenamedCython.Build/<code>

在開發者們這邊,沒辦法提供此類構建依賴項。而對於用戶這邊,則需要預先安裝所有的包構建依賴,即使他們不會在運行時使用到。為了解決這個問題, PEP-518【4】被創建了。

其思想是,與其將主機的 Python 與其當前安裝的構建包一起使用,不如給軟件包提供一種能力,令其清楚地說明其構建操作所需的內容。另外,與其在主機 Python 上提供此功能,我們是創建了一個獨立的 Python(類似某種虛擬環境)來運行打包。

python setup.py install 現在可以:

  1. 創建一個臨時文件夾
  2. 創建一個隔離的(從三方庫的 site packages 中)Python 環境 python -m virtualenv our_build_env,讓我們將這個 Python 可執行文件稱為python_isolated
  3. 安裝構建的依賴項
  4. 通過python_isolated setup.py bdist_wheel,生成一個用於安裝的 wheel
  5. 提取 wheel 到 Python 的 site packages 文件夾

有了這個,我們可以安裝依賴於cython 的包,但不必在運行的 Python 環境中實際安裝cython。指定構建依賴項的文件與方法的是pyproject.toml元數據文件:

<code>[build-system]requires=["setuptools>=40.8.0","wheel>=0.30.0","cython>=0.29.4",]/<code>

此外,它還允許打包者指定他們需要的最小版本,而藉助用戶機器上的 pip,可以輕易地找出這些版本。

當在開發者的機器上生成源發行版或 wheel 時,也可以使用相同的機制。當一個人調用pip wheel . --no-deps命令時,該命令會自動在後臺創建一個包含構建依賴項的獨立 Python,然後在該環境中調用python setup.py bdist_wheel或python setup.py sdist 命令。

Python 打包——過去、現在與未來

Bruce Galpin攝/Unsplash--yay!

多樣的打包工具

但這裡還有一個問題。請注意,所有這些操作仍然須通過 20 年前引入的機制,即執行setup.py。整個生態系統仍然構建在 distutils 和 setuptools 的接口基礎之上,由於試圖保持向後兼容性,沒法作太大的變更。

此外,在打包過程中執行用戶端 Python 代碼是危險的,這可能會導致經驗較少的用戶難以調試的細微錯誤。命令式的(imperative)構建系統在 20 年前對於靈活性來說非常重要,當時我們還不知道所有的情況,但是現在我們已經認識清楚了,很可能可以為不同的情況創建出非常健壯和簡單的包構建器。

引用 Paul Ganssle【5】(setuptools 與 dateutil 的維護者)的話:

理想情況下,默認選項應該是一個聲明式的(declarative)構建配置,適用於 99% 的情況,再提供一個退回到命令式系統的選項,供真正需要靈活性時使用。在這情況下,如果你發現還需要選擇用命令式的構建,那麼我們可以認為出現了壞味道代碼。

setup.py 的最大的問題是大多數人是聲明式地使用它,所以當他們用命令式時,往往會將 bug 引入到構建系統。一個這樣的例子:如果你有一個 Python2.7 的依賴項,你可能會試圖有條件地在 setup.py 中指定 sys.version,但 sys.version 僅指的是執行構建的解釋器;相反,你應該對需求項使用聲明式的環境標記…

在 2015 年的引入的flit【6】已經證明了這一假設的正確性。它已經成為許多 Python 新手最喜歡的打包工具,因為它可以確保新用戶避免很多這樣的麻煩。然而,要達到這個目的,flit 必須再次構建在 distutils/setuptools 之上,這使得它的實現非常關鍵,並且代碼倉出現相當多的墊片層(例如,它仍然為源發行版生成 setup.py 文件)。

現在是時候把它從這些束縛中解放出來了,同時也鼓勵其他人構建自己的打包工具來簡化打包,是時候讓 setup.py 成為例外而不是默認的了。setuptools 計劃提供【7】一個用戶專用的setup.cfg 接口來起帶頭作用,當一個 PEP-517 系統就位時,在大多數情況下,你應該選擇它而不是使用 setup.py。

為了不把所有東西都綁定到 setuptools 和 distutils 上,並使後端的構建變得便利, PEP-517【8】被創建了。它將構建器分成後端和前端。前端提供了一個隔離的 Python 環境,滿足所有聲明的構建依賴項;後端提供了鉤子,被前端從其隔離環境中調用,以生成源發行版或者 wheel。

此外,我們不再通過 setup.py 文件或命令與後端通信,而是使用了 Python 模塊和函數。所有後端的打包必須提供一個 Python 對象 API,至少實現 build_wheel【9】和 build_sdist【10】兩個方法。該 API 對象是通過 pyproject.toml 文件指定的,使用build-backend 鍵值:

<code>[build-system]requires=["flit"]build-backend="flit.api:main"/<code> 

上述代碼對於前端意味著,你可以通過在隔離的 Python 環境中運行它來控制後端:

<code>importflit.apibackend=flit.api.main#buildwheelviabackend.build_wheel()#buildsourcedistributionviabackend.build_sdist()/<code>

由後端決定要在哪裡和怎樣公開自己的官方 API:

  1. flit【11】通過flit.buildapi實現
  2. setuptools【12】提供了兩種變體:setuptools.build_meta(後面會解釋原因)
  3. poetry【13】通過poetry.masonry.api實現

因為這些,我們就擁有了不再受 distutils 遺留決策約束的打包工具。

Python 打包——過去、現在與未來

Sarthak Dubey攝/Unsplash--更多 yay!

tox 和打包

tox 是一個測試工具【14】,大多數項目使用它來確保某個包在多個 Python 解釋器上的版本兼容性。它還可以輕鬆地創建 Python 環境,在裡面安裝被監測的包,從而更快地復現問題。

為了能夠測試一個包,它首先需要構建一個源發行版。雖然 PEP-518 和 PEP-517 都帶有好的意圖,但是在某些情況下,啟用它們可能會破壞打包過程。因此,當 tox 在 3.3.0 版本中添加隔離構建時,決定暫時不默認啟用它。你需要手動啟用它(可能會在今年晚些時候——2019 年的版本 4 中默認啟用)。

一旦你指定了一個pyproject.toml ,寫了適當的requires 和build-backend,你需要啟用tox.ini 中的isolated_build標誌:

<code>[tox]isolated_build=True/<code>

在此之後,在打包過程中【15】,tox 將在獨立的 Python 環境中為每個 PEP-518 提供構建依賴項,來構建源發行版,並調用 PEP-517 所述的構建後端。

若不啟用該功能,tox 將使用老方法構建源發行版,也就是使用安裝了 tox 的解釋器來調用python setup.py sdist命令。

Python 打包——過去、現在與未來

Matthew Henry攝/Unsplash--這裡沒有免費的午餐呢!

小結

Python 打包官方希望所有這些都是有意義的,並因此擁有一個更用戶友好的、防錯的(error proof )和健壯的構建。這些標準的規範是在 2015 年至 2017 年的長期主題中寫作並爭論出來的。這兩個提案(PEP-517/518)被認為是足夠好的,可以獲得最大的收益,但是一些不太主流的場景可能會被忽略。

如果你的情況是被忽略的,不要擔心,如果我們認為必要的話,PEP 在任何時候都是擁抱改進意見的。在本系列的下一篇文章中【16】,我將討論社區在發佈這兩個 PEP 時碰撞到的一些痛點。這些都是我們應該吸取的教訓,並且表明著我們仍有一些工作要做。還不是一切都完美,但我們正在變得更好。如果你可以幫幫忙,就加入打包社區吧,讓我們一起把事情做得更好!

附1:勘誤

在前一篇文章中,source distribution 被譯成“源碼分發”,但它還有一個更被人採用的譯法是“源發行版”,為了便於接受,所以本文已作修改。翻譯匆忙,如有錯誤,歡迎讀者指正!萬分感謝!PS:後續若有修正,會在知乎專欄修改,請關注“Python進階之旅”:https://zhuanlan.zhihu.com/pythonCat

附2:相關鏈接

[1] Python packaging - Past, Present, Future: https://www.bernat.tech/pep-517-518/

[2] distutils: https://packaging.python.org/key_projects/#distutils

[3] twine: https://pypi.org/project/twine/

[4] PEP-518 : https://www.python.org/dev/peps/pep-0518/

[5] Paul Ganssle: https://twitter.com/pganssle

[6] flit: https://pypi.org/project/flit/

[7] 計劃提供: https://github.com/pypa/setuptools/pull/1675

[8] PEP-517: https://www.python.org/dev/peps/pep-0517/

[9] build_wheel: https://www.python.org/dev/peps/pep-0517/#build-wheel

[10] build_sdist: https://www.python.org/dev/peps/pep-0517/#id9

[11] flit: https://flit.readthedocs.io/en/latest/

[12] setuptools: https://setuptools.readthedocs.io/en/latest/history.html#v40-8-0

[13] poetry: https://poetry.eustace.io/docs/pyproject/#poetry-and-pep-517

[14] 測試工具: https://tox.readthedocs.io/en/latest/

[15] 打包過程中: https://tox.readthedocs.io/en/latest/#system-overview

[16] 下一篇文章中: https://www.bernat.tech/growing-pain/


主要分享 Python基礎、Python進階、Python哲學、編程語言、書籍推薦等內容,另外還有官方 PEP 翻譯與優質外文的翻譯,值得關注一同學習。


分享到:


相關文章: