Featured image of post 在Python中导入当前路径下的所有模块

在Python中导入当前路径下的所有模块

问题背景

在使用 Python 的 Flask 框架开发 Web 应用的过程中,一个基本的服务端程序结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from flask import Flask

app = Flask(__name__)

@app.route('/handler1')
def handler1():
    ...

@app.route('/handler2')
def handler2():
    ...

@app.route('/handler3')
def handler3():
    ...

可以按照这种模式无限添加处理视图(handler),但是随着项目增大,这种将所有 handler 都放在一个 py 文件里的模式显然是不合适的,这时可以使用 blueprint 将每个 handler(或一组 handler)放在互相独立的文件里。

项目结构如下:

1
2
3
4
5
6
7
.
├── app.py
└── services
    ├── __init__.py
    ├── login.py
    ├── register.py
    ...

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
""" === app.py === """
from flask import Flask
from services import blueprint

app = Flask(__name__)
app.register_blueprint(blueprint)


""" === services/__init__.py === """
from flask import Blueprint

blueprint = Blueprint('api', __name__)

# 为了在程序启动过程中能运行两个 handler 文件,需要在这里 import 它们
from . import login
from . import register


""" === services/login.py === """
from . import blueprint

@blueprint.route("/login")
def handle_login():
    # 处理登录逻辑
    ...

""" === services/register.py === """
from . import blueprint

@blueprint.route("/register", methods=["POST"])
def handle_register():
    # 处理注册逻辑
    ...

这样做存在两个问题:

  1. 每次新增一个文件都需要在__init__.py中添加相应的 import 语句,较为麻烦;
  2. PEP-8 中要求将 import 语句放在文件的顶部,__init__.py显然不符合(但必须如此),因而静态检查器有可能在此处报错(E402)。

于是就自然而然地想到了:如何在模块初始化时自动导入当前路径下的所有子模块呢?

解决方法

services/__init__.py改为这样:

1
2
3
4
5
6
7
8
import pkgutil
import importlib
from flask import Blueprint

blueprint = Blueprint('api', __name__)

for spec in pkgutil.iter_modules(path=__path__, prefix=''):
    importlib.import_module("."+spec.name, __name__)

原理探究

可以通过 pdb 或 print 获得这段程序所涉及变量的值:

1
2
3
4
__name__ = 'services'
__path__ = ['/tmp/test/services']
spec = ModuleInfo(module_finder=FileFinder('/tmp/test/services'), name='login', ispkg=False)
spec = ModuleInfo(module_finder=FileFinder('/tmp/test/services'), name='register', ispkg=False)

其中__name__为当前包的名字,__path__为文件所在文件夹的路径。 pkgutil模块的iter_modules函数会找到提供的path下的所有子模块,在这里就是loginregister,并返回它们的 ModuleInfo

在 for 循环内提取 ModuleInfo 的 name,得到模块的名字,加上前缀.,即当前目录下的对应子模块。 最后使用 importlib 的 import_module 函数导入子模块(即运行子模块的代码,将 handler 注册到 router 上)。这样无论增加多少 handler 文件,__init__.py 都可以找到并加载它们。

使用 Hugo 构建
主题 StackJimmy 设计