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篇:模块搜索路径和包导入

(完)

posted @ 2022-03-10 15:41  大师兄啊哈  阅读(1943)  评论(0编辑  收藏  举报