在這篇文章中,我想對 Python 中的依賴管理有所了解。Python 依賴管理是一個完全不同的世界。
20 多年來,我一直在為 JVM 開發代碼,首先是 Java,然後是 Kotlin。但是,JVM 並不是靈丹妙藥,例如,在腳本中:
- 虛擬機需要額外的內存
- 在許多情況下,腳本運行的時間不夠長,無法在性能方面獲得任何好處。位元組碼被解釋並且永遠不會編譯為本機代碼。
由於這些原因,我現在用 Python 編寫腳本。其中之一從不同來源收集社交媒體指標並將其存儲在 BigQuery 中以供分析。
我不是 Python 開發人員,但我正在學習 - 很難。在這篇文章中,我想對 Python 中的依賴管理有所了解。
Python 中足夠的依賴管理
在 JVM 上,依賴管理似乎是一個已解決的問題。首先,您選擇您的構建工具,最好是 Maven 或另一種我不應該命名的工具。然後,您聲明您的直接依賴關係,並且該工具管理間接依賴關係。這並不意味著沒有陷阱,但您可以或多或少地快速解決它們。
Python 依賴管理是一個完全不同的世界。首先,在 Python 中,運行時及其依賴項是系統範圍的。一個系統只有一個運行時,並且依賴項在該系統上的所有項目之間共享。因為不可行,所以開始一個新項目的第一件事就是創建一個虛擬環境。
這個問題的解決方案是創建一個虛擬環境,一個自包含的目錄樹,其中包含特定版本的 Python 的 Python 安裝,以及一些額外的包。
然後不同的應用程序可以使用不同的虛擬環境。為了解決前面的衝突需求示例,應用程序 A 可以擁有自己的安裝了 1.0 版的虛擬環境,而應用程序 B 擁有另一個安裝了 2.0 版的虛擬環境。如果應用程序 B 需要將庫升級到版本 3.0,這不會影響應用程序 A 的環境。
--虛擬環境和包
一旦完成,事情就會認真開始。
pipPython 提供了一個開箱即用的依賴管理工具:
您可以使用名為 pip 的程序安裝、升級和刪除軟體包。
--使用 pip 管理包
工作流程如下:
- 在虛擬環境中安裝所需的依賴項:pip install flask
- 在安裝了所有必需的依賴項後,將它們保存在requirements.txt按約定命名的文件中:pip freeze > requirements.txt
該文件應與常規代碼一起保存在一個人的VCS中。
- pip其他項目開發人員可以通過指向安裝相同的依賴項requirements.txt:pip install -r requirements.txt
以下是requirements.txt上述命令的結果:
click==8.1.3
Flask==2.2.2
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.1
Werkzeug==2.2.2
依賴和傳遞依賴
在描述這個問題之前,我們需要解釋一下什麼是傳遞依賴。傳遞依賴項是項目不直接需要的依賴項,而是項目的依賴項之一或依賴項的依賴項一直需要的依賴項。在上面的示例中,我添加了flask依賴項,但pip總共安裝了 6 個依賴項。
我們可以安裝deptree依賴來檢查依賴樹。
pip install deptree
輸出如下:
Flask==2.2.2 # flask
Werkzeug==2.2.2 # Werkzeug>=2.2.2
MarkupSafe==2.1.1 # MarkupSafe>=2.1.1
Jinja2==3.1.2 # Jinja2>=3.0
MarkupSafe==2.1.1 # MarkupSafe>=2.0
itsdangerous==2.1.2 # itsdangerous>=2.0
click==8.1.3 # click>=8.0
# deptree and pip trees
它的內容如下:Flaskrequires Werkzeug,這反過來又需要MarkupSafe。Werkzeug並MarkupSafe有資格作為我的項目的傳遞依賴項。
版本部分也很有趣。第一部分提到安裝的版本,而注釋部分是指兼容的版本範圍。例如Jinja需要版本3.0或以上,安裝的版本為3.1.2.
安裝的版本是安裝時找到的最新兼容pip版本。pip並deptree了解setup.py沿每個庫分發的文件的兼容性:
安裝腳本是使用 Distutils 構建、分發和安裝模塊的所有活動的中心。設置腳本的主要目的是向 Distutils 描述您的模塊分發,以便對您的模塊進行操作的各種命令執行正確的操作。
--編寫安裝腳本
flask在這裡:
from setuptools import setup
setup(
name="Flask",
install_requires=[
"Werkzeug >= 2.2.2",
"Jinja2 >= 3.0",
"itsdangerous >= 2.0",
"click >= 8.0",
"importlib-metadata >= 3.6.0; python_version < '3.10'",
],
extras_require={
"async": ["asgiref >= 3.2"],
"dotenv": ["python-dotenv"],
},
)
點和傳遞依賴
出現問題是因為我希望我的依賴項是最新的。為此,我已將 Dependabot 配置為監視requirements.txt. 當這樣的事件發生時,它會在我的 repo 中打開一個PR 。大多數時候,PR 就像一個魅力,但在少數情況下,當我在合併後運行腳本時會發生錯誤。它如下所示:
純文本1錯誤:libfoo 1.0.0 要求 libbar<2.5,>=2.0,但您將擁有不兼容的 libbar 2.5。
問題是 Dependabot 為列出的每個庫打開一個 PR。但是可以發布一個新的庫版本,這超出了兼容性的範圍。
想像一下下面的情況。我的項目需要libfoo依賴。反過來,libfoo需要libbar依賴。在安裝時,pip使用最新版本libfoo和最新兼容版本的libbar. 結果requirements.txt是:
純文本1libfoo==1.0.02庫==2.0
一切都按預期工作。過了一會兒,Dependabot 運行並發現libbar已經發布了一個新版本,例如2.5. 忠實地,它打開了一個 PR 來合併以下更改:
純文本1libfoo==1.0.02庫==2.5
是否出現上述問題僅取決於如何libfoo 1.0.0在setup.py. 如果2.5在兼容範圍內,則有效;如果沒有,它不會。
pip-compile拯救
問題pip在於它列出了傳遞依賴和直接依賴。Dependabot 然後獲取所有依賴項的最新版本,但不驗證傳遞依賴項版本更新是否在該範圍內。它可能會檢查,但requirements.txt文件格式不是結構化的:它不區分直接依賴和傳遞依賴。顯而易見的解決方案是僅列出直接依賴項。
好消息是pip只允許列出直接依賴項;它自動安裝傳遞依賴。壞消息是我們現在有兩個requirements.txt選項無法區分它們:一些僅列出直接依賴關係,而另一些則列出所有依賴關係。
它需要一個替代方案。pip-tools有一個:
- 一個在一個文件中列出了它們的直接依賴關係,該requirements.in文件的格式與requirements.txt
- 該pip-compile工具requirements.txt從requirements.in.
例如,給定我們的 Flask 示例:
#
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile requirements.in
#
click==8.1.3
# via flask
flask==2.2.2
# via -r requirements.in
itsdangerous==2.1.2
# via flask
jinja2==3.1.2
# via flask
markupsafe==2.1.1
# via
# jinja2
# werkzeug
werkzeug==2.2.2
# via flask
pip install -r requirements.txt
它具有以下好處和後果:
- 生成的requirements.txt包含注釋以了解依賴關係樹
- 由於pip-compile生成文件,您不應將其保存在 VCS 中
- 該項目與依賴於的遺留工具兼容requirements.txt
- 最後但同樣重要的是,它改變了安裝工作流程。不是安裝包然後保存它們,而是首先列出包然後安裝它們。
此外,Dependabot 可以管理pip-compile.
結論
這篇文章描述了默認 Python 的依賴管理系統以及它如何破壞自動版本升級。我們繼續描述pip-compile解決問題的替代方案。
請注意,Python 存在依賴管理規範,PEP 621 – 在 pyproject.toml 中存儲項目元數據。它類似於 Maven 的 POM,但格式不同。這在我的腳本上下文中是多餘的,因為我不需要分發項目。但是你應該這樣做,知道它pip-compile是兼容的。