首页
/ Flask 应用工厂模式:从 create_app 原理到 CLI、扩展与测试的完整实践

Flask 应用工厂模式:从 create_app 原理到 CLI、扩展与测试的完整实践

2026-09-04 12:42:22作者:宣海椒Queenly

应用工厂(Application Factories)是 Flask 官方推荐的组织方式:把 Flask 应用的创建过程封装进一个工厂函数,从而获得可测试、可多实例、可扩展的应用构建能力。本文以 Flask 仓库文档 appfactories.rst 为主线,覆盖工厂函数的基本写法、在蓝图内访问应用的技巧、扩展的 init_app 解耦模式,并结合 src/flask/cli.py 的源码逐层剖析 flask --app 命令是如何发现、调用工厂函数并传入参数的,最后以教程项目 flaskr 的真实代码说明工厂模式如何支撑单元测试。

一、为什么需要应用工厂

Flask 应用通常由包(packages)和蓝图(blueprints,见 docs/blueprints.rst)组成。一种常见但有限制的做法是在导入蓝图时直接创建应用对象。工厂模式的核心改动只有一步:把应用对象的创建从模块顶层移入一个函数。这样后续就可以创建多个该应用的实例。

官方文档给出了两个核心动机:

  1. 测试。可以创建配置不同的多个应用实例来测试各种情况(如测试模式、不同的数据库路径);
  2. 多实例。可以在同一个应用进程中运行同一应用的多个实例(不同的配置),而不仅仅是在 Web 服务器层面配置多个实例。

二、基本工厂写法

工厂函数的基本形态是把整个应用的搭建过程放进一个函数里,接受配置文件名作为参数:

def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_pyfile(config_filename)

    from yourapplication.model import db
    db.init_app(app)

    from yourapplication.views.admin import admin
    from yourapplication.views.frontend import frontend
    app.register_blueprint(admin)
    app.register_blueprint(frontend)

    return app

其中 app.config.from_pyfile 的实现在 src/flask/config.py:它把文件名定位到应用包内(或 instance_path 内),执行该文件后从中收集大写变量并写入配置,支持 silent=True 忽略文件缺失——这一参数在教程项目里用于可选加载实例配置。

三、蓝图内无法直接拿到 app:使用 current_app

工厂模式带来一个限制:在导入时不能直接使用应用对象(因为它还不存在),但可以在请求处理期间使用。官方给出的方式是使用 current_app

from flask import current_app, Blueprint, render_template
admin = Blueprint('admin', __name__, url_prefix='/admin')

@admin.route('/')
def index():
    return render_template(current_app.config['INDEX_TEMPLATE'])

这里从配置中查出模板名。从源码看,current_app 并不是一个普通对象,而是 src/flask/globals.py 中基于 ContextVarLocalProxy

_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx")
current_app: FlaskProxy = LocalProxy(_cv_app, "app", unbound_message=_no_app_msg)

也就是说 current_app 是一个代理,它会在每次属性访问时解引用当前应用上下文(由 app.app_context() 或请求生命周期压栈)中的 app 属性。这解释了为什么蓝图代码可以“延迟”绑定到具体实例:无论哪个应用上下文激活,current_app 都指向那个应用。这也正是工厂模式与上下文机制(参见 docs/appcontext.rst)配合的原因。

四、工厂与扩展:init_app 解耦模式

官方文档强烈建议:扩展对象在创建时不要绑定到应用。以 Flask-SQLAlchemy 为例,不应该在工厂内这样写:

def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_pyfile(config_filename)

    db = SQLAlchemy(app)   # 不推荐:绑定发生在导入时

而应该在 model.py(或等价模块)的顶层创建扩展实例,在工厂函数中再初始化:

# model.py
db = SQLAlchemy()

# application.py
def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_pyfile(config_filename)

    from yourapplication.model import db
    db.init_app(app)

这种设计下,扩展对象上不再保存任何应用相关的状态,因此一个扩展对象可以被多个应用复用

docs/extensiondev.rst 对这一模式做了更完整的说明,要点包括:

  • 所有扩展需要一个入口把扩展应用到应用,最常见的模式是带 init_app 方法的类,__init__ 可接收可选的 app 参数:
class HelloExtension:
    def __init__(self, app=None):
        if app is not None:
            self.init_app(app)

    def init_app(self, app):
        app.before_request(...)
  • 不要在扩展上保存 app(不要写 self.app = app)。扩展只有在 init_app 期间才应直接访问应用,其余时候应使用 current_app。这样做同时带来了三个好处:支持应用工厂模式、避免在其他模块导入扩展实例时的循环导入问题、便于用不同配置做测试;
  • 如果需要把引用存到应用上,可以使用 Flask.extensions 字典,注意这是一个共享命名空间,扩展名要取唯一名字(通常用扩展名去掉 "flask" 前缀)。

五、运行工厂应用:flask 命令的发现与传参机制

文档指出,运行工厂应用可以直接使用 flask 命令:

$ flask --app hello run

Flask 会自动检测模块 hello 中名为 create_appmake_app 的工厂函数。还可以直接给工厂传参:

$ flask --app 'hello:create_app(local_auth=True)' run

执行后 hello 中的 create_app 会收到关键字参数 local_auth=True。更完整的 --app 取值规则见 docs/cli.rst。下面结合 src/flask/cli.py 源码说明这些行为的实际实现。

5.1 应用发现顺序:find_best_app

--app 只给出模块名、没有冒号指定对象名时,走的是 find_best_appsrc/flask/cli.py),其查找顺序为:

  1. 依次检查模块属性 appapplication 是否为 Flask 实例;
  2. 否则收集模块中所有 Flask 实例:恰好一个则返回;多于一个则报错并要求用 module:name 显式指定;
  3. 仍找不到时,依次尝试 create_appmake_app 两个函数名,确认是函数后先尝试不带参数调用,若返回 Flask 实例则使用。

这里有一个容易被忽略的细节:如果工厂函数有必填参数,app_factory() 会抛 TypeError。源码用 _called_with_wrong_argssrc/flask/cli.py)遍历异常 traceback,判断这个 TypeError 究竟是“调用时参数不匹配”还是“工厂体内部代码自身抛出”,若是前者就给出明确的指引:

Detected factory 'create_app' in module '...', but could not call it without arguments.
Use '...:create_app(args)' to specify arguments.

这与测试用例 tests/test_cli.py 中的覆盖一致:测试了无参工厂、关键字参数工厂、make_app 命名,以及工厂内主动 raise TypeError 的边界情况(此时不应被误判为参数问题而重新抛出原异常)。

5.2 传参解析:字符串如何变成函数调用

flask --app 'hello:create_app(local_auth=True)' 中的 hello:create_app(local_auth=True) 部分由 find_app_by_stringsrc/flask/cli.py)处理,其机制值得注意:

  • ast.parse(app_name.strip(), mode="eval") 把参数串解析成 Python 表达式,从而区分“属性名”(ast.Name)和“函数调用”(ast.Call)两种形式;
  • 若是函数调用,只接受简单函数名(expr.func 必须是 ast.Name),并用 ast.literal_eval 解析位置参数与关键字参数——这意味着参数只能是 Python 字面量(字符串必须带引号,如 'hello:create_app("dev")'),不能是任意表达式,从机制上杜绝了任意代码执行;
  • 最后 getattr(module, name) 取出对象,是函数就 attr(*args, **kwargs) 调用;结果若不是 Flask 实例则抛出 NoAppException

仓库自带的 tests/test_apps/cliapp/factory.py 正是针对该机制的测试样本:

def create_app():
    return Flask("app")

def create_app2(foo, bar):
    return Flask("_".join(["app2", foo, bar]))

tests/test_cli.py 用参数化用例验证了各种书写形式:cliapp.factory:create_app()cliapp.factory:create_app2("foo", "bar")、带尾随逗号或多余空格的调用都能正确解析,而 create_app2("foo")(参数不足)与 create_app((括号不闭合)会按预期报错。

5.3 加载入口与回退策略

命令侧的加载统一收敛在 ScriptInfo.load_appsrc/flask/cli.py):

  • 若通过 --app 指定了路径,按冒号拆分为模块名与对象名(注意正则 r":(?![\\/])" 排除了 Windows 盘符冒号),先经 prepare_import 处理文件路径(.py 后缀剥离、__init__.py 归并到包、把所在目录插入 sys.path[0]),再交给 locate_app
  • 若未指定 --app,依次尝试导入 wsgi.pyapp.py,并容忍“找不到”;都失败才抛出提示使用 --app 选项或 FLASK_APP 环境变量的错误;
  • 加载成功后 load_app 会缓存实例(self._loaded_app),同一次命令执行中多次调用返回同一对象;若 set_debug_flag 为真,还会通过描述符把 --debug/FLASK_DEBUG 的结果写回应用的 debug 属性。

因此生产部署时也可以直接用“模块:工厂调用串”的形式交给 WSGI 服务器,例如 docs/deploying/gunicorn.rst 中给出:

# equivalent to 'from hello import create_app; create_app()'
$ gunicorn -w 4 'hello:create_app()'

六、工厂改进方向:flaskr 的真实落地

文档在结尾列出了三个“straightforward to implement”的改进方向,仓库中的教程项目 flaskr 完整实现了第一个方向,可作为生产级参考。

改进 1:允许传入配置值,避免为单元测试在文件系统上创建配置文件。

examples/tutorial/flaskr/init.py 的工厂函数接收 test_config 参数:

def create_app(test_config=None):
    """Create and configure an instance of the Flask application."""
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_mapping(
        # a default secret that should be overridden by instance config
        SECRET_KEY="dev",
        # store the database in the instance folder
        DATABASE=os.path.join(app.instance_path, "flaskr.sqlite"),
    )

    if test_config is None:
        # load the instance config, if it exists, when not testing
        app.config.from_pyfile("config.py", silent=True)
    else:
        # load the test config if passed in
        app.config.update(test_config)

    # ensure the instance folder exists
    os.makedirs(app.instance_path, exist_ok=True)

    @app.route("/hello")
    def hello():
        return "Hello, World!"

    # register the database commands
    from . import db
    db.init_app(app)

    # apply the blueprints to the app
    from . import auth
    from . import blog
    app.register_blueprint(auth.bp)
    app.register_blueprint(blog.bp)

    # make url_for('index') == url_for('blog.index')
    app.add_url_rule("/", endpoint="index")

    return app

这段代码体现了文档前述全部要点:先用 from_mapping 写默认值,非测试路径用 from_pyfile(..., silent=True) 叠加实例配置,测试路径则 config.update(test_config)db.init_app(app) 延后到工厂内调用;蓝图在函数体内局部导入,避免模块级循环导入。

配套的 pytest fixture(examples/tutorial/tests/conftest.py)展示了“每个测试一个隔离实例”的完整用法:

@pytest.fixture
def app():
    """Create and configure a new app instance for each test."""
    # create a temporary file to isolate the database for each test
    db_fd, db_path = tempfile.mkstemp()
    # create the app with common test config
    app = create_app({"TESTING": True, "DATABASE": db_path})

    # create the database and load test data
    with app.app_context():
        init_db()
        get_db().executescript(_data_sql)

    yield app

    # close and remove the temporary database
    os.close(db_fd)
    os.unlink(db_path)

每个测试函数拿到的是独立配置、独立临时数据库的 app 实例,测试结束后删除临时文件——这正是工厂模式“测试”动机的直接收益。

改进 2:为蓝图提供应用初始化钩子。 在应用搭建时调用蓝图的一个函数,作为修改应用属性的集中位置(例如挂载 before_request/teardown_appcontext 处理器)。flaskr 的 blog.py/auth.py 蓝图中即按此模式定义了 init_app 风格的回调(参见 examples/tutorial/flaskr/blog.py)。

改进 3:在创建应用时按需加入 WSGI 中间件。 工厂函数持有 app 的完整控制权,是包裹中间件的天然位置,例如:

def create_app(config_filename):
    app = Flask(__name__)
    # ... 常规搭建 ...
    app.wsgi_app = MyMiddleware(app.wsgi_app)
    return app

由于工厂在部署时才执行(flask rungunicorn 'hello:create_app()' 等路径都会真正调用它),中间件不会在导入阶段产生副作用。

七、小结

应用工厂模式可以归纳为四条可验证的实践规则:

  1. 应用创建移入 create_app/make_app 函数,返回配置完整的 Flask 实例;
  2. 蓝图与扩展在模块顶层只“声明”,通过 current_app(底层是 ContextVar + LocalProxy,见 src/flask/globals.py)在运行时绑定具体实例;
  3. 扩展实例不绑定应用、不在自身保存 app,统一走 init_app(app),应用侧状态存于 Flask.extensions
  4. flask --app 'module:create_app(args)' 的传参经由 ast 解析与 literal_eval 完成,只接受字面量参数(src/flask/cli.py),工厂无参时才能被自动发现。

掌握这套模式后,读者可以在自己的 Flask 项目中直接复用本文的工厂骨架,并用 tests/test_cli.pyexamples/tutorial/tests 中的用例作为行为验证的参照。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384