经常有初级Flask开发者跑来问我这样一个问题:"为什么别人写的Flask项目里有个create_app函数,而我学的教程里都是直接app = Flask(__name__)?这不折腾吗?"
这个问题问得特别好,因为直接整app = Flask(__name__)确实是很多人的第一个Flask程序——包括我的第一个。但为什么那么多真实项目、开源框架、教程进阶篇都要绕一圈搞一个"应用工厂"?等你接手一个稍微像样点的项目,或者在部署、测试环节里吃过几次亏,你就明白了。这篇就专门把这两种模式掰开揉碎讲清楚,顺便附上我踩坑多年的实战经验。
1. 先搞懂:两种模式分别是什么
1.1 全局模式:最直觉的写法
所谓全局模式,就是在模块顶层直接创建Flask实例:
from flask import Flask app = Flask(__name__) @app.route("/") def index(): return "Hello, World!" if __name__ == "__main__": app.run()这段代码你大概率见过,Flask官方文档第一页就是这个样子。它的核心特征只有一个:app实例在模块导入的那一刻就被创建,并且整个进程内只有一个app对象。
因为是模块级变量,所以你在任何地方都能通过from xxx import app把它引进来,然后注册路由、注册蓝图、挂载扩展,全都在这个全局对象上操作。对刚开始学Flask的人来说,这几乎是最符合直觉的写法:我创建了一个应用,然后往这个应用上加东西,跑起来完事。想加个页面就加个装饰器,想改端口就改一个参数,心智负担极低。
1.2 工厂模式:绕了一圈的"函数化"
工厂模式则完全不同,它把"创建应用"这个动作封装成一个函数:
from flask import Flask def create_app(): app = Flask(__name__) app.config.from_object("config.DevelopmentConfig") # 注册扩展 from app.extensions import db db.init_app(app) # 注册蓝图 from app.main import bp_main app.register_blueprint(bp_main) return app然后这样启动:
flask --app 'factory:create_app' run或者用flask --app factory run,只要Flask能找到一个create_app函数就行。
可以看到,工厂模式的核心Call点:app实例不再是"在导入时立刻存在",而是"在调用create_app()之后才存在"。你想要一个app,就得调用一次函数;你想再要一个app,就再调用一次函数。这就好比从"全局只有一个出口的游戏地图"变成了"一个每次都能按你的参数重新生成的游戏地图生成器"。
2. 为什么说全局模式写着爽、扩展难
2.1 初始化顺序:Flask扩展的坑从这里开始
初学阶段你可能没注意过,但Flask扩展的初始化有两种方式。以flask_sqlalchemy为例:
db = SQLAlchemy(app) # 方式一:直接在创建时传入app db.init_app(app) # 方式二:先创建扩展对象,后续再绑定app方式一是"侵入式初始化",扩展一创建就和app绑死了。这在全局模式里看着没问题,但一旦你需要动态配置数据库地址、测试库地址,你就麻烦了——因为db在导入时就被绑定,而app.config很可能还没来得及加载完整的配置。
方式二是"延迟绑定",也是工厂模式常用的做法:先声明一个没有app的扩展对象,等create_app()内部拿到完整配置后,再执行init_app(app)完成绑定。
生活化类比:全局模式像是在你还没确定去哪吃饭的时候,服务员已经把你按在了一张固定桌位上;工厂模式是你进门之后告诉服务员你的需求,她再带你去合适的桌位。前者方便,但遇到特殊情况(比如这个餐厅有三层、每层氛围不一样)就僵住了。
2.2 循环导入:几乎所有初级开发者都会撞上的墙
全局模式里最常见的崩溃场景:
# app.py(主文件) from flask import Flask app = Flask(__name__) from views import index# views.py from app import app @app.route("/") def index(): return "Hello"初看好像没问题:app.py先创建app,再从views.py导入路由。但实际执行顺序可能是反的——如果views.py先被某个模块通过其他路径导入,它就会尝试从app.py导入app,而此时app.py还没执行到app = Flask(__name__)那行,于是直接报ImportError: cannot import name 'app' from 'app'。
真实的项目里模块一多,导入顺序就变得极难控制和预测。蓝图虽然能缓解,但如果蓝图的模块本身又依赖app实例里的东西,循环导入照样阴魂不散。
工厂模式之所以能根治这个问题,是因为app的创建发生在函数内部,而且创建时机完全由你控制。蓝图的导入也在函数内部完成,导入的是"创建蓝图的模块"而不是"app本身",依赖方向被彻底理顺了。
2.3 多环境配置:全局模式没法优雅换挡
开发、测试、生产,一个稍微正式点的项目这仨环境几乎跑不掉。全局模式下的典型操作是:
import os app = Flask(__name__) app.config.from_object(f"config.{os.environ.get('FLASK_CONFIG', 'default').capitalize()}")这个写法不算错,但它有个隐患:配置是在模块导入时执行的。测试框架导入你的app模块时,环境变量可能还没准备好;管理器执行的时机也可能和预想不一致。而且一旦app被创建了,配置就已经固定,你再想按测试需求动态注入一个内存数据库URL,就得费一番功夫。
工厂模式天然支持配置参数:
def create_app(config_name=None): if config_name is None: config_name = os.environ.get("FLASK_CONFIG", "default") app = Flask(__name__) app.config.from_object(config[config_name]) return app测试时我可以直接create_app("testing");生产部署时create_app("production")。不需要任何环境变量技巧,一个参数搞定。
3. 应用工厂模式核心原理与正确姿势
3.1 create_app 到底做了什么
一个标准的create_app()内部通常包含四件事:
- 创建app实例:
app = Flask(__name__),这个__name__不是乱填的,它决定了Flask去哪里找静态文件、模板文件夹,最好传入应用包的名字。 - 加载配置:从对象、文件或环境变量引入配置。
- 初始化扩展:对支持延迟绑定的扩展调用
init_app(app)。 - 注册蓝图和钩子:把路由、错误处理器、上下文处理器全挂上。
我习惯的写法是这样:
# app/__init__.py from flask import Flask from config import config_map def create_app(config_name="default"): app = Flask(__name__) app.config.from_object(config_map[config_name]) config_map[config_name].init_app(app) # 可选的配置初始化钩子 # 初始化扩展 from .extensions import db, migrate, login_manager db.init_app(app) migrate.init_app(app, db) login_manager.init_app(app) # 注册蓝图 from .main import main_bp from .api import api_bp app.register_blueprint(main_bp) app.register_blueprint(api_bp, url_prefix="/api") # 注册错误处理器和请求钩子 register_error_handlers(app) return app3.2 扩展注册的两种时机,别再混淆了
我再强调一遍:扩展是否在导入时绑定app,决定了你的架构能走多远。下面这张表应该贴在你屏幕上:
| 扩展类型 | 全局模式下的做法 | 工厂模式下的做法 | 适合场景 |
|---|---|---|---|
| 侵入式(创建时绑定) | db = SQLAlchemy(app) | 不推荐,因为app还不存在 | 单文件工具、超小型Demo |
| 延迟绑定(init_app) | db = SQLAlchemy()然后db.init_app(app) | db = SQLAlchemy()在模块层,db.init_app(app)在工厂内 | 几乎所有正式项目 |
常见的支持延迟绑定的扩展有:flask_sqlalchemy、flask_migrate、flask_login、flask_bootstrap等。如果你用的第三方扩展不支持init_app,要么换扩展,要么就得在工厂函数里特殊处理。
3.3 配置加载的正确姿势
配置建议用一个字典来组织,这样工厂函数传参时特别方便:
# config.py class BaseConfig: SECRET_KEY = "dev-key-change-me" SQLALCHEMY_TRACK_MODIFICATIONS = False class DevelopmentConfig(BaseConfig): DEBUG = True SQLALCHEMY_DATABASE_URI = "sqlite:///dev.db" class TestingConfig(BaseConfig): TESTING = True SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" class ProductionConfig(BaseConfig): DEBUG = False SQLALCHEMY_DATABASE_URI = "postgresql://..." config_map = { "development": DevelopmentConfig, "testing": TestingConfig, "production": ProductionConfig, "default": DevelopmentConfig, }注意一个细节:我用的是类,不是散落的常量。类的好处是能继承,BaseConfig里放公共配置,子类只覆写差异项。这样新增环境只需要新增一个类,不需要改动工厂函数。
然后app.config.from_object(config_map[config_name])就完成了加载。如果你的配置里还有敏感变量(数据库密码、API密钥),建议用from_envvar或者在类里用os.environ.get()兜底,别把密钥硬编码进代码库。
4. 完整实战:从全局模式重构为工厂模式
4.1 目录结构调整对比
全局模式的典型结构:
project/ ├── app.py # 所有东西全在这一个文件 ├── config.py └── requirements.txt工厂模式的结构会更分层:
project/ ├── config.py ├── run.py # 入口文件(也可以省略,直接flask --app) ├── requirements.txt └── app/ ├── __init__.py # create_app()在这里 ├── extensions.py # db、migrate等扩展对象在这里声明 ├── models/ # 数据库模型 ├── main/ # 主业务蓝图 │ ├── __init__.py │ ├── views.py │ └── forms.py └── templates/你会发现,工厂模式下app包变成了一个"应用容器",业务模块各自独立成包,每个包内再处理自己的蓝图。这对多人协作特别友好:A同学写用户模块,B同学写订单模块,两人互不干扰,最后在create_app()里统一注册。
4.2 关键代码迁移对比
先看全局模式里最常见的耦合写法:
# 全局模式:app.py from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.config.from_object("config") db = SQLAlchemy(app) from models import User @app.route("/") def index(): return User.query.all()重构后:
# app/extensions.py:扩展对象统一在这里声明,不绑定app from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager db = SQLAlchemy() migrate = Migrate() login_manager = LoginManager()# app/models.py from .extensions import db class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True)# app/main/views.py from . import main_bp from ..extensions import db from ..models import User @main_bp.route("/") def index(): users = User.query.all() return f"Total: {len(users)}"# app/__init__.py from flask import Flask from config import config_map def create_app(config_name="default"): app = Flask(__name__) app.config.from_object(config_map[config_name]) from .extensions import db, migrate, login_manager db.init_app(app) migrate.init_app(app, db) login_manager.init_app(app) from .main import main_bp app.register_blueprint(main_bp) return app这个迁移最核心的差异就是:原来分散在app.py里的"导入即执行"的逻辑,全部收敛到了create_app()函数内部,而且扩展对象和业务代码不再互相耦合。
4.3 入口文件的写法差异
全局模式下你可能习惯了:
if __name__ == "__main__": app.run(debug=True, port=5000)工厂模式下这个入口基本可以删除,直接用Flask CLI:
# 开发环境 export FLASK_CONFIG=development flask --app 'app:create_app' run --debug # 测试环境 flask --app 'app:create_app("testing")' run如果你还是想要一个可执行的run.py,只需要两行:
from app import create_app app = create_app()记得千万别写成:
if __name__ == "__main__": app.run()这种做法在工厂模式下会掩盖flask run的很多行为差异,建议直接交给CLI管理。
5. 初级开发者最容易踩的坑
5.1 循环导入陷阱:我看过不下十个人栽在这
你可能会觉得,工厂模式都用了,循环导入应该不存在了吧?不好意思,还是会有。最经典的一个:
# app/__init__.py def create_app(): from .extensions import db from .models import User # 如果在注册蓝图之前导入模型并初始化db db.init_app(app)问题出在哪?如果models.py里用了db.Column,这个db是从.extensions导入的没问题;但如果models.py里还有一行from app import create_app或者from app import db,一旦触发导入,又会回到顶层app/__init__.py,而此时__init__.py还没执行完,create_app的定义可能还没完成或者函数依赖的模块还在半初始化状态,直接炸。
我的经验法则是:工厂函数只负责挂载,绝不负责业务导入。模型、视图、表单,都应该在蓝图模块内部按需导入,而且永远不要从app包这个顶层模块去导入业务组件。
常见的引荐做法是:需要用到db的地方,一律from .extensions import db;需要用到模型的地方,一律from ..models import User;需要用到当前app上下文的地方,用current_app而不是from app import app。
5.2 扩展只声明未初始化
是我见过最多的报错:
RuntimeError: The current Flask app is not registered with this 'SQLAlchemy' instance. Did you forget to call 'init_app'?原因特别简单:扩展在不同模块里分别被创建了两次,或者你在工厂里忘了db.init_app(app)。我建议所有扩展对象在extensions.py里面统一创建,然后在create_app()里逐个init_app。这样排查的时候只需要检查两个文件。
另外要注意:直接在extensions.py里定义了db,但是全局模式下又写了个db = SQLAlchemy(app),两个db就会互相干扰。迁移到工厂模式后,全局模式下遗留的db = SQLAlchemy(app)必须删掉,否则你大概率遇到"一个app关联了两个db对象"的问题。
5.3 测试时的隔离问题
工厂模式最大的红利就是测试环境干净。我平时写测试会配合pytest:
# tests/conftest.py import pytest from app import create_app from app.extensions import db @pytest.fixture def app(): app = create_app("testing") with app.app_context(): db.create_all() yield app @pytest.fixture def client(app): return app.test_client()每次测试都通过create_app("testing")创建一个全新的app实例,配上内存数据库,测试互不污染。这在全局模式下几乎做不到——如果你只有一个全局app,测试完脏数据清不掉,配置也改不了,写测试会写出一种"在流沙上盖房子"的感觉。
如果你还在用全局模式,又坚持要写测试,你的通常退路是手动改app.config再重新初始化db,整套流程不仅慢,还容易遗留状态。这也是为什么很多初级开发者写Flask项目时觉得测试"难写"——不是你能力不行,是架构没给你创造条件。
5.4 静态文件与模板路径的小陷阱
使用工厂模式后,Flask(__name__)里的__name__会变成app(包名)。如果你的模板文件夹放在app/templates/下面,Flask默认会去包目录/templates/找,这没问题。但如果你把模板放到了项目根目录的templates/,就会找不到。
解决方式有两种:一是调整目录,把模板放进app/templates/;二是在创建Flask实例时显式指定:
app = Flask(__name__, template_folder="../templates", static_folder="../static")我更推荐第一种。项目结构干净,路径心智负担小。第二种写法路径容易算错,尤其嵌套层级深了之后,两个点..就得变成三个点。
6. 最后再分享两个我实际用下来的小技巧
我在把项目从全局模式迁到工厂模式时踩过不少坑,走了几趟弯路之后总结出两个特别好用的小技巧,分享出来供参考。
第一个技巧:如果你不想一次大改,可以搞一个"兼容层"。在工厂模式改造的过渡期,你可以还在app/__init__.py末尾加上一句:
app = create_app()这样旧代码里那些from app import app的模块不用立刻改掉,也能跑起来。等迁移稳定后,再把这一行删掉,所有代码改为依赖current_app或者CLI入口。这个技巧对我个人来说特别好用,让我从全局模式平滑过渡到工厂模式,避免了一次大规模重构带来的混乱。
第二个技巧:蓝图的统一注册函数。当蓝图多了以后,create_app()会被注册代码塞得很胖。我建议把蓝图注册单独拆一个函数:
def register_blueprints(app): from .main import main_bp from .api import api_bp from .admin import admin_bp app.register_blueprint(main_bp) app.register_blueprint(api_bp, url_prefix="/api") app.register_blueprint(admin_bp, url_prefix="/admin")然后在create_app()里就一行register_blueprints(app)。同样的逻辑也可以用在错误处理器、请求钩子上。create_app()保持瘦,各模块保持独立,后期维护会轻松很多。
最后再说一句真心话:工厂模式不是Flask官方为了炫技搞出来的概念,它是被真实项目的复杂性逼出来的最佳实践。初级开发者越早理解这两种模式的差异,越能在项目从小变大的过程中少走弯路。如果你现在还在写全局模式,别急着推翻重写,先从最小的重构开始——把配置抽离、把扩展改成init_app、加一个create_app,一步步来,很快你就会发现那种"一切在掌控之中"的踏实感。