问题背景
在使用 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():
# 处理注册逻辑
...
|
这样做存在两个问题:
- 每次新增一个文件都需要在
__init__.py
中添加相应的 import 语句,较为麻烦; - 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下的所有子模块,在这里就是login
与register
,并返回它们的 ModuleInfo。
在 for 循环内提取 ModuleInfo 的 name,得到模块的名字,加上前缀.
,即当前目录下的对应子模块。
最后使用 importlib 的 import_module 函数导入子模块(即运行子模块的代码,将 handler 注册到 router 上)。这样无论增加多少 handler 文件,__init__.py
都可以找到并加载它们。