Flask 应用工厂模式详解:用 create_app() 构建可测试、可配置的 Flask 应用
Flask 应用本质上是一个 Flask 类的实例,配置、路由等一切都注册在这个实例上。本文基于官方教程的 Application Setup 章节(docs/tutorial/factory.rst),系统讲解“应用工厂(application factory)”模式:为什么不在模块顶层创建全局 Flask 实例、如何在 create_app() 中完成实例创建与配置加载、instance folder 机制如何隔离本地数据,以及如何用 flask --app flaskr run --debug 启动并验证应用。读完本文,你将掌握应用工厂的完整写法,并理解其中每个参数在 Flask 源码中的实际作用。
为什么需要应用工厂
最直接的 Flask 应用写法是在代码顶部直接创建一个全局 Flask 实例——"Hello, World!" 示例就是这么做的。这种方式简单、在小型项目中完全够用,但随着项目增长会暴露一些问题:
- 全局实例在导入时即被创建,配置无法在创建前注入,导致测试时难以隔离(每个测试都想用独立的临时数据库,但全局实例只能有一份配置);
- 同一个应用难以按环境(开发/测试/生产)切换不同配置;
- 多实例部署或扩展初始化时,全局对象容易产生隐式依赖。
Flask 官方的解决方案是:不在全局创建 Flask 实例,而是在一个函数内创建它。这个函数就是应用工厂(application factory),应用所需的全部配置、注册和初始化都在函数体内完成,最后返回 app。
创建 flaskr 包与应用工厂函数
创建 flaskr 目录并添加 __init__.py 文件。__init__.py 身兼两职:既容纳应用工厂函数,又告诉 Python 将 flaskr 目录当作一个包处理。
$ mkdir flaskr
# flaskr/__init__.py
import os
from flask import Flask
def create_app(test_config=None):
# create and configure the app
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY='dev',
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.from_mapping(test_config)
# ensure the instance folder exists
os.makedirs(app.instance_path, exist_ok=True)
# a simple page that says hello
@app.route('/hello')
def hello():
return 'Hello, World!'
return app
create_app 就是应用工厂函数。教程的后续章节会继续向它添加数据库命令和蓝图,但此时它已经完成了大量核心工作。下面逐步解析。
1. Flask(__name__, instance_relative_config=True):创建实例
__name__是当前 Python 模块的名字。Flask 需要知道应用位于何处以便推导若干路径(如root_path),__name__就是最方便的告知方式。在__init__.py中它即"flaskr"。instance_relative_config=True告诉应用:相对路径的配置文件以 instance folder(实例文件夹)为基准,而不是以应用根目录为基准。
源码印证:该参数在 sansio/app.py 中的定义为——“if set to True relative filenames for loading the config are assumed to be relative to the instance path instead of the application root.”;而 instance_path 参数默认值则是“the folder 'instance' next to the package or module”(包/模块旁边的 instance 文件夹)。
instance folder 位于 flaskr 包外部,专门存放不应提交到版本控制的本地数据,例如配置密钥和数据库文件。关于 instance folder 的完整说明见官方配置文档 docs/config.rst。
2. app.config.from_mapping():设置默认配置
from_mapping 写入应用将使用的默认配置项:
| 配置项 | 取值 | 说明 |
|---|---|---|
SECRET_KEY |
'dev' |
Flask 和扩展用它保证数据安全(如签名 cookie)。'dev' 只是开发期方便用的占位值,部署时必须用随机值覆盖 |
DATABASE |
os.path.join(app.instance_path, 'flaskr.sqlite') |
SQLite 数据库文件的存放路径,位于 app.instance_path(即 Flask 为实例文件夹计算出的路径)下 |
从源码看,from_mapping 等价于 update 但会忽略非大写键(见 config.py),所以上下文中的键都必须是全大写——这也是 Flask 配置的约定。
3. app.config.from_pyfile('config.py', silent=True):加载实例配置
如果 instance folder 中存在 config.py,它覆盖上面的默认值。典型用途是部署时设置真正的 SECRET_KEY。例如 instance folder 中的 config.py:
SECRET_KEY = "development-key" # 部署时替换为随机字符串
silent=True 的含义可从 config.py 的实现确认:from_pyfile 内部以 exec 执行配置模块后调用 from_object;当文件不存在(ENOENT/EISDIR/ENOTDIR)且 silent 为真时静默返回 False,因此“instance 配置不存在”不是错误,开发环境可以直接跑起来。
test_config 分支:也可以向工厂传入 test_config 字典,它会被 app.config.from_mapping(test_config)(仓库示例代码中写作 app.config.update(test_config),两者对 dict 等价)加载,替代实例配置。这是为了让教程后续编写的测试独立于你本地的开发配置。
4. os.makedirs(app.instance_path, exist_ok=True):确保实例文件夹存在
Flask 不会自动创建 instance folder,但项目稍后要把 SQLite 数据库文件写在那里,所以工厂函数显式创建它。exist_ok=True 保证文件夹已存在时不报错。
5. @app.route('/hello'):一个简单路由
创建一个 URL /hello 到视图函数的映射,返回字符串 'Hello, World!'。它让你在教程继续深入之前就能亲眼看到应用跑起来。
运行应用
在终端用 flask 命令运行应用——告诉 Flask 去哪里找到应用,并开启 debug 模式。注意:此时你应处于顶层项目目录(如 flask-tutorial)下,而不是 flaskr 包内部。
$ flask --app flaskr run --debug
Debug 模式会在页面抛出异常时展示交互式调试器,并在代码改动后自动重启服务器。你可以让它一直运行,跟随教程只需刷新浏览器页面即可。
正常输出类似:
* Serving Flask app "flaskr"
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: nnn-nnn-nnn
在浏览器访问 http://127.0.0.1:5000/hello,应看到 "Hello, World!" 消息。
端口被占用的处理:如果 5000 端口已被其他程序占用,服务器启动时会报 OSError: [Errno 98](Linux)或 OSError: [WinError 10013](Windows)。处理方法(改用 --port 指定其他端口)见 CLI 文档 docs/cli.rst。
用测试验证工厂行为
仓库中的 tutorial 示例项目(examples/tutorial)提供了可直接参考的完整实现与测试,印证了工厂模式“测试注入配置”的设计意图。
单元测试直接验证工厂的两种调用路径(test_factory.py):
def test_config():
"""Test create_app without passing test config."""
assert not create_app().testing
assert create_app({"TESTING": True}).testing
def test_hello(client):
response = client.get("/hello")
assert response.data == b"Hello, World!"
而 pytest 的 fixture 正是靠 test_config 参数,为每个测试构造一个使用临时数据库的独立应用(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)
这就是工厂模式的核心收益:create_app() 每次调用都返回全新实例,测试彼此隔离,且完全不受开发者 instance folder 中配置的影响。
完整示例中的后续扩展
教程示例最终演进的工厂函数(examples/tutorial/flaskr/init.py)在本文讲解的基础上又增加了三步,展示了工厂作为“集中注册点”的扩展方式:
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:
app.config.from_pyfile("config.py", silent=True)
else:
app.config.update(test_config)
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
注意两个值得学习的细节:db.init_app(app) 展示了扩展“两阶段初始化”(扩展模块不持有全局 app,而是由工厂把 app 传给它),以及 register_blueprint 集中注册蓝图。从源码结构看,这些调用全部发生在工厂函数内,意味着任何实例都获得了相同的注册结果,而配置可以按实例不同——这正是应用工厂模式相对全局实例的本质优势。
小结
- Flask 应用是
Flask实例,应用工厂函数create_app()是该实例的统一创建点:创建实例、设置默认配置、加载环境配置、确保 instance folder 存在、注册路由,最后返回app; instance_relative_config=True+ instance folder 把“不应入库的本地数据”(密钥、SQLite 数据库)隔离到包外,from_pyfile('config.py', silent=True)允许在部署时静默覆盖默认值;- 传入
test_config的分支让测试以独立配置构造应用,仓库中的 conftest.py 与 test_factory.py 是该机制的完整落地; - 运行入口为
flask --app flaskr run --debug,端口冲突时按 docs/cli.rst 的说明换端口即可。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00