原文地址:Current State of Python Packaging - 2019
原文作者:Stefano Borini
譯文出自:掘金翻譯計(jì)劃
本文永久鏈接:github.com/xitu/gold-m…
譯者:EmilyQiRabbit
校對者:TokenJan,IT-rosalyn
在這篇文章中,我將會試著給你講清楚 python 打包那些錯(cuò)綜復(fù)雜的細(xì)節(jié)。我在過去的兩個(gè)月中,使用每天晚上精力最好的黃金時(shí)段盡可能多的收集相關(guān)信息、如今的解決方案,并搞清楚哪些是遺留的問題。
含糊不清的 python 術(shù)語是導(dǎo)致混亂的第一個(gè)來源。在編程相關(guān)的語境中,“包”(package)這個(gè)詞意味著一個(gè)可以安裝的組件(比如可以是一個(gè)庫)。但是在 python 中卻不是這樣,在這里,可安裝組件的術(shù)語是“發(fā)行版”(distribution)。但是,除非必要(特別是在官方文檔和 Python 增強(qiáng)提案中),否則根本沒人真的去用“發(fā)行版”這個(gè)術(shù)語。順便說一下,使用這個(gè)術(shù)語其實(shí)是個(gè)非常糟糕的選擇,因?yàn)椤癲istribution”一詞一般用來描述 Linux 的一個(gè) brand。
這是一個(gè)你應(yīng)該牢記于心的警告,因?yàn)?python 打包其實(shí)并不真的是關(guān)于 python 的包,而是關(guān)于它的發(fā)行版。但是我還是稱之為打包。
我不想花那么多時(shí)間去閱讀。能不能給我個(gè)簡短的版本?在 2019 年,我應(yīng)該如何管理 python 包呢?
我假設(shè)你是一名想要開始研發(fā)一個(gè) python 包程序員,步驟如下:
如果你真的想使用需要 setuptools 的老方法:
我的時(shí)間很充裕。請幫我解釋清楚吧。
首先我將闡述目前存在的問題,真的有很多問題。
假設(shè)我想要用 python 創(chuàng)建某“項(xiàng)目”:它也許是一個(gè)獨(dú)立程序,也許是一個(gè)庫。這個(gè)項(xiàng)目的開發(fā)和使用需要包含以下“角色”:
我們的目標(biāo)就是讓所有的用戶或者設(shè)備對該項(xiàng)目滿意,但是他們都有不同的工作流和需求,并且有時(shí)候這些需求會有重疊的部分。另外,當(dāng)項(xiàng)目發(fā)生更改、發(fā)布新版本、廢除舊版本,或者幾乎所有代碼都要依賴其他代碼來完成其任務(wù)的時(shí)候會產(chǎn)生問題。項(xiàng)目中必定存在依賴,而隨著時(shí)間推移,這些依賴會發(fā)生變化,它們也許是必要的也許也不是,它們可能在很底層運(yùn)行,所以我們必須考慮在不同操作系統(tǒng)甚至在同樣的操作系統(tǒng)中它們都可能是不可移植的。這已經(jīng)非常復(fù)雜了。
更糟糕的是,你的直接依賴也有各自的依賴集合。如果你的包直接依賴于 A 和 B,而它們兩個(gè)都依賴于 C 又會怎樣呢?你應(yīng)該安裝哪個(gè)版本的 C?如果 A 希望安裝 C 的嚴(yán)格版本 2 而 B 則希望安裝 C 的嚴(yán)格版本 1,是否可能做到呢?
為了一定程度上整治這種混亂,人們設(shè)計(jì)出代碼打包的方法,這樣代碼包就可以被復(fù)用、安裝、版本化并給出一些描述性的元信息,例如:“已在 windows 64 位系統(tǒng)上打包”,或者“僅適用于 macos 系統(tǒng)”,或者“需要該版本或以上才可運(yùn)行”。
好吧,現(xiàn)在我知道問題所在了。那么解決方案是什么呢?
第一步是定義一個(gè)集合了指定軟件指定發(fā)布版本的可交付實(shí)體。這個(gè)可交付實(shí)體就是我們所謂的包(或者專業(yè)的 python 說法是發(fā)行版)。你可以用兩種方式交付:
兩種方式都可能有用,通常情況下,兩種都提供是不錯(cuò)的選擇。當(dāng)然,我們需要能夠正確完成打包的工具,尤其是為了完成如下的任務(wù):
可以說得更詳細(xì)一些嗎?我寫代碼之前,必須要做什么呢?
當(dāng)然。在你寫代碼之前,通常你要完成如下步驟:
我需要使用什么工具來完成這些嗎?
這個(gè)不好說,因?yàn)楣ぞ叻浅6嗖⑶以诓粩嘧兓?。一個(gè)選擇是你可以使用 python 內(nèi)建的 venv 創(chuàng)建獨(dú)立的 python “虛擬環(huán)境”。然后使用 pip(也是 python 內(nèi)建工具)來安裝依賴的包。逐個(gè)輸入并安裝太麻煩了,所以人們通常會將具體依賴(硬編碼的版本號)寫入一個(gè)文件內(nèi)然后通知 pip:“讀取這個(gè)文件并安裝文件中寫明的所有包”。pip 就會照做了。這個(gè)文件就是人盡皆知的 requirements.txt,你可能已經(jīng)在其他項(xiàng)目里見過了。
好吧,可是 pip 到底是什么呢?
pip 是一個(gè)用來下載和安裝包的程序。如果這些包也有依賴,那么 pip 也會安裝這些子依賴的。
pip 是怎么做到的?
它會在遠(yuǎn)程服務(wù) pypi 上,通過名稱和版本號找到對應(yīng)的包并下載、安裝。如果這個(gè)包已經(jīng)是二進(jìn)制文件,那么只需要安裝它。如果是源代碼,pip 就會進(jìn)行編譯然后再安裝。但是 pip 做的還不止這些,因?yàn)檫@個(gè)包本身可能會有其他的依賴,所以它也會獲取這些依賴,并且安裝它們。
為什么你說使用 requirements.txt 的方法只是一個(gè)“選擇”?
因?yàn)檫@種方式會隨著項(xiàng)目擴(kuò)展而變得冗長而且復(fù)雜。對于不同的平臺,你需要手動管理直接依賴版本。例如,在 windows 系統(tǒng)你需要安裝某個(gè)包,而在 linux 或其他系統(tǒng)你則需要另外的包,那結(jié)果是你就需要同時(shí)維護(hù) win-requirements.txt、linux-requirements.txt 等等多個(gè)文件。
你還必須考慮到,一些依賴是你的軟件運(yùn)行所必需的;而其他只是用來運(yùn)行測試,這些依賴只是開發(fā)者或者 CI 設(shè)備必需的,但是對于其他使用你的軟件的人,其實(shí)并不需要,所以它們此時(shí)就不能作為項(xiàng)目的依賴了。因此,你就需要一個(gè)新的文件 dev-requirements.txt。
問題在于,requirements.txt 或許只會指定直接依賴,但是在實(shí)際應(yīng)用的時(shí)候,你想要定制好創(chuàng)建環(huán)境所需要的所有依賴。為什么要這樣?比方說,如果你安裝了直接依賴 A,而 A 又依賴于版本 1.1 的 C。但是有一天 C 發(fā)布了新版本 1.2,那么從此之后,當(dāng)你創(chuàng)建環(huán)境的時(shí)候,pip 就會下載可能帶有漏洞的 1.2 版本的 C。也就是忽然間你的測試無法通過了,但你又不知道為什么。
所以你就想在 requirements.txt 中同時(shí)指定依賴和這些依賴的子依賴。但是這樣的話,你在文件中卻無法區(qū)分出這兩種依賴了,那么當(dāng)某個(gè)依賴出現(xiàn)問題你想要調(diào)試它的時(shí)候,你就要找出文件中哪個(gè)才是它的子依賴,以及…
現(xiàn)在你懂了。真的一團(tuán)糟,你并不想去處理這樣的亂局吧。
接下來你會面臨的一個(gè)問題就是,pip 可以決定使用更加原始的方式來安裝哪個(gè)版本,這可能會讓它自己運(yùn)行到一個(gè)死胡同里,呈現(xiàn)給你的就是某個(gè)無法工作的環(huán)境或者是錯(cuò)誤。記住這個(gè)例子:包 A 和 B 都依賴于 C。因此你需要一個(gè)更加復(fù)雜的過程,在這個(gè)過程里,基本上使用 pip 僅僅是為了下載已經(jīng)定義好版本的包,而需要決定安裝什么版本的權(quán)限則交給其他程序,這個(gè)程序要有全局的考量,并能作出更明智的版本判定。
比如說?請給我舉個(gè)例子吧。
pipenv 就是一個(gè)例子。它將 venv、pip 和其他一些黑科技集合在一起,你只需給出直接依賴列表,它則會盡最大努力為你解決上文提到的混亂并給你交付一個(gè)可運(yùn)行的環(huán)境。Poetry 是另外一個(gè)例子。人們經(jīng)常會討論兩者,并且由于人為和政策的原因還會引起一些爭執(zhí)。但是大多數(shù)人更偏向于 Poetry。
一些公司如 Continuum 和 Enthought 都有他們自己的版本管理(即 conda 和 edm),它們通常都可以避免由于平臺不同而附加的依賴版本的復(fù)雜性。在這里我們就不展開講了。我只想說,如果你想要用那些很多已經(jīng)被編譯好的依賴關(guān)系或者(這些依賴關(guān)系)依賴于編譯好的庫,比如說在科學(xué)計(jì)算的場景下這種需求就很常見,那么你最好用它們的系統(tǒng)來管理你的環(huán)境,這會為你免去不少麻煩。因?yàn)檫@本來就是它們拿手的。
那么 pipenv 和 Poetry 究竟哪個(gè)更好用呢?
正如我剛才說的,人們更偏向于 Poetry。這兩個(gè)我都嘗試過,于我而言 Poetry 也要更好一些,它提供了更具兼容性、更優(yōu)質(zhì)的解決方案。
嗯好,所以至少我們要去用 Poetry,它可以為我們創(chuàng)建好環(huán)境,這樣我就可以安裝依賴并開始編程了。
沒錯(cuò)。但我還沒有談?wù)摰綐?gòu)建。也就是,一旦你有了代碼,你該如何創(chuàng)建發(fā)布版呢?
嗯是的,所以這就是 setup.py、setuptools 和 distutils 的用武之地了?
可以這么說,但也并不確切。最初情況下,當(dāng)你想要?jiǎng)?chuàng)建一個(gè)源代碼或者二進(jìn)制發(fā)行版的時(shí)候,你需要使用一個(gè)名為 distutils 的標(biāo)準(zhǔn)庫模塊。方法是使用一個(gè)名為 setup.py 的 python 腳本,它可以魔法般的創(chuàng)建出你可以交付給他人的項(xiàng)目。這個(gè)腳本可以任意命名,但 setup.py 是標(biāo)準(zhǔn)的命名方式,其他的工具(比如廣泛使用的 pip)就會只尋找以此命名的文件。而如果 pip 沒有找到需要依賴的可構(gòu)建版本,它將會下載源代碼并構(gòu)建它,簡單來說,只需運(yùn)行 setup.py,然后我們只能祈禱結(jié)果是好的了。
但是,distutils 并不好用,所以有些人找到了替代的方案,它可以做比 distutils 多得多的事。盡管挑戰(zhàn)很大,混亂很多,發(fā)展之路漫長,但是 setuptools 要更好,每個(gè)人都可以使用。如今 setuptools 還是使用 setup.py 文件,給人一種其實(shí)它們并沒有變化、創(chuàng)建環(huán)境的過程也保持不變的假象。
為什么說我們只能祈禱結(jié)果是好的?
因?yàn)?pip 并不能保證它運(yùn)行 setup.py 構(gòu)建的包是真的可以運(yùn)行的。它只是一個(gè) python 腳本,也許會有自己的依賴,而你又無法在出現(xiàn)問題的時(shí)候修改它的依賴或者進(jìn)行追蹤。這是先有雞還是先有蛋的問題了。
但是在 setuptools.setup() 中有 setup_requires 選項(xiàng)啊
這個(gè)方法就是個(gè)坑,你基本不能使用它解決什么問題。這還是個(gè)先有雞還是先有蛋的問題。PEP 518 對此進(jìn)行了詳細(xì)的討論,最后結(jié)論就是它就是渣渣。別用了。
所以 setuptools 和 setup.py 到底是不是構(gòu)建發(fā)布的可選方法呢??
過去是的。但現(xiàn)在不一定是了,只是或許有時(shí)候還可以用。這要看你要發(fā)布的內(nèi)容是什么了?,F(xiàn)在的情況是,沒人希望 setuptools 是唯一一種能決定包如何發(fā)布的方法。問題的根源要更深入一些,會涉及到一些技術(shù)型問題,但是如果你好奇,可以看一看 PEP 518。最重要的部分我在上文已經(jīng)提到了:如果 pip 想要構(gòu)建它下載的依賴,它該怎么確定下載哪個(gè)版本同時(shí)用來執(zhí)行 setup 腳本呢?沒錯(cuò),它可以假設(shè)需要依靠 setuptools,但也只是假設(shè)。而你的環(huán)境中可能并不需要 setuptools,那么 pip 又該怎么做決策?在更多情況下,為什么必須使用 setuptools 而不是其他的工具呢?
很多時(shí)候這決定了,任何想要寫自己的包管理工具的人應(yīng)該都可以這么做,因此你只需要另一個(gè)配置工具來定義使用哪個(gè)包系統(tǒng)以及你需要哪些依賴來構(gòu)建項(xiàng)目。
使用 pyproject.toml?
正確。更確切的來說,是一個(gè)可以在其中定義用來構(gòu)建包的“后端”的子節(jié)。如果你想要使用一種不同的構(gòu)建后端,pip 就可以完成。而如果你不想這樣,那么 pip 會假設(shè)你在使用工具 distutils 或者 setuptools,因此它就會退而尋找 setup.py 文件并執(zhí)行,我們祈禱它能構(gòu)建成功吧。
setup.py 最終到底會不會消失?setuptools(在它之前是 distutils)用 setup.py 來描述如何生成構(gòu)建。而其他工具或許會使用其他方法?;蛟S,它們會依賴于為 pyproject.toml 添加一些內(nèi)容而完成。
同時(shí),你終于可以在 pyproject.toml 中規(guī)定用來執(zhí)行構(gòu)建的依賴了,這就解除了前文說得那種先有雞還是先有蛋的難題。
為什么選擇 toml 格式的文件?我都還從來沒有聽說過它。為什么不用 JSON、INI 或者 YAML?
標(biāo)準(zhǔn)的 JSON 不允許寫注釋。但是人們真的很需要依賴注釋傳遞關(guān)于項(xiàng)目的信息。你可以不按照規(guī)則來,但那也就不是 JSON 了。另外,JSON 其實(shí)有些反人類,寫起來并讓人覺得不賞心悅目。
INI 則其實(shí)根本不是一種標(biāo)準(zhǔn)的寫法,而且它在功能上有很多限制。
YAML 則可能會成為你項(xiàng)目潛在的安全威脅,它簡直就像是病毒。
這樣的話選擇 toml 就可以理解了。但是,他們不能將 setuptools 包含在標(biāo)準(zhǔn)庫中嗎?
或許可以,但問題是標(biāo)準(zhǔn)庫的發(fā)布周期真的超級長。distutils 的更新非常緩慢,這正激發(fā)了 setuptools 的應(yīng)用和崛起。但是 setuptools 也不能保證滿足所有需求。一些包或許會有一些特殊的需求。
好吧,那么我這么理解是否正確:我需要使用 Poetry 創(chuàng)建工作環(huán)境。使用 setup.py 和 setuptools,或者 pyproject.toml 構(gòu)建包。
如果你想要使用 setuptools,你就需要 setup.py,但是你可能會遇到的問題是,其他用戶也需要安裝 setuptools 來構(gòu)建你的包。
那么除了 setuptools 我還能使用什么其他的工具呢?
可以用 flit,或者 Poetry。
Poetry 不需要安裝依賴嗎?
需要,但它也可以用來構(gòu)建。pipenv 就不行。
順便說一下,如果我使用 setup.py 的話,為什么我就必須寫明依賴呢?我下載的 setup.py 與 pipenv、Poetry 和 requirements.txt 有什么關(guān)系呢?
這些都是運(yùn)行包需要的抽象依賴,也是 pip 在決定下載和安裝哪些版本的時(shí)候需要的依賴。這里你應(yīng)當(dāng)放寬對依賴版本的限制,因?yàn)槿绻悴贿@樣…還記得我之前說過的 A 和 B 都依賴于 C 的例子嗎?如果 A 要求:“我要 1.2.1 版本的 C”,但是 B 要求:“我要 1.2.2 版本的 C”,那該怎么辦呢?
當(dāng)要構(gòu)建下載資源的源代碼發(fā)行版的時(shí)候,pip 沒有其他的選擇。pip 并不能獲取到你寫在 requirements.txt 文件中的需求。它只會去運(yùn)行 setup.py,而這會導(dǎo)致 pip 去使用 setuptools,然后再次調(diào)用 pip 來將抽象依賴解析為具體的可安裝依賴。
那么 eggs、easy install、.egg-info directories、distribute、virtualenv(這個(gè)不等于 venv)、zc.buildout、bento 這些工具又怎么樣呢?
忽略它們吧。它們要么是一些遺留工具或者其他工具的分支,要么是一些毫無結(jié)果的嘗試。
那 Wheels 呢?
還記得我之前說的嗎?pip 需要知道從 pypi 下載什么資源,從而才能下載正確的版本和操作系統(tǒng)。Wheel 就是一個(gè)包含了要下載資源的文件,并且有一些特殊的、規(guī)定好的字段,pip 安裝依賴和子依賴的時(shí)候會使用它們來決策。
Wheels 的文件名包含了作為元數(shù)據(jù)的標(biāo)簽(例如 pep-0425),所以當(dāng)某些資源(例如 CPython)被編譯了,Wheels 能知道編譯的版本、ABI 等等。文件名中的標(biāo)簽有一個(gè)標(biāo)準(zhǔn)層,元數(shù)據(jù)中特定的詞都有特定的含義。
記住,要為二進(jìn)制發(fā)行版構(gòu)建 wheels。
那么 .pyz 怎么樣呢?
忽略它就好,嚴(yán)格來講它和打包無關(guān)。但在其他某些方面它可能有用,如果你想知道更詳細(xì)的信息,可以看 PEP-441。
那么 pyinstaller 怎么樣呢?
Pyinstaller 是關(guān)于完全不同的另一個(gè)話題了。你看,“打包”這個(gè)單詞的問題是,它沒有清楚的表述出它真正的含義。到目前位置,我們討論了關(guān)于:
但是這些通常是應(yīng)用于庫的。而關(guān)于發(fā)行應(yīng)用,情況就不同了。當(dāng)你打包庫的時(shí)候,你知道它將會是一個(gè)更大的項(xiàng)目體的一部分。而當(dāng)你打包一個(gè)應(yīng)用,那么這個(gè)應(yīng)用就是那個(gè)更大的項(xiàng)目體。
另外,如果你想為人們提供應(yīng)用,那就應(yīng)指定應(yīng)用的平臺。例如,你想要提供一個(gè)帶圖標(biāo)的可執(zhí)行文件,但是在 Windows、macOS 和 Linux 平臺上,它們應(yīng)當(dāng)是有所不同的。
當(dāng)你想要?jiǎng)?chuàng)建一個(gè)獨(dú)立可執(zhí)行應(yīng)用的時(shí)候,PyInstaller 是可以使用的工具。它能夠?yàn)槟阍谟脩糇烂嫔蟿?chuàng)建出最終完成的應(yīng)用。打包是關(guān)于管理你需要用來創(chuàng)建應(yīng)用的依賴、庫和工具的網(wǎng)絡(luò),而創(chuàng)建這個(gè)應(yīng)用你可能會、也可能不會使用 pyinstaller。
注意不管怎樣,使用這個(gè)方法的前提是,假設(shè)你的應(yīng)用是比較簡單并且是自包含的。如果應(yīng)用在安裝的時(shí)候需要做更復(fù)雜的事情,比如創(chuàng)建 Windows 登錄密碼,那你就需要一個(gè)更合適的、更成熟的安裝器,比如 NSIS。我不知道在 Python 世界中是否有像 NSIS 這樣的東西。但無論如何,NSIS 都不知道你部署了什么。你當(dāng)然可以使用 pyinstaller 創(chuàng)建可執(zhí)行應(yīng)用,然后使用 NSIS 來部署它,并且還可以完成例如注冊表修改或者文件系統(tǒng)修改這樣的附加需求,讓應(yīng)用可以運(yùn)作。
好的,但是我如何安裝那些我已經(jīng)有資源包的項(xiàng)目呢?使用 python setup.py?
不對。用 pip install .,因?yàn)檫@個(gè)命令能保證你之后還可以卸載應(yīng)用,而且它總體上更好一些。pip 這時(shí)候會檢查 pyproject.toml 并在后臺運(yùn)行構(gòu)建。而如果 pip 沒有找到 pyproject.toml 文件,它就只好退回到老方法,運(yùn)行 setup.py 來嘗試構(gòu)建。
我很喜歡這篇文章,但是我還是有些問題沒有搞清楚
你可以自己開一個(gè) issue。如果我知道答案,我將會馬上為你解答。如果我不知道,我會做一下研究并盡快給你回復(fù)。我的目標(biāo)是這篇文章能讓人們最終理解 python 打包。