
Flask 设计决策深度解析显式应用对象、路由系统、上下文局部变量与 async 支持背后的权衡【免费下载链接】flaskThe Python micro framework for building web applications.项目地址: https://gitcode.com/gh_mirrors/fl/flask导读本文基于 Flask 官方文档 docs/design.rst系统拆解这个 microframework 的核心设计决策为什么必须显式创建Flask应用对象、为什么路由交给 Werkzeug 并按复杂度自动排序、为什么坚定地只绑定一种模板引擎Jinja、micro 一词的真实含义、上下文局部变量Context Locals如何化解循环导入以及 Flask 如何在保持 WSGI 兼容的前提下支持async。阅读完本文你将理解 Flask 每个看似随意的取舍背后的工程理由并掌握current_app、g、应用工厂等机制的正确使用方式。显式应用对象为什么app Flask(__name__)必不可少任何基于 WSGI 的 Python Web 应用都必须有一个实现应用逻辑的中央可调用对象。在 Flask 中这个对象就是flask.Flask类的实例核心实现见 src/flask/app.py。每个应用都必须自己创建这个实例并把模块名作为第一个参数传给它from flask import Flask app Flask(__name__) app.route(/) def index(): return Hello World!假如 Flask 像某些框架那样隐式地管理应用对象代码大概会变成这样from hypothetical_flask import route route(/) def index(): return Hello World!隐式方案虽然看起来更简洁但会带来三个致命问题这也是 Flask 坚持显式实例化的原因。原因一隐式对象意味着同一时刻只能有一个应用隐式应用对象要求同一时刻只能存在一个实例。虽然可以通过维护一个应用栈来伪造多应用但这样会引发一系列复杂问题。而真实场景中同时需要多个应用实例的情况非常常见最典型的例子就是单元测试测试某个特定行为时可以创建一个最小化的应用当这个应用对象被删除时它分配的所有资源都会被释放。显式对象让创建—使用—销毁的整个生命周期都掌握在开发者手中。原因二包名import_name是资源定位的地基每次创建 Flask 实例时传入的__name__绝非可有可无。Flask 依赖这个名字来相对你的模块正确加载资源——例如模板和静态文件。借助 Python 强大的反射能力Flask 能通过包名定位包路径进而确定templates/、static/目录的位置相关方法为Flask.open_resource()见 src/flask/app.pywith app.open_resource(schema.sql) as f: conn.executescript(f.read())open_resource内部以root_path为基准拼接路径os.path.join(self.root_path, resource)并限制只能以r、rt、rb三种只读模式打开文件。如果不用包名而依赖当前工作目录来定位资源会非常不可靠当前工作目录是进程级的如果同一个进程中运行多个应用这在某些 Web 服务器中会不知不觉地发生路径就会错乱更糟的是很多 Web 服务器并不会把工作目录设置为应用目录而是设置为文档根目录document root这通常与应用目录不是同一个文件夹。因此import_name的正确取值至关重要。源码 src/flask/sansio/app.py 的文档对此有专门说明单模块应用__name__永远是正确值包结构应用建议硬编码包名例如app Flask(yourapplication)或app Flask(__name__.split(.)[0])。为什么因为某些扩展如 Flask-SQLAlchemy在调试模式下会根据应用的导入名去定位触发 SQL 查询的代码导入名设置不当会丢失这部分调试信息例如只能追踪到yourapplication.app而漏掉yourapplication.views.frontend。此外root_path、static_folder、template_folder、instance_path等构造参数见 src/flask/sansio/app.py 的__init__签名都依赖这一根路径。若import_name为__main__App.name属性还会从运行文件猜出显示名见 src/flask/sansio/app.py。原因三显式优于隐式这个应用对象就是你的 WSGI 应用本身你不需要记住任何额外的魔法。想套一层 WSGI 中间件直接包裹即可from werkzeug.middleware.proxy_fix import ProxyFix app.wsgi_app ProxyFix(app.wsgi_app)更好的做法是通过Flask.wsgi_app属性见 src/flask/app.py进行包裹这样你不会丢失对原始应用对象的引用——wsgi_app是真正面向 WSGI 服务器暴露的调用入口而app本身仍可用于开发、测试与配置。由显式对象衍生出的应用工厂模式显式实例化还带来了一个重要的工程红利可以用工厂函数来创建应用这对单元测试和类似场景极其有用详见 docs/patterns/appfactories.rst。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工厂模式带来两大收益测试可以用不同配置创建多个应用实例逐一验证各种场景多实例可以在同一个进程中运行同一应用的多个实例。在工厂模式下蓝图中不能在使用时直接引用应用对象但可以在请求中通过current_app访问见 docs/patterns/appfactories.rstfrom 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])配合flask命令运行时Flask 会自动发现名为create_app或make_app的工厂$ flask --app hello run $ flask --app hello:create_app(local_authTrue) run第二种写法会把关键字参数local_authTrue传给工厂。路由系统按复杂度自动排序 URL 唯一性保证Flask 的路由并非自己实现而是直接使用Werkzeug 路由系统。Werkzeug 路由有一个核心设计目标自动按复杂度对规则排序。这意味着你可以以任意顺序声明路由匹配结果依然正确app.route(/user/username) def show_user(username): ... app.route(/user/me) def show_me(): ...即使show_me声明在后访问/user/me也会命中更具体的规则而不是被username通配规则抢走。这种能力是装饰器式路由的硬性要求——当应用被拆分成多个模块时装饰器的执行顺序是不可控的只有按复杂度排序的匹配器才能保证结果稳定。Werkzeug 路由的另一个设计决策是尽力保证 URL 的唯一性。当一条路由存在歧义时Werkzeug 会自动重定向到规范 URL消除重复内容的隐患。这与路由相关的类型定义url_rule_class Rule、url_map_class Map以及add_url_rule方法都能在 src/flask/sansio/app.py 与 src/flask/sansio/app.py 中找到。add_url_rule也是app.route装饰器的底层实现且受_check_setup_finished保护应用处理过第一个请求之后任何迟到的路由注册都会抛出AssertionError见 src/flask/sansio/app.py。单一模板引擎为什么 Flask 只绑定 JinjaFlask 坚定地选择一种模板引擎Jinja。为什么不提供可插拔的模板引擎接口核心原因在于模板引擎之间差异巨大抽象层会抹平它们的独特能力。文档 docs/design.rst 中给出了生动的对比Jinja拥有庞大的过滤器系统、独特的模板继承方式、可在模板和 Python 代码中复用的宏macros、迭代式模板渲染、可配置的语法等Genshi基于 XML 流求值模板继承依赖 XPath 的可用性Mako把模板当作类似 Python 模块的东西来处理。表面上看所有引擎都用一组变量求值模板并返回字符串但相似之处到此为止。一个不牺牲各引擎独特能力的模板抽象层本身就是一门科学对 microframework 来说体量过大。更关键的是总是配置好 Jinja为扩展生态带来了确定性扩展可以放心地依赖 Jinja 的存在。你当然可以自由使用自己的模板语言但一个扩展仍可以安全地依赖 Jinja 本身。从源码看Flask 的 Jinja 集成远不止渲染它使用 Jinja 强大的自动转义autoescaping并提供从模板访问宏的能力。Flask.create_jinja_environment()见 src/flask/app.py会基于jinja_options构建环境若未显式指定autoescape则使用select_jinja_autoescape按模板扩展名自动选择转义策略auto_reload也会依据TEMPLATES_AUTO_RELOAD配置生效。模板环境的默认配置与jinja_options字典定义在 src/flask/sansio/app.py。Micro 的真实含义核心简单但可扩展Micro 绝不意味着你的整个 Web 应用必须塞进一个 Python 文件尽管它完全可以也不意味着 Flask 功能匮乏。micro 指的是Flask 致力于让核心保持简单但可扩展。Flask 不会替你做出太多决定——比如该用哪个数据库。它替你做出的决定比如用哪个模板引擎也易于更换。除此之外的一切都由你自己掌控让 Flask 成为你需要的一切而不是你不需要的任何东西。默认情况下Flask 不包含数据库抽象层表单校验其他已有成熟库可以承担的职责。取而代之的是扩展机制扩展能为应用添加这些功能使用起来就像功能本来就长在 Flask 里一样。大量扩展覆盖了数据库集成、表单校验、文件上传、各种开放认证技术等场景。Flask 虽然 micro但它已经为多种生产需求做好了准备。依赖 Werkzeug 和 Jinja 是否自相矛盾有人质疑既然叫 microframework为什么还要依赖两个库Werkzeug 和 Jinja文档给出的答案是为什么不呢。在 Ruby 生态中有一个与 WSGI 极为相似的协议叫 Rack但几乎没有人直接与 Rack 打交道而是使用同名库。这个 Rack 库在 Python 中有两个对应物WebOb前身是 Paste和Werkzeug。两者同步发展目标一致为其他应用提供良好的 WSGI 实现。Flask 正是站在 Werkzeug 的肩膀上正确地接入 WSGI这本身有时是件复杂的事。得益于 Python 包基础设施的发展带依赖的库不再是问题几乎没有理由反对库依赖其他库。上下文局部变量化解循环导入与全局数据传递两大难题Flask 使用特殊的上下文局部变量Context Locals和代理Proxies让任何在请求、CLI 命令等活动中运行的代码都能访问到当前的 app 和 request 数据。上下文局部变量与处理该活动的 worker 绑定——可以是线程、进程、协程或 greenlet。相关实现集中在 src/flask/globals.pyfrom contextvars import ContextVar from werkzeug.local import LocalProxy _cv_app: ContextVar[AppContext] ContextVar(flask.app_ctx) current_app: FlaskProxy LocalProxy(_cv_app, app, unbound_message_no_app_msg) g: _AppCtxGlobalsProxy LocalProxy(_cv_app, g, unbound_message_no_app_msg) request: RequestProxy LocalProxy(_cv_app, request, unbound_message_no_req_msg) session: SessionMixinProxy LocalProxy(_cv_app, session, unbound_message_no_req_msg)注意底层使用的是 Python 标准库的ContextVar上下文变量这意味着在异步代码中每个协程也能持有自己的上下文这正是worker 隔离的现代实现。这些代理对象在 src/flask/init.py 中被导出为公开 APIcurrent_app、request、session、g。解决的两个开发难题难题一循环导入circular imports:data:current_app让你无需直接导入应用对象即可访问它。在应用工厂模式下蓝图模块在被导入时应用对象可能尚未创建更无法直接import app那会造成循环导入。通过current_app可以绕开这一困境。难题二到处传递全局数据:data:request、:data:session和:data:g可以随时导入以访问当前请求的数据而不必把request、db等对象作为参数穿过项目中的每一个函数。g对象尤其适合存放请求期间共享的数据如惰性加载的数据库连接它的类可以通过app_ctx_globals_class定制见 src/flask/sansio/app.py。正确的使用前提这些代理对象只有在对应的上下文中才能工作。如果在应用上下文之外访问current_app或g会抛出带有明确提示的RuntimeErrorWorking outside of application context.并建议使用with app.app_context():显式进入上下文提示文本定义于 src/flask/globals.py。同理在请求上下文之外访问request会得到Working outside of request context.的报错src/flask/globals.py。仓库中的测试 tests/test_appctx.py 与 tests/test_reqctx.py 对这两类行为有完整的用例覆盖。Async/await 与 ASGI 支持兼容优先线程兜底Flask 支持async协程视图函数但其策略与原生 ASGI 框架截然不同Flask 通过在独立线程上执行协程来支持 async而不是像 async-first 框架那样在主线程上运行事件循环。这一取舍的必要性在于向后兼容Python 引入async之前编写的扩展和代码必须继续无缝工作。代价是明显的由于线程开销与 ASGI 框架相比存在性能成本。源码层面的证据在 src/flask/app.pydef ensure_sync(self, func): Ensure that the function is synchronous for WSGI workers. Plain def functions are returned as-is. async def functions are wrapped to run and wait for the response. if iscoroutinefunction(func): return self.async_to_sync(func) return func def async_to_sync(self, func): try: from asgiref.sync import async_to_sync as asgiref_async_to_sync except ImportError: raise RuntimeError( Install Flask with the async extra in order to use async views. ) from None return asgiref_async_to_sync(func)要点解读ensure_sync对普通def函数原样返回对async def函数包装为可同步调用包装依赖asgiref的async_to_sync若未安装会提示Install Flask with the async extra这两个方法都可以在子类中被覆写从而自定义异步视图的运行方式相关测试见 tests/test_async.py。关于 ASGI 的长期展望文档态度务实由于 Flask 代码与 WSGI 绑定很深Flask类能否同时支持 ASGI 和 WSGI 尚不明朗Werkzeug 正在开展与 ASGI 协作的工作未来或许能让 Flask 受益。更深入的讨论可参阅 docs/async-await.rst。Flask 是什么Flask 不是什么这一节的边界定义极其清晰Flask 永远不会有数据库层表单库任何类似方向的强制功能。Flask 本身只做三件事桥接 Werkzeug实现一个规范的 WSGI 应用桥接 Jinja处理模板渲染绑定少量标准库包如logging参见 src/flask/logging.py。其余一切交给扩展。为什么这么克制因为不同的人有不同的偏好和需求如果把这些强制塞进核心Flask 就无法满足所有人。事实是大多数 Web 应用多少都需要模板引擎但并非每个应用都需要 SQL 数据库。所以模板引擎进了核心数据库则留给选择。边界带来的自由随着你的代码库增长你可以自由地为项目做出合适的设计决策在 SQLAlchemy 或其他数据库工具中实现高级模式按需引入非关系型数据持久化直接使用为 WSGI 构建的、与框架无关的工具。Flask 的理念是为所有应用打好一个良好的地基。其余一切由你或扩展来完成。这在 examples/tutorial/flaskr 的示例应用中得到印证——db.pySQLite 连接与auth.py、blog.py视图与蓝图以模块化的方式组合在create_app工厂中而不是由框架强加任何数据层约束。结语一份关于克制的设计哲学回顾 docs/design.rst 的全部设计决策可以提炼出一条贯穿始终的主线Flask 把该你决定的事留给你把决定后要承担的事降到最低。显式应用对象换来的是可测试性与可扩展性把路由交给 Werkzeug 换来的是声明顺序无关的确定性绑定一种模板引擎换来的是扩展生态的确定性上下文局部变量化解的是工程组织难题而 async 支持以线程为代价换来了生态的完整兼容。理解这些决策背后的权衡你就能在使用 Flask 时做出更符合其设计意图的选择也能在遇到为什么不这样实现的疑问时给出有理有据的回答。【免费下载链接】flaskThe Python micro framework for building web applications.项目地址: https://gitcode.com/gh_mirrors/fl/flask创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考