1. 程式人生 > 其它 >python工程結構及模組匯入最佳實踐

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工程目錄結構,目錄之間自定義包模組檔案的引用

[3] Python 學習 第13篇:模組搜尋路徑和包匯入

(完)