1. 程式人生 > 程式設計 >[譯] Python 的打包現狀(寫於 2019 年)

[譯] Python 的打包現狀(寫於 2019 年)

Python 的打包現狀(寫於 2019 年)

在這篇文章中,我將會試著給你講清楚 python 打包那些錯綜複雜的細節。我在過去的兩個月中,使用每天晚上精力最好的黃金時段儘可能多的收集相關資訊、如今的解決方案,並搞清楚哪些是遺留的問題。

含糊不清的 python 術語是導致混亂的第一個來源。在程式設計相關的語境中,“包”(package)這個詞意味著一個可以安裝的元件(比如可以是一個庫)。但是在 python 中卻不是這樣,在這裡,可安裝元件的術語是“發行版”(distribution)。但是,除非必要(特別是在官方檔案和 Python 增強提案中),否則根本沒人真的去用“發行版”這個術語。順便說一下,使用這個術語其實是個非常糟糕的選擇,因為“distribution”一詞一般用來描述 Linux 的一個 brand。

這是一個你應該牢記於心的警告,因為 python 打包其實並不真的是關於 python 的,而是關於它的發行版。但是我還是稱之為打包。

我不想花那麼多時間去閱讀。能不能給我個簡短的版本?在 2019 年,我應該如何管理 python 包呢?

我假設你是一名想要開始研發一個 python 包程式設計師,步驟如下:

  • 首先使用 Poetry 建立開發環境,並使用嚴格模式指定專案的直接依賴。這樣就可以保證你的研發和測試環境總是可以被重複建立的。
  • 建立一個 pyproject.toml 檔案,然後使用 poetry 作為後端建立原始碼版和二進位制發行版。
  • 下一步要指定抽象包依賴。注意應指定你能確定的該包可執行的最低版本。這樣就可以保證不會創建出無用的、會和其他包衝突的版本。

如果你真的想使用需要 setuptools 的老方法:

  • 建立 setup.py 檔案,在檔案中指定所有的抽象依賴,並在 install_requires 中指定這些依賴使用可工作的最低版本。
  • 建立 requirements.txt 檔案,在其中指定嚴格、具體(即指定某個版本)、直接的依賴。接下來你將會需要使用這個檔案生成實際的工作環境。
  • 使用命令 python -m venv 建立一個虛擬環境,啟用該環境然後在該環境下使用 pip install -rrequirements.txt 命令安裝依賴。用這個環境來開發。
  • 如果你需要用於測試的依賴(當然這也是非常有可能的事情),那麼你需要建立一個 dev-requirements.txt
    檔案,並同樣為其安裝依賴。
  • 如果你需要將所有環境配置凍結(這是推薦的做法),執行 pip freeze >requirements-freeze.txt 並且以後也要用這個命令建立環境。

我的時間很充裕。請幫我解釋清楚吧。

首先我將闡述目前存在的問題,真的有很多問題。

假設我想要用 python 建立某“專案”:它也許是一個獨立程式,也許是一個庫。這個專案的開發和使用需要包含以下“角色”:

  • 開發者:負責寫程式碼的人或者團隊。
  • CI:測試這個專案的自動化過程。
  • 構建:從我們的 git 倉庫到其他人可以安裝使用這個專案的自動或半自動過程。
  • 終端使用者:最終使用這個專案的人或者團隊。如果這個專案是一個庫,那麼終端使用者也許是其他開發者;或者如果是一個應用,終端使用者可能就是普通民眾。又或者這個專案是某一種網路服務,那麼終端使用者就是雲端計算微服務。當然還有很多可能,你明白我的意思,不一一列舉了。

我們的目標就是讓所有的使用者或者裝置對該專案滿意,但是他們都有不同的工作流和需求,並且有時候這些需求會有重疊的部分。另外,當專案發生更改、釋出新版本、廢除舊版本,或者幾乎所有程式碼都要依賴其他程式碼來完成其任務的時候會產生問題。專案中必定存在依賴,而隨著時間推移,這些依賴會發生變化,它們也許是必要的也許也不是,它們可能在很底層執行,所以我們必須考慮在不同作業系統甚至在同樣的作業系統中它們都可能是不可移植的。這已經非常複雜了。

更糟糕的是,你的直接依賴也有各自的依賴集合。如果你的包直接依賴於 A 和 B,而它們兩個都依賴於 C 又會怎樣呢?你應該安裝哪個版本的 C?如果 A 希望安裝 C 的嚴格版本 2 而 B 則希望安裝 C 的嚴格版本 1,是否可能做到呢?

為了一定程度上整治這種混亂,人們設計出程式碼打包的方法,這樣程式碼包就可以被複用、安裝、版本化並給出一些描述性的元資訊,例如:“已在 windows 64 位系統上打包”,或者“僅適用於 macos 系統”,或者“需要該版本或以上才可執行”。

好吧,現在我知道問題所在了。那麼解決方案是什麼呢?

第一步是定義一個集合了指定軟體指定釋出版本的可交付實體。這個可交付實體就是我們所謂的(或者專業的 python 說法是發行版)。你可以用兩種方式交付:

  • 原始碼:將原始碼打包為 zip 或者 tar.gz 格式的檔案,然後由使用者自己編譯。
  • 二進位制檔案:由你編譯程式碼,然後釋出編譯好的內容,使用者可以直接使用,無需附加步驟。

兩種方式都可能有用,通常情況下,兩種都提供是不錯的選擇。當然,我們需要能夠正確完成打包的工具,尤其是為了完成如下的任務:

  • 建立可交付的包(也就是前文提到的構建
  • 將包釋出在某處,這樣其他人就可以獲取到
  • 下載並安裝包
  • 處理依賴。如果包 A 需要包 B 才能執行怎麼辦?如果包 A 需不需要包 B 取決於你如何使用 A?如果包 A 只在 windows 上被安裝時才需要包 B?
  • 定義執行時間。如前文所述,通常情況下一個小小的軟體也需要很多依賴才能執行,並且這些依賴最好和其他軟體的依賴需求隔離開。不管是當你進行開發的時候還是執行的時候,都應該這樣。

可以說得更詳細一些嗎?我寫程式碼之前,必須要做什麼呢?

當然。在你寫程式碼之前,通常你要完成如下步驟:

  1. 建立一個獨立於系統 python 的 python 環境。這樣你可以同步研發多個專案。而且如果不這樣操作,A 專案的內容和 B 專案的內容可能會混在一起。
  2. 如果你想要規定專案的依賴,那麼請牢記有兩種方式可以完成:抽象方式,此時你只需要籠統地指出需要那些依賴(例如 numpy),以及具體方式,這時候你必須要規定版本號(例如 numpy 1.1.0)。至於為什麼會有這樣的區分,後文會詳細說明。如果你想要建立一個可執行的開發環境,需要具體地規定依賴。
  3. 現在你已經做完了需要做的,可以開始研發了。

我需要使用什麼工具來完成這些嗎?

這個不好說,因為工具非常多並且在不斷變化。一個選擇是你可以使用 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 是關於完全不同的另一個話題了。你看,“打包”這個單詞的問題是,它沒有清楚的表述出它真正的含義。到目前位置,我們討論了關於:

  1. 建立一個可以開發庫的環境
  2. 把你建立的專案構建為其他人也可以使用的格式

但是這些通常是應用於庫的。而關於發行應用,情況就不同了。當你打包庫的時候,你知道它將會是一個更大的專案體的一部分。而當你打包一個應用,那麼這個應用就是那個更大的專案體

另外,如果你想為人們提供應用,那就應指定應用的平臺。例如,你想要提供一個帶圖示的可執行檔案,但是在 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 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄