[英]What is the best practice for imports when developing a Python package?
我正在嘗試構建一個 Python package,其中包含子模塊和子包(“庫”)。 我到處都在尋找正確的方法,但令人驚訝的是我發現它非常復雜。 當然也經歷了 StackOverFlow 中的多個線程..
問題如下:
為了從另一個目錄導入模塊或 package,在我看來有 2 個選項:a。 將絕對路徑添加到sys.path
。 b. 使用setuptools.setup
function 在 package 的主目錄中的setup.py
文件中安裝 package - 將 package 安裝到正在使用的特定 Python 版本的site-packages
目錄中。
選項a對我來說似乎太笨拙了。 選項b很棒,但我發現它不切實際,因為我目前正在工作和編輯包的源代碼 - 當然,更改不會在 package 的安裝目錄上更新。 另外package的安裝目錄沒有被Git跟蹤,不用說我用的是Git原目錄。
總結這個問題:從當前正在建設中的 Python package 的子目錄中自由且良好地導入模塊和子包的最佳實踐是什么?
我覺得我遺漏了一些東西,但到目前為止找不到合適的解決方案。
謝謝!
這是一個很好的問題,我希望更多的人能按照這些思路思考。 在其他人可以輕松使用之前,絕對有必要使模塊可導入並最終可安裝。
在我回答之前,我會說當我對現有 package 結構之外的文件進行初始開發時,我確實使用了 sys.path munging。 我有一個編輯器片段可以構造如下代碼:
import sys, os
sys.path.append(os.path.expanduser('~/path/to/parent'))
from module_of_interest import * # NOQA
給定我使用的當前文件的路徑:
import ubelt as ub
fpath = ub.Path('/home/username/path/to/parent/module_of_interest.py')
modpath, modname = ub.split_modpath(fpath, check=False)
modpath = ub.Path(modpath).shrinkuser() # abstract home directory
為了構建必要的部分,代碼片段將插入到文件中,這樣我就可以在 IPython 中與它進行交互。 我發現花了一些額外的時間來刪除對我的顯式主文件夾的引用,這樣只要用戶具有與主目錄相同的相對路徑結構,代碼仍然可以工作,這使得它稍微更便攜。
話雖這么說, sys.path munging 不是一個可持續的解決方案。 最終,您希望您的 package 由 python package 經理管理。 我知道很多人使用詩歌,但我喜歡普通的舊 pip,所以我可以描述這個過程,但知道這不是唯一的方法。
為此,我們需要了解一些基礎知識 go。
你必須知道你在什么 Python 環境中工作。理想情況下,這是一個用pyenv (或conda或 mamba 或 poetry ...)管理的虛擬環境。 但也可以在您的全局系統 Python 環境中執行此操作,但不建議這樣做。 我喜歡在 my.bashrc 中始終激活的單一默認 Python 虛擬環境中工作。 它總是很容易切換到一個新的或吹走它/重新開始。
您需要考慮兩個根路徑:您的存儲庫的根目錄,我將其稱為您的 repo 路徑,以及您的根目錄到您的 package,package 路徑或模塊路徑,它應該是一個名稱為頂級 Python 的文件夾package。您將使用此名稱導入它。 此 package 路徑必須位於回購路徑內。 一些 repos,比如xdoctest ,喜歡將模塊路徑放在src
目錄中。 其他人,如ubelt ,喜歡在存儲庫的頂層擁有 repo 路徑。 我認為第二種情況對於新的 package 創建者/維護者來說在概念上更容易,所以讓我們 go 吧。
所以現在,你處於一個激活的 Python 虛擬環境中,我們已經指定了一個路徑,我們將檢查 repo。 我喜歡在$HOME/code
中克隆 repos,所以也許 repo 路徑是$HOME/code/my_project
。
在此回購路徑中,您應該有根路徑 package。 假設您的 package 名為 mypymod。 任何包含__init__.py
文件的目錄在概念上都是一個 python 模塊,其中__init__.py
的內容是您導入該目錄名稱時獲得的內容。 目錄模塊和普通文件模塊之間的唯一區別是目錄模塊/包可以有子模塊或子包。
例如,如果你在my_project
mypymod
ls
,你有一個看起來像這樣的文件結構......
+ my_project
+ mypymod
+ __init__.py
+ submod1.py
+ subpkg
+ __init__.py
+ submod2.py
您可以導入以下模塊:
import mypymod
import mypymod.submod1
import mypymod.subpkg
import mypymod.subpkg.submod2
如果您確保當前工作目錄始終是存儲庫根目錄,或者將存儲庫根目錄放入sys.path
,那么這就是您所需要的。 在sys.path
或 CWD 中可見是另一個模塊可以看到您的模塊所需要的。
現在的訣竅是:你如何確保你的其他包/腳本總能看到這個模塊? 這就是 package 經理的用武之地。為此,我們需要一個setup.py
或更新的pyproject.toml
變體。 我將描述舊的setup.py
做事方式。
您需要做的就是將setup.py
放在您的repo root中。 注意:它不在您的 package 目錄中的 go。 有很多關於如何編寫 setup.py的資源,所以我不會詳細描述它,但基本上您需要的只是用足夠的信息填充它,以便它知道 package 的名稱、它的位置和它的版本.
from setuptools import setup
setup(
name='mypymod',
version='0.1.0',
packages=find_packages(include=['mypymod', 'mypymod.*']),
install_requires=[],
)
所以你的 package 結構將如下所示:
+ my_project
+ setup.py
+ mypymod
+ __init__.py
+ submod1.py
+ subpkg
+ __init__.py
+ submod2.py
您可以指定很多其他內容,我建議您查看 ubelt 和 xdoctest 作為示例。 我會注意到它們包含從requirements.txt
或requirements/*.txt
文件中解析需求的非標准方式,我認為這通常比人們處理需求的標准方式要好。 但我離題了。
給定pip
或其他一些 package 管理器(例如 pipx、poetry)識別為package 清單的內容 - 一個描述 package 內容的文件,您現在可以安裝它。 如果您仍在開發它,您可以在可編輯模式下安裝它,而不是將 package 復制到您的站點包中,只創建一個符號鏈接,因此每次調用 Python(或立即調用)時,代碼中的任何更改都會反映出來如果你使用 IPython 自動重新加載)。
使用 pip 就像運行pip install -e <path-to-repo-root>
一樣簡單,這通常通過導航到 repo 並運行pip install -e.
.
恭喜,您現在有一個 package 可以從任何地方參考。
現在你有了一個 package,你可以引用它,就好像它是通過 pip 從 pypi 安裝的一樣。 有一些技巧可以有效地使用它。 第一個是運行腳本。
在 Python 中,您無需指定文件路徑即可將其作為腳本運行。可以僅使用其模塊名稱將腳本作為__main__
運行。 這是通過 Python 的-m
參數完成的。例如,您可以運行python -m mypymod.submod1
它將調用$HOME/code/my_project/mypymod/submod1.py
作為主模塊(即它的__name__
屬性將被設置為"__main__"
)。
此外,如果您想使用目錄模塊執行此操作,您可以在該目錄中創建一個名為__main__.py
的特殊文件,這就是將要執行的腳本。 例如,如果我們修改我們的 package 結構
+ my_project
+ setup.py
+ mypymod
+ __init__.py
+ __main__.py
+ submod1.py
+ subpkg
+ __init__.py
+ __main__.py
+ submod2.py
現在python -m mypymod
將執行$HOME/code/my_project/mypymod/__main__.py
和python -m mypymod.subpkg
將執行$HOME/code/my_project/mypymod/subpkg/__main__.py
。 這是使模塊兼作可導入 package 和命令行可執行文件(例如 xdoctest 執行此操作)的一種非常方便的方法。
您可能會注意到的一個痛點是,在上面的代碼中,如果您運行:
import mypymod
mypymod.submod1
你會得到一個錯誤,因為默認情況下 package 在導入之前不知道它的子模塊。 您需要填充__init__.py
以公開您希望在頂層訪問的任何屬性。 您可以使用以下內容填充mypymod/__init__.py
:
from mypymod import submod1
現在上面的代碼可以工作了。
不過這有一個折衷。 您立即訪問的東西越多,導入模塊所需的時間就越多,而且對於大包來說,它會變得相當麻煩。 此外,您還必須手動編寫代碼來公開您想要的內容,所以如果您想要一切,那將是一件痛苦的事情。
如果你看一下ubelt 的init .py ,你會發現它有大量代碼明確地使每個子模塊中的每個 function 都可以在頂層訪問。 我已經編寫了另一個名為mkinit的庫,它實際上自動執行了這個過程,它還可以選擇使用lazy_loader庫來減輕在頂層公開所有屬性對性能的影響。 我發現 mkinit 工具在編寫大型嵌套包時非常有用。
總結以上內容:
__init__.py
。mkinit
自動生成__init__.py
文件的內容。setup.py
/ pyproject.toml
放在“回購路徑”的根目錄中。pip install -e.
在開發時以可編輯模式安裝 package。python -m
將模塊名稱作為腳本調用。希望這可以幫助。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.