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篇:模塊搜索路徑和包導入

(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM