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工程目录结构,目录之间自定义包模块文件的引用
(完)