- 原文地址:Current State of Python Packaging - 2019
- 原文做者:Stefano Borini
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:EmilyQiRabbit
- 校對者:TokenJan,IT-rosalyn
在這篇文章中,我將會試着給你講清楚 python 打包那些錯綜複雜的細節。我在過去的兩個月中,使用天天晚上精力最好的黃金時段儘量多的收集相關信息、現在的解決方案,並搞清楚哪些是遺留的問題。html
含糊不清的 python 術語是致使混亂的第一個來源。在編程相關的語境中,「包」(package)這個詞意味着一個能夠安裝的組件(好比能夠是一個庫)。可是在 python 中卻不是這樣,在這裏,可安裝組件的術語是「發行版」(distribution)。可是,除非必要(特別是在官方文檔和 Python 加強提案中),不然根本沒人真的去用「發行版」這個術語。順便說一下,使用這個術語實際上是個很是糟糕的選擇,由於「distribution」一詞通常用來描述 Linux 的一個 brand。前端
這是一個你應該牢記於心的警告,由於 python 打包其實並不真的是關於 python 的包,而是關於它的發行版。可是我仍是稱之爲打包。python
我不想花那麼多時間去閱讀。能不能給我個簡短的版本?在 2019 年,我應該如何管理 python 包呢?linux
我假設你是一名想要開始研發一個 python 包程序員,步驟以下:android
若是你真的想使用須要 setuptools 的老方法:ios
requirements.txt
文件,在其中指定嚴格、具體(即指定某個版本)、直接的依賴。接下來你將會須要使用這個文件生成實際的工做環境。python -m venv
建立一個虛擬環境,激活該環境而後在該環境下使用 pip install -rrequirements.txt
命令安裝依賴。用這個環境來開發。dev-requirements.txt
文件,並一樣爲其安裝依賴。pip freeze >requirements-freeze.txt
而且之後也要用這個命令建立環境。個人時間很充裕。請幫我解釋清楚吧。git
首先我將闡述目前存在的問題,真的有不少問題。程序員
假設我想要用 python 建立某「項目」:它也許是一個獨立程序,也許是一個庫。這個項目的開發和使用須要包含如下「角色」:github
咱們的目標就是讓全部的用戶或者設備對該項目滿意,可是他們都有不一樣的工做流和需求,而且有時候這些需求會有重疊的部分。另外,當項目發生更改、發佈新版本、廢除舊版本,或者幾乎全部代碼都要依賴其餘代碼來完成其任務的時候會產生問題。項目中一定存在依賴,而隨着時間推移,這些依賴會發生變化,它們也許是必要的也許也不是,它們可能在很底層運行,因此咱們必須考慮在不一樣操做系統甚至在一樣的操做系統中它們均可能是不可移植的。這已經很是複雜了。macos
更糟糕的是,你的直接依賴也有各自的依賴集合。若是你的包直接依賴於 A 和 B,而它們兩個都依賴於 C 又會怎樣呢?你應該安裝哪一個版本的 C?若是 A 但願安裝 C 的嚴格版本 2 而 B 則但願安裝 C 的嚴格版本 1,是否可能作到呢?
爲了必定程度上整治這種混亂,人們設計出代碼打包的方法,這樣代碼包就能夠被複用、安裝、版本化並給出一些描述性的元信息,例如:「已在 windows 64 位系統上打包」,或者「僅適用於 macos 系統」,或者「須要該版本或以上纔可運行」。
好吧,如今我知道問題所在了。那麼解決方案是什麼呢?
第一步是定義一個集合了指定軟件指定發佈版本的可交付實體。這個可交付實體就是咱們所謂的包(或者專業的 python 說法是發行版)。你能夠用兩種方式交付:
兩種方式均可能有用,一般狀況下,兩種都提供是不錯的選擇。固然,咱們須要可以正確完成打包的工具,尤爲是爲了完成以下的任務:
能夠說得更詳細一些嗎?我寫代碼以前,必需要作什麼呢?
固然。在你寫代碼以前,一般你要完成以下步驟:
我須要使用什麼工具來完成這些嗎?
這個很差說,由於工具很是多而且在不斷變化。一個選擇是你可使用 python 內建的 venv 建立獨立的 python 「虛擬環境」。而後使用 pip(也是 python 內建工具)來安裝依賴的包。逐個輸入並安裝太麻煩了,因此人們一般會將具體依賴(硬編碼的版本號)寫入一個文件內而後通知 pip:「讀取這個文件並安裝文件中寫明的全部包」。pip 就會照作了。這個文件就是人盡皆知的 requirements.txt,你可能已經在其餘項目裏見過了。
好吧,但是 pip 究竟是什麼呢?
pip 是一個用來下載和安裝包的程序。若是這些包也有依賴,那麼 pip 也會安裝這些子依賴的。
pip 是怎麼作到的?
它會在遠程服務 pypi 上,經過名稱和版本號找到對應的包並下載、安裝。若是這個包已是二進制文件,那麼只須要安裝它。若是是源代碼,pip 就會進行編譯而後再安裝。可是 pip 作的還不止這些,由於這個包自己可能會有其餘的依賴,因此它也會獲取這些依賴,而且安裝它們。
爲何你說使用 requirements.txt 的方法只是一個「選擇」?
由於這種方式會隨着項目擴展而變得冗長並且複雜。對於不一樣的平臺,你須要手動管理直接依賴版本。例如,在 windows 系統你須要安裝某個包,而在 linux 或其餘系統你則須要另外的包,那結果是你就須要同時維護 win-requirements.txt、linux-requirements.txt 等等多個文件。
你還必須考慮到,一些依賴是你的軟件運行所必需的;而其餘只是用來運行測試,這些依賴只是開發者或者 CI 設備必需的,可是對於其餘使用你的軟件的人,其實並不須要,因此它們此時就不能做爲項目的依賴了。所以,你就須要一個新的文件 dev-requirements.txt。
問題在於,requirements.txt 或許只會指定直接依賴,可是在實際應用的時候,你想要定製好建立環境所須要的全部依賴。爲何要這樣?比方說,若是你安裝了直接依賴 A,而 A 又依賴於版本 1.1 的 C。可是有一天 C 發佈了新版本 1.2,那麼今後以後,當你建立環境的時候,pip 就會下載可能帶有漏洞的 1.2 版本的 C。也就是突然間你的測試沒法經過了,但你又不知道爲何。
因此你就想在 requirements.txt 中同時指定依賴和這些依賴的子依賴。可是這樣的話,你在文件中卻沒法區分出這兩種依賴了,那麼當某個依賴出現問題你想要調試它的時候,你就要找出文件中哪一個纔是它的子依賴,以及…
如今你懂了。真的一團糟,你並不想去處理這樣的亂局吧。
接下來你會面臨的一個問題就是,pip 能夠決定使用更加原始的方式來安裝哪一個版本,這可能會讓它本身運行到一個死衚衕裏,呈現給你的就是某個沒法工做的環境或者是錯誤。記住這個例子:包 A 和 B 都依賴於 C。所以你須要一個更加複雜的過程,在這個過程裏,基本上使用 pip 僅僅是爲了下載已經定義好版本的包,而須要決定安裝什麼版本的權限則交給其餘程序,這個程序要有全局的考量,並能做出更明智的版本斷定。
好比說?請給我舉個例子吧。
pipenv 就是一個例子。它將 venv、pip 和其餘一些黑科技集合在一塊兒,你只需給出直接依賴列表,它則會盡最大努力爲你解決上文提到的混亂並給你交付一個可運行的環境。Poetry 是另一個例子。人們常常會討論二者,而且因爲人爲和政策的緣由還會引發一些爭執。可是大多數人更偏向於 Poetry。
一些公司如 Continuum 和 Enthought 都有他們本身的版本管理(即 conda 和 edm),它們一般均可以免因爲平臺不一樣而附加的依賴版本的複雜性。在這裏咱們就不展開講了。我只想說,若是你想要用那些不少已經被編譯好的依賴關係或者(這些依賴關係)依賴於編譯好的庫,好比說在科學計算的場景下這種需求就很常見,那麼你最好用它們的系統來管理你的環境,這會爲你免去很多麻煩。由於這原本就是它們拿手的。
那麼 pipenv 和 Poetry 究竟哪一個更好用呢?
正如我剛纔說的,人們更偏向於 Poetry。這兩個我都嘗試過,於我而言 Poetry 也要更好一些,它提供了更具兼容性、更優質的解決方案。
嗯好,因此至少咱們要去用 Poetry,它能夠爲咱們建立好環境,這樣我就能夠安裝依賴並開始編程了。
沒錯。但我尚未談論到構建。也就是,一旦你有了代碼,你該如何建立發佈版呢?
嗯是的,因此這就是 setup.py、setuptools 和 distutils 的用武之地了?
能夠這麼說,但也並不確切。最初狀況下,當你想要建立一個源代碼或者二進制發行版的時候,你須要使用一個名爲 distutils 的標準庫模塊。方法是使用一個名爲 setup.py 的 python 腳本,它能夠魔法般的建立出你能夠交付給他人的項目。這個腳本能夠任意命名,但 setup.py 是標準的命名方式,其餘的工具(好比普遍使用的 pip)就會只尋找以此命名的文件。而若是 pip 沒有找到須要依賴的可構建版本,它將會下載源代碼並構建它,簡單來講,只需運行 setup.py,而後咱們只能祈禱結果是好的了。
可是,distutils 並很差用,因此有些人找到了替代的方案,它能夠作比 distutils 多得多的事。儘管挑戰很大,混亂不少,發展之路漫長,可是 setuptools 要更好,每一個人均可以使用。現在 setuptools 仍是使用 setup.py 文件,給人一種其實它們並無變化、建立環境的過程也保持不變的假象。
爲何說咱們只能祈禱結果是好的?
由於 pip 並不能保證它運行 setup.py 構建的包是真的能夠運行的。它只是一個 python 腳本,也許會有本身的依賴,而你又沒法在出現問題的時候修改它的依賴或者進行追蹤。這是先有雞仍是先有蛋的問題了。
可是在 setuptools.setup() 中有 setup_requires 選項啊
這個方法就是個坑,你基本不能使用它解決什麼問題。這仍是個先有雞仍是先有蛋的問題。PEP 518 對此進行了詳細的討論,最後結論就是它就是渣渣。別用了。
因此 setuptools 和 setup.py 究竟是不是構建發佈的可選方法呢??
過去是的。但如今不必定是了,只是或許有時候還能夠用。這要看你要發佈的內容是什麼了。如今的狀況是,沒人但願 setuptools 是惟一一種能決定包如何發佈的方法。問題的根源要更深刻一些,會涉及到一些技術型問題,可是若是你好奇,能夠看一看 PEP 518。最重要的部分我在上文已經提到了:若是 pip 想要構建它下載的依賴,它該怎麼肯定下載哪一個版本同時用來執行 setup 腳本呢?沒錯,它能夠假設須要依靠 setuptools,但也只是假設。而你的環境中可能並不須要 setuptools,那麼 pip 又該怎麼作決策?在更多狀況下,爲何必須使用 setuptools 而不是其餘的工具呢?
不少時候這決定了,任何想要寫本身的包管理工具的人應該均可以這麼作,所以你只須要另外一個配置工具來定義使用哪一個包系統以及你須要哪些依賴來構建項目。
使用 pyproject.toml?
正確。更確切的來講,是一個能夠在其中定義用來構建包的「後端」的子節。若是你想要使用一種不一樣的構建後端,pip 就能夠完成。而若是你不想這樣,那麼 pip 會假設你在使用工具 distutils 或者 setuptools,所以它就會退而尋找 setup.py 文件並執行,咱們祈禱它能構建成功吧。
setup.py 最終到底會不會消失?setuptools(在它以前是 distutils)用 setup.py 來描述如何生成構建。而其餘工具或許會使用其餘方法。或許,它們會依賴於爲 pyproject.toml 添加一些內容而完成。
同時,你終於能夠在 pyproject.toml 中規定用來執行構建的依賴了,這就解除了前文說得那種先有雞仍是先有蛋的難題。
爲何選擇 toml 格式的文件?我都還歷來沒有據說過它。爲何不用 JSON、INI 或者 YAML?
標準的 JSON 不容許寫註釋。可是人們真的很須要依賴註釋傳遞關於項目的信息。你能夠不按照規則來,但那也就不是 JSON 了。另外,JSON 其實有些反人類,寫起來並讓人以爲不賞心悅目。
INI 則其實根本不是一種標準的寫法,並且它在功能上有不少限制。
YAML 則可能會成爲你項目潛在的安全威脅,它簡直就像是病毒。
這樣的話選擇 toml 就能夠理解了。可是,他們不能將 setuptools 包含在標準庫中嗎?
或許能夠,但問題是標準庫的發佈週期真的超級長。distutils 的更新很是緩慢,這正激發了 setuptools 的應用和崛起。可是 setuptools 也不能保證知足全部需求。一些包或許會有一些特殊的需求。
好吧,那麼我這麼理解是否正確:我須要使用 Poetry 建立工做環境。使用 setup.py 和 setuptools,或者 pyproject.toml 構建包。
若是你想要使用 setuptools,你就須要 setup.py,可是你可能會遇到的問題是,其餘用戶也須要安裝 setuptools 來構建你的包。
那麼除了 setuptools 我還能使用什麼其餘的工具呢?
能夠用 flit,或者 Poetry。
Poetry 不須要安裝依賴嗎?
須要,但它也能夠用來構建。pipenv 就不行。
順便說一下,若是我使用 setup.py 的話,爲何我就必須寫明依賴呢?我下載的 setup.py 與 pipenv、Poetry 和 requirements.txt 有什麼關係呢?
這些都是運行包須要的抽象依賴,也是 pip 在決定下載和安裝哪些版本的時候須要的依賴。這裏你應當放寬對依賴版本的限制,由於若是你不這樣…還記得我以前說過的 A 和 B 都依賴於 C 的例子嗎?若是 A 要求:「我要 1.2.1 版本的 C」,可是 B 要求:「我要 1.2.2 版本的 C」,那該怎麼辦呢?
當要構建下載資源的源代碼發行版的時候,pip 沒有其餘的選擇。pip 並不能獲取到你寫在 requirements.txt 文件中的需求。它只會去運行 setup.py,而這會致使 pip 去使用 setuptools,而後再次調用 pip 來將抽象依賴解析爲具體的可安裝依賴。
那麼 eggs、easy install、.egg-info directories、distribute、virtualenv(這個不等於 venv)、zc.buildout、bento 這些工具又怎麼樣呢?
忽略它們吧。它們要麼是一些遺留工具或者其餘工具的分支,要麼是一些毫無結果的嘗試。
那 Wheels 呢?
還記得我以前說的嗎?pip 須要知道從 pypi 下載什麼資源,從而才能下載正確的版本和操做系統。Wheel 就是一個包含了要下載資源的文件,而且有一些特殊的、規定好的字段,pip 安裝依賴和子依賴的時候會使用它們來決策。
Wheels 的文件名包含了做爲元數據的標籤(例如 pep-0425),因此當某些資源(例如 CPython)被編譯了,Wheels 能知道編譯的版本、ABI 等等。文件名中的標籤有一個標準層,元數據中特定的詞都有特定的含義。
記住,要爲二進制發行版構建 wheels。
那麼 .pyz 怎麼樣呢?
忽略它就好,嚴格來說它和打包無關。但在其餘某些方面它可能有用,若是你想知道更詳細的信息,能夠看 PEP-441。
那麼 pyinstaller 怎麼樣呢?
Pyinstaller 是關於徹底不一樣的另外一個話題了。你看,「打包」這個單詞的問題是,它沒有清楚的表述出它真正的含義。到目前位置,咱們討論了關於:
可是這些一般是應用於庫的。而關於發行應用,狀況就不一樣了。當你打包庫的時候,你知道它將會是一個更大的項目體的一部分。而當你打包一個應用,那麼這個應用就是那個更大的項目體。
另外,若是你想爲人們提供應用,那就應指定應用的平臺。例如,你想要提供一個帶圖標的可執行文件,可是在 Windows、macOS 和 Linux 平臺上,它們應當是有所不一樣的。
當你想要建立一個獨立可執行應用的時候,PyInstaller 是可使用的工具。它可以爲你在用戶桌面上建立出最終完成的應用。打包是關於管理你須要用來建立應用的依賴、庫和工具的網絡,而建立這個應用你可能會、也可能不會使用 pyinstaller。
注意無論怎樣,使用這個方法的前提是,假設你的應用是比較簡單而且是自包含的。若是應用在安裝的時候須要作更復雜的事情,好比建立 Windows 登陸密碼,那你就須要一個更合適的、更成熟的安裝器,好比 NSIS。我不知道在 Python 世界中是否有像 NSIS 這樣的東西。但不管如何,NSIS 都不知道你部署了什麼。你固然可使用 pyinstaller 建立可執行應用,而後使用 NSIS 來部署它,而且還能夠完成例如註冊表修改或者文件系統修改這樣的附加需求,讓應用能夠運做。
好的,可是我如何安裝那些我已經有資源包的項目呢?使用 python setup.py?
不對。用 pip install .
,由於這個命令能保證你以後還能夠卸載應用,並且它整體上更好一些。pip 這時候會檢查 pyproject.toml 並在後臺運行構建。而若是 pip 沒有找到 pyproject.toml 文件,它就只好退回到老方法,運行 setup.py 來嘗試構建。
我很喜歡這篇文章,可是我仍是有些問題沒有搞清楚
你能夠本身開一個 issue。若是我知道答案,我將會立刻爲你解答。若是我不知道,我會作一下研究並儘快給你回覆。個人目標是這篇文章能讓人們最終理解 python 打包。
有沒有參考連接能讓我更深刻的學習呢?
固然,請見:
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。