python工程結構及模組匯入最佳實踐
原文連結:https://www.cnblogs.com/harrymore/p/15989783.html
1. 工程結構
參考了一些博主和專案經驗,總結出的一套比較通用的結構,如下:
FastPro/ |-- scripts/ | |— run.sh |-- logs/ | |-- 2022-3-10.log |-- src/ | |-- tests/ | | |-- __init__.py | | |-- test_main.py| |-- main.py
| |-— config.py |-- docs/ | |-- data_api.md
| |-- syscfg.yaml|-- setup.py |-- requirements.txt |—- README.md
scripts: 也可以命名為bin,存放一些可執行檔案,如指令碼,我一般用來存放專案的啟停管理指令碼。
logs: 存放日誌檔案。
src: 存放python原始碼,入口檔案最好命名為main.py。網上也有建議不要直接命名為src的,因為我是使用vscode作為編輯器,一些補全和高亮程式碼等外掛在尋找路徑的時候會預設將src新增到路徑中,如果用其他名字的話需要修改配置。
src/tests:存放單元測試指令碼。
docs: 存放文件和配置檔案。
setup.py: 專案安裝指令碼。
requirements.txt: 專案依賴。
README.md: 專案說明檔案。
2. 模組匯入的常見問題
在開發過程中,同事經常會反映找不到模組的問題。其實大部分是因為沒有理解python的模組搜尋路徑。
關於模組搜尋路徑:
在執行python指令碼的時候,當一個模組被匯入的時候,直譯器首先會去找內建模組,如果找不到再去sys.path的值中尋找是否有該模組。sys.path是一個環境變數,預設包括以下路徑:
- 被呼叫指令碼所在目錄。
- PATHONPATH目錄:具體是PATHONPATH環境變數中配置的目錄,是第二個被搜尋的目錄,Python會從左到右搜尋PATHONPATH環境變數中設定的所有目錄。
- 標準連結庫目錄:Python按照標準模組的目錄,是在安裝Python時自動建立的目錄,譬如sitepackages。
- 路徑檔案中的路徑:
在模組搜尋目錄中(指令碼所在目錄,…/sitepackages目錄等),建立路徑檔案,字尾名為.pth,該檔案每一行都是一個有效的目錄。Python會讀取路徑檔案中的內容,每行都作為一個有效的目錄,載入到模組搜尋路徑列表中。簡而言之,當路徑檔案存放到搜尋路徑中時,其作用和PYTHONPATH環境變數的作用相同。
一般在一些複雜的專案中,有些模組引用到其他專案的模組,有的人往往用.pth去新增搜尋路徑,但是這種做法是有問題的,問題在於不知道另外一個專案什麼時候會被改動過。試過有同事把.pth放在標準庫所在的sitepackges目錄,有個模組突然就報錯了,後面才發現引用了另外一個專案,那個專案又修改了一些東西。更好的辦法是將子模組都放到當前的專案中。
說回搜尋路徑,正常情況下,專案入口為main.py,相當於把專案路徑”FastPro/src” 加到搜尋路徑了,這個時候其他的模組都按照這個路徑去匯入模組即可:
main.py:
import sys print("file:{},sys.path:{}".format(__file__, sys.path)) from mod1.my_api import print_date if __name__ == "__main__": print_date()
my_api.py:
import config def print_date(): print("today is: ", config.start_date)
config.py:
start_date = "2022-3-10 11:59:35"
執行:python main.py,輸出:
file:main.py, sys.path:[‘D:\\1-Work\\python_src\\python_egn\\src’, ‘…’, 此處省略多個路徑]
today is: 2022-3-10 11:59:35
其中__file__為當前檔案路徑,後面會用到。
如果這個時候,如果my_api.py由另外一個人撰寫,他想測試他的函式是否能夠正常執行,直接在本檔案進行測試:
import sys
print("file:{},sys.path:{}".format(__file__, sys.path))
import config def print_date(): print("today is: ", config.start_date) if __name__ == "__main__": print_date()
在src目錄下,執行:python python mod1/my_api.py
發現報錯:
file:mod1/my_api.py,sys.path:['D:\\1-Work\\python_src\\python_egn\\src\\mod1',‘…’, 此處省略多個路徑]
Traceback (most recent call last):
File "mod1/my_api.py", line 3, in <module>
import config
ModuleNotFoundError: No module named 'config'
可以發現搜尋路徑是在src/mod1,因此在這個目錄下並沒有發現config模組。
機智的小夥伴,可能會試著通過相對匯入路徑去匯入上層的模組:
from ..config import start_date
如果這麼做你會發現報另外一個錯誤:
ValueError: attempted relative import beyond top-level package
主要原因是越過當前目錄,去找上級目錄的包了,這樣造成了報錯。但是事情又不是這麼簡單,關於相對匯入,其實python做得挺複雜的,譬如它有點像相對路徑,但是不能引用不是包的目錄;相對匯入只適用於在合適的包中的模組,在頂層的指令碼的簡單模組中,它們將不起作用。這裡不作展開,詳情可以參考:使用相對路徑名匯入包中子模組。
聰明的做法是把測試邏輯全部放在tests模組中,譬如在tests目錄中建立一個專門測試mod1模組的測試指令碼:
test_mod1.py:
import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from mod1.my_api import print_date if __name__ == "__main__": print_date()
為了不影響工程的正常邏輯,我們在測試程式碼中將專案原始碼的頂層目錄加入搜尋路徑中。(也可以將這個邏輯獨立成path.py檔案,其他測試檔案import這個檔案即可)
執行測試:python ./tests/test_mod1.py,正常執行。
3. 總結
- python工程程式碼最好按照不同的功能存放和組織在不同的目錄中。
- 避免引入不可控的外部依賴。
- 子模組全部使用單元測試模組進行測試。
4. 參考
[1] 使用相對路徑名匯入包中子模組
[2] Python工程目錄結構,目錄之間自定義包模組檔案的引用
(完)