原文鏈接: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工程目錄結構,目錄之間自定義包模塊文件的引用
(完)