flask之路【番外篇】flask源码分析
flask3.1.1源码分析系列:
1.对flask源码进行分析,然后做心得记录
2.也可以对drf(django)进行分析
具体分析内容:
作业1. drf源码分析系列
- 01 restful规范
- 02 从cbv到drf的视图 / 快速了解drf
- 03 视图
- 04 版本
- 05 认证
- 06 权限
- 07 节流
- 08 jwt
- 持续更新中...
2. flask源码分析系列
- 01 werkzurg 了解wsgi
- 02 快速使用
- 03 threading.local和高级
- 04 LocalStack和Local对象实现栈的管理
- 05 Flask源码之:配置加载
- 06 Flask源码之:路由加载
- ...
0.小知识点
-
def __init__(self, context_var: ContextVar[dict[str, t.Any]] | None = None) -> None: pass
这个函数定义的参数为什么可以这么表示?
__init__(self, context_var: ContextVar[dict[str, t.Any]] | None = None) -> None: __init__: 这是一个类的构造方法,在创建对象时自动调用。 self: 表示类实例本身,通过它可以在类的方法中访问属性和其他方法。 context_var: 参数名,用于接收传入的变量。 ContextVar[dict[str, t.Any]] | None: 类型注解,表示context_var可以是一个ContextVar类型的对象,其内部存储的是一个字典,字典的键是字符串,值可以是任意类型(由t.Any表示)。同时,| None表示context_var也可以是None。 t.Any 是 typing 模块中的一个类型,表示它可以是任何类型。 ContextVar 是 contextvars 模块中的一个类,用于在异步程序中管理上下文变量。 | 是联合类型运算符,表示该参数可以是多种类型之一。 = None: 默认参数值,如果调用者没有提供context_var参数,则默认值为None。 -> None: 返回值类型注解,表示该方法不返回任何值。 总结 整个函数定义的意思是:这是一个类的构造方法,接受一个名为context_var的参数,该参数可以是一个ContextVar类型的对象(内部存储的是键为字符串、值为任意类型的字典),也可以是None,并且如果没有提供该参数,则默认值为None。此外,该方法不返回任何值。
rule
:URL 规则的字符串,例如/hello
。endpoint
:路由的端点名称,默认为None
。view_func
:处理该路由的视图函数,默认为 ``。
1. werkzeug 了解wsgi(werkzeug的run_simple
)
Flask 3.1.1 中的 WSGI 实现是其处理 HTTP 请求的核心机制,它基于 WSGI 规范将 HTTP 请求转化为 Python 可处理的对象,并通过上下文管理、路由分发等模块完成响应生成。
- flask框架基于 Werkzeug的wsgi(web服务网关接口)实现,flask自己没有wsgi
- 用户请求一旦到来就会执行
app.__call__
方法 - flask首先实例化运行app.run()
1.1通过app.run()解析源码
#app.run源码
def run(
self,
host: str | None = None,
port: int | None = None,
debug: bool | None = None,
load_dotenv: bool = True, #是否加载 .env 文件(Flask 2.0+ 已弃用,需手动处理)。
**options: t.Any, #传递给 werkzeug.serving.run_simple 的额外参数(如 use_reloader=True)。
) -> None:
server_name = self.config.get("SERVER_NAME")
sn_host = sn_port = None
# 解析 SERVER_NAME 配置 (格式: "host:port")
if server_name:
sn_host, _, sn_port = server_name.partition(":")
if not host:
if sn_host:
host = sn_host
else:
host = "127.0.0.1"
if port or port == 0:
port = int(port)
elif sn_port:
port = int(sn_port)
else:
port = 5000
from werkzeug.serving import run_simple
try:
run_simple(t.cast(str, host), port, self, **options)
finally:
# reset the first request information if the development server
# reset normally. This makes it possible to restart the server
# without reloader and that stuff from an interactive shell.
self._got_first_request = False
- 可以看到源码中调用
run_simple
启动Werkzeug服务器,传递host
,port
,self
(Flask应用实例)和额外选项(如debug=True
)。
✅所以本质上还是调用的werkzeug.serving中的run_simple。werkzeug.serving.run_simple()
功能与用途
-
启动服务器:
run_simple('127.0.0.1', 5000, app)
会在本地主机的 5000 端口上启动一个简单的 HTTP 服务器,运行app
这个 Flask 应用程序。-
werkzeug.serving.run_simple()中有一个srv.serve_forever()方法,srv是一个make_server()函数返回的BaseWSGIServer对象实例,里边包含了app。
-
BaseWSGIServer( host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd ) BaseWSGIServer这个类有一个serve_forever()方法,该方法启动一个无限循环,使服务器持续监听指定的 `hostname` 和 `port`。接收到请求会将请求的数据传递给 WSGI 应用程序,即括号里边的app。即run_simple(host,port,app)传递过来的app来生成Response来回复。
-
-
💡当客户端(如浏览器)发起 HTTP 请求时,服务器会立即接收并处理。对每个传入的请求,服务器会调用初始化时注册的
application
(这个里边run_simple('127.0.0.1', 5000, app)就是给了app这个函数去处理。- Flask中就是 Flask 应用实例本身(
app
对象)),生成响应并返回给客户端(Flask生成的响应是通过app.__call__去实现的,因为这里run_simple('127.0.0.1', 5000, app)有了请求就会调用app实例,所以会调用app.__all__去实现响应)。
- Flask中就是 Flask 应用实例本身(
-
-
它提供了自动重载、调试器等有用的功能:如参数
use_reloader=True
启用了自动重载功能;use_debugger=True
启用了调试器;threaded=True
启用多线程,可以同时处理多个请求;processes=2
指定使用 2 个进程处理请求;static_files
参数可以用于指定静态文件的映射,例如将/static
路径映射到本地文件系统中的某个目录。
所以我们可以写成如下代码:
# 这里自定义了响应
from werkzeug.serving import run_simple
# 定义 WSGI 应用(示例)
def myapp(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return [b'Hello, World!']
if __name__ == '__main__':
run_simple(
'localhost', # 主机名
5000, # 端口
myapp, # WSGI 应用对象
use_reloader=True, # 开启自动重载
use_debugger=True # 开启调试器
)
或者这样:
# 这个例子利用了app.route()里边的响应来回复run_simple()里的请求
from flask import Flask
from werkzeug.serving import run_simple
app = Flask(__name__)
@app.route('/')
def home():
return "Hello from Flask!"
if __name__ == '__main__':
run_simple(
'0.0.0.0',
8080,
app,
use_reloader=True,
threaded=True # 启用多线程
)
或者这样(等同于上边的@app.route('/')....):
# 这里是继承了werkzeug里的响应基类,来定义响应,app.route()里边深层也是写成了一个继承自BaseResponse的Response回复。
from werkzeug.serving import run_simple
from werkzeug.wrappers import BaseResponse
def func(environ, start_response):
print('请求来了')
response = BaseResponse('你好')
return response(environ, start_response)
if __name__ == '__main__':
run_simple('127.0.0.1', 5000, func)
1.2 app.__call__方法
本质上flask中还是用app.__call__来生成一个Response类来响应run_simple中的HTTP请求。
app.__call__具体源码:
# __call__函数
def __call__(self, environ: dict, start_response: t.Callable) -> t.Any:
return self.wsgi_app(environ, start_response)
# wsgi_app
def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any:
'''💎
ctx = self.request_context(environ)中其实做了封装的事情,内部封装三个值分别是
ctx.request=Request(environ) ctx.session = None ctx.app = app
'''
ctx = self.request_context(environ)
error: BaseException | None = None
try:
try:
ctx.push() # 💎第三节会讲到push的内容
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except: # noqa: B001
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if "werkzeug.debug.preserve_context" in environ:
environ["werkzeug.debug.preserve_context"](_cv_app.get())
environ["werkzeug.debug.preserve_context"](_cv_request.get())
if error is not None and self.should_ignore_error(error):
error = None
ctx.pop(error)
这段代码没什么可说的,就是按照WSGI的要求,接收特定的参数和返回特定的值。 值得关注的是内部的request
和response
以及请求上下文ctx
,他们将会实际处理请求。后面将解析flask的请求对象以及响应对象。
wsgi_app
具体实现见第三节(path匹配view function视图函数)
✅总结:
flask依赖werkzeug进行封装,上述内容只是简单介绍werkzeug的run_simple
,太过详细介绍werkzeug将会偏离主题,后续的分析中还会多次遇到werkzeug的源码,到时候再进行逐步分析。
flask是实现WSGI协议的框架,因此,处理请求的函数或者类(就是上面的wsgi_app)一定需要按照WSGI的接口要求,在此基础上再开展请求处理、响应处理、错误捕捉之类的内容。
知道了每次请求将会调用wsgi_app之后,我们就可以从wsgi_app内部开始分析(分析在下一节)。
2.Flask实例初始化
from flask import Flask
app = Flask(__name__)
# endpoint为视图函数定义别名,如果不写那endpoint默认等于下边的视图函数名字。url_for("idx")会获得链接
@app.route("/",endpoint = "idx" )
def index():
return "hello,world"
if __name__ == "main":
app.run()
2.1__init__方法
app = Flask(__name__)
会创建一个flask实例,会调用Flask类里的 __init__方法
-
python对象小知识点
__init__
方法:- 通常被称为构造函数。它在创建类的实例时自动调用,并用于初始化对象的状态。通过
__init__
方法,你可以定义一个对象被创建时应该拥有的属性。
class Dog: def __init__(self, name, age): self.name = name # 初始化name属性 self.age = age # 初始化age属性 def bark(self): return f"{self.name} says woof!" # 创建Dog类的一个实例 my_dog = Dog("Buddy", 3) # 访问实例的属性和方法 print(my_dog.name) # 输出: Buddy print(my_dog.age) # 输出: 3 print(my_dog.bark()) # 输出: Buddy says woof!
在这个例子中,
Dog
类有一个__init__
方法,它接受两个参数name
和age
,并在创建Dog
的实例时将这些值赋给实例的name
和age
属性。这样每个Dog
实例都会有自己的name
和age
属性。 - 通常被称为构造函数。它在创建类的实例时自动调用,并用于初始化对象的状态。通过
-
Flask类里的
__init__方法
源码:-
def __init__( self, import_name: str, static_url_path: str | None = None, #静态文件URL前缀(默认/static): static_folder: str | os.PathLike[str] | None = "static", #静态文件存放目录(默认"static") static_host: str | None = None, #静态文件的专用域名 host_matching: bool = False, #是否启用主机名匹配 subdomain_matching: bool = False, #是否启用子域名匹配 #模板文件目录(默认"templates") template_folder: str | os.PathLike[str] | None = "templates", instance_path: str | None = None, #实例文件路径 instance_relative_config: bool = False, # 配置文件是否相对于实例路径 root_path: str | None = None, #应用的根目录路径 ): self.cli = cli.AppGroup() #创建CLI命令组(flask命令的子命令 self.cli.name = self.name #设置命令组名称与应用名称一致 if self.has_static_folder: ## 检查是否存在静态文件夹 assert bool(static_host) == host_matching, ( "Invalid static_host/host_matching combination" ) self_ref = weakref.ref(self) # 注册静态文件路由 self.add_url_rule( f"{self.static_url_path}/<path:filename>", endpoint="static", host=static_host, view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 )
注意:这里静态文件也是添加路由进去的,
add_url_rule
添加路由对象Rule下边还会讲到
-
✅总结:
创建一个flask实例时,会调用__init__
方法主要对静态文件、模板、主机等进行了存放地址和值及路径的默认定义
3.路由视图源码
路由和视图函数绑定进-->rule-->map(add_url_rule)
先来看看flask中如何进行路由和视图函数绑定:
from flask import Flask
app = Flask(__name__)
@app.route('/route', methods=['GET'])
def route():
return 'route'
@app.get('/get')
def get():
return 'get'
@app.post('/post')
def post():
return 'post'
def add_func():
return 'add'
app.add_url_rule('/add', view_func=add_func)
if __name__ == '__main__':
app.run(host='127.0.0.1')
上面展示了flask中常见的路由绑定方式。
通过@app.route装饰器,传入路由地址,请求方式(默认GET请求)。 通过查看源码可以知道,不管使用app.get,app.post还是app.route,最后都是调用了app.add_url_rule进行绑定:
# .../falsk/sansio/scaffold.py
# get和post是通过传入具体请求方法,给外部提供了方便的调用方式
@setupmethod
def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
return self._method_route("GET", rule, options)
@setupmethod
def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
return self._method_route("POST", rule, options)
# 调用了route方法
def _method_route(
self,
method: str,
rule: str,
options: dict,
) -> t.Callable[[T_route], T_route]:
if "methods" in options:
raise TypeError("Use the 'route' decorator to use the 'methods' argument.")
return self.route(rule, methods=[method], **options)
@setupmethod
def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
def decorator(f: T_route) -> T_route:
endpoint = options.pop("endpoint", None)
self.add_url_rule(rule, endpoint, f, **options)
return f
return decorator
可以看到route函数对业务函数进行装饰。route接收一个route
路由地址,以及其他的options
键值对参数。判断options
有无endpoint
,这个将用来进行参数匹配。这里如果没有传入endpoint
,这里就会设置成空,在后续处理中会默认为视图函数名。
flask/sansio/scaffold.py提供了add_url_rule的函数签名,有两个实现,一个是提供上面普通调用方式,另一个是提供给蓝图。蓝图后续会继续分析。
@setupmethod
def add_url_rule(
self,
rule: str,
endpoint: str | None = None,
view_func: ft.RouteCallable | None = None,
provide_automatic_options: bool | None = None,
**options: t.Any,
) -> None:
raise NotImplementedError
具体实现:
# .../flask/sansio/app.py
@setupmethod
def add_url_rule(
self,
rule: str,
endpoint: str | None = None,
view_func: ft.RouteCallable | None = None,
provide_automatic_options: bool | None = None,
**options: t.Any,
) -> None:
if endpoint is None:
endpoint = _endpoint_from_view_func(view_func) # type: ignore
options["endpoint"] = endpoint
methods = options.pop("methods", None)
# if the methods are not given and the view_func object knows its
# methods we can use that instead. If neither exists, we go with
# a tuple of only ``GET`` as default.
if methods is None:
methods = getattr(view_func, "methods", None) or ("GET",)
if isinstance(methods, str):
raise TypeError(
"Allowed methods must be a list of strings, for"
' example: @app.route(..., methods=["POST"])'
)
methods = {item.upper() for item in methods}
# Methods that should always be added
required_methods = set(getattr(view_func, "required_methods", ()))
# starting with Flask 0.8 the view_func object can disable and
# force-enable the automatic options handling.
if provide_automatic_options is None:
provide_automatic_options = getattr(
view_func, "provide_automatic_options", None
)
if provide_automatic_options is None:
if "OPTIONS" not in methods:
provide_automatic_options = True
required_methods.add("OPTIONS")
else:
provide_automatic_options = False
# Add the required methods now.
methods |= required_methods
rule = self.url_rule_class(rule, methods=methods, **options)
rule.provide_automatic_options = provide_automatic_options # type: ignore
self.url_map.add(rule)
if view_func is not None:
old_func = self.view_functions.get(endpoint)
if old_func is not None and old_func != view_func:
raise AssertionError(
"View function mapping is overwriting an existing"
f" endpoint function: {endpoint}"
)
self.view_functions[endpoint] = view_func
通过上面的代码得知:
- 如果没有设置请求方法methods,就默认是GET,并规定methods是包含字符串的列表。还会有一个required_method。如果传入的视图函数对象
view_func
有required_methods
,就会通过methods |= required_methods
合并请求方法。 - 如果没有传入
endpoint
,就会调用_endpoint_from_view_func
,返回视图函数对象的名称:
def _endpoint_from_view_func(view_func: t.Callable) -> str:
"""Internal helper that returns the default endpoint for a given
function. This always is the function name.
"""
assert view_func is not None, "expected view func if endpoint is not provided."
return view_func.__name__
- 通过
provide_automatic_options
自动填充OPTIONS
接着是重点的路由绑定:
-
rule是
self.url_rule_class
返回的一个Rule
对象,并将Rule
对象存到url_map
中,url_map
是一个Map
对象。Rule
和Map
后续会展开分析。 -
这里会将视图函数通过
endpoint
作为key储存在view_functions
这个字典中。这边会有一个endpoint
是否绑定多个不同的函数。 这是什么意思呢?
首先看一下如果用route装饰同名的视图函数:@app.post('/post') def post(): return 'post' @app.post('/post2') def post(): return post
这是会报错的,因为两个视图函数都是【post】,但是他们并不是同一个对象,然后源码中是通过
if old_func is not None and old_func != view_func:
进行判断,所以这个过程将会是"/post"的enpoint
是【post】,第二次绑定的时候,"/post2"对应的enpoint
也是【post】,那么,这将会开始判断两个视图函数是否是同一个对象,不是,就报错。
因此,假如需要在不同的路由中使用同一个函数,可以通过这种方式:def post(): return 'post' app.add_url_rule('/post', view_func=post) app.add_url_rule('/post2', view_func=post)
这时候,视图函数名字一样,并且是属于同一个对象,这样就不会抛出错误了。
通过设置不同的
endpoint
也是可以避免错误的:@app.post('/post',endpoint='post') def post(): return 'post' @app.post('/post2',endpoint='post2') def post(): return post
看样子,
endpoint
和实际的业务,以及对接口调用者不相干,只负责视图函数的映射。但是从请求的角度来说,我们是通过路由找到的视图函数。endpoint
好像和路由有着什么关系,这个后续会对比Rule
、Map
和endpoint
三者的关系和作用。
path匹配view function视图函数
我们直接看一下当访问某个url地址的时候,flask是如何调用到对应的视图函数的:
python 体验AI代码助手 代码解读复制代码# flask/app.py
def wsgi_app(
self, environ: WSGIEnvironment, start_response: StartResponse
) -> cabc.Iterable[bytes]:
'''💎
ctx = self.request_context(environ)中其实做了封装的事情,内部封装三个值分别是
ctx.request=Request(environ) ctx.session = None ctx.app = app
'''
ctx = self.request_context(environ)
error: BaseException | None = None
try:
try:
# 💎下边讲到push的内容
ctx.push()
# 获取响应
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except: # noqa: B001
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if "werkzeug.debug.preserve_context" in environ:
environ["werkzeug.debug.preserve_context"](_cv_app.get())
environ["werkzeug.debug.preserve_context"](_cv_request.get())
if error is not None and self.should_ignore_error(error):
error = None
ctx.pop(error)
def full_dispatch_request(self) -> Response:
"""Dispatches the request and on top of that performs request
pre and postprocessing as well as HTTP exception catching and
error handling.
.. versionadded:: 0.7
"""
self._got_first_request = True
try:
request_started.send(self, _async_wrapper=self.ensure_sync)
rv = self.preprocess_request() #请求钩子中间件如before_request
if rv is None:
rv = self.dispatch_request()
except Exception as e:
rv = self.handle_user_exception(e)
return self.finalize_request(rv)
def dispatch_request(self) -> ft.ResponseReturnValue:
req = request_ctx.request
if req.routing_exception is not None:
self.raise_routing_exception(req)
# 获取请求中的Rule类
rule: Rule = req.url_rule # type: ignore[assignment]
# if we provide automatic options for this URL and the
# request came with the OPTIONS method, reply automatically
if (
getattr(rule, "provide_automatic_options", False)
and req.method == "OPTIONS"
):
return self.make_default_options_response()
# otherwise dispatch to the handler for that endpoint
view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment]
# 根据Rule类中的endpoint匹配对应的视图函数
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
根据wsgi的协议,每一个请求过来,都会调用满足wsgi协议的可调用对象,在flask中就是wsgi_app
。
最终调用的是dispatch_request
。
flask会从request_ctx
中获取请求上下文。请求上下文这个概念后续再进行展开,现在只要知道关于一个请求的所有信息,都会被储存在这个地方,包括了url地址,以及下面需要用到的endpoint。
最后flask通过return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
返回具体的视图函数调用结果。
这里可以看到,通过Rule类中的endpoint匹配上文中提到的储存好的视图函数。
这一步得知,url地址通过Rule类匹配endpoint
,endpoint
匹配视图函数。这是在falsk中完成的。
Rule类
Rule的构建需要werkzueg完成。 上文中的add_url_rule
函数中会初始化一个Rule类的实例:
python 体验AI代码助手 代码解读复制代码# sansio/app.py
def add_url_rule(
self,
rule: str,
endpoint: str | None = None,
view_func: ft.RouteCallable | None = None,
provide_automatic_options: bool | None = None,
**options: t.Any,
) -> None:
........
rule_obj = self.url_rule_class(rule, methods=methods, **options)
rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined]
self.url_map.add(rule_obj)
.......
它需要rule
url path和endpoint
,这也是后来可以根据path找到endpoint的原因。
重点在于self.url_map.add(rule_obj)
,这是Map类和Rule的关联。
Map类
和Rule类一样,Map类由werkzeug提供,flask会初始化一个Map类( 每个 Flask 实例只有一个 Map
对象):
python 体验AI代码助手 代码解读复制代码# sansio/app.py
self.url_map = self.url_map_class(host_matching=host_matching)
# 它会在add_url_rule中调用add函数
rule_obj = self.url_rule_class(rule, methods=methods, **options)
rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined]
self.url_map.add(rule_obj)
python 体验AI代码助手 代码解读复制代码# werkzeug/routing/map.py
def add(self, rulefactory: RuleFactory) -> None:
for rule in rulefactory.get_rules(self):
rule.bind(self)
if not rule.build_only:
self._matcher.add(rule)
self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
self._remap = True
# werkzeug/routing/rules.py
def bind(self, map: Map, rebind: bool = False) -> None:
if self.map is not None and not rebind:
raise RuntimeError(f"url rule {self!r} already bound to map {self.map!r}")
self.map = map
if self.strict_slashes is None:
self.strict_slashes = map.strict_slashes
if self.merge_slashes is None:
self.merge_slashes = map.merge_slashes
if self.subdomain is None:
self.subdomain = map.default_subdomain
self.compile()
可以看到,初始化一个Rule
实例之后,会调用add
函数进行绑定。
这里有点复杂,先看Rule.bind
。它会调用compile
进行编译。 详细的过程这里不展开,包括后续werkzeug如果解析url path,都不是本节的主要内容,后续会针对url解析的内容进行分析。
经过compile
函数之后,Rule
实例的_parts
获得了赋值,这是一个列表,列表的元素是RulePart
类的实例。
接下来是Map._matcher.add
。_matcher
是StateMachineMatcher
的实例。调用add
时会添加Rule
类:
# werkzeug/routing/matcher.py
@dataclass
class State:
dynamic: list[tuple[RulePart, State]] = field(default_factory=list)
rules: list[Rule] = field(default_factory=list)
static: dict[str, State] = field(default_factory=dict)
class StateMachineMatcher:
def __init__(self, merge_slashes: bool) -> None:
self._root = State()
self.merge_slashes = merge_slashes
def add(self, rule: Rule) -> None:
state = self._root
for part in rule._parts:
if part.static:
state.static.setdefault(part.content, State())
state = state.static[part.content]
else:
for test_part, new_state in state.dynamic:
if test_part == part:
state = new_state
break
else:
new_state = State()
state.dynamic.append((part, new_state))
state = new_state
state.rules.append(rule)
这就是当使用@app.route
时候的全部过程了。但是现在还不是很明确,Rule
和Map
的关系,要结合一下请求进来的时候发生了什么。
endpoint、Rule、Map
上文提到,当请求进来的时候,通过wsgi_app
,最后是调用了self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
这是通过Rule
获得了endpoint
,用endpoint
去匹配view_functions
的视图函数。
那么在此之前,还需要完成如果从url path到Rule这一步。
可以在代码中看到Rule
对象都是从请求上下文中获取到的:
req = request_ctx.request
rule: Rule = req.url_rule
请求上下文现在不展开分析,只要知道,它是一个栈,每次请求过来,都会将所需要的内容储存在一个对象中,并压入栈。这里所需的内容,就包括了url path。
# flask/app.py
def wsgi_app(
self, environ: WSGIEnvironment, start_response: StartResponse
) -> cabc.Iterable[bytes]:
ctx = self.request_context(environ)
error: BaseException | None = None
try:
try:
# 入栈
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except: # noqa: B001
error = sys.exc_info()[1]
raise
return response(environ, start_response)
......
看一下push
中发生了什么:
# flask/ctx.py RequestContext类中的push
def push(self) -> None:
app_ctx = _cv_app.get(None)
if app_ctx is None or app_ctx.app is not self.app:
'''
#💎这里self.app即app,可以查看ctx这个实例的字段知道。
这里创建了一个app_ctx,封装了如下内容
app_ctx.app = app
app.g = None
'''
app_ctx = self.app.app_context()
app_ctx.push() #💎见下边的push代码
else:
app_ctx = None
"""
💎_cv_request.set(self),把ctx(封装了app,session,request内容)放进ContextVar中
首先是`_cv_request.set(self)`,这是把`self`也就是`ctx`实例存入`Contextvar`中。
并返回一个`token`。`token`可以通过`reset`来重置,让`Contextvar`设置成上一个值。`get`获取值
将此时的ctx的token和app_ctx都放入token中,方便重置
"""
self._cv_tokens.append((_cv_request.set(self), app_ctx))
if self.session is None: #💎往session里边放值
session_interface = self.app.session_interface
self.session = session_interface.open_session(self.app, self.request)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
if self.url_adapter is not None: #self.url_adapter 是 URLAdapter 对象,用于解析URL
self.match_request() #match_request() 会根据请求的URL和HTTP方法,确定匹配的视图函数和路由参数。
####################
# 实现路由匹配
def match_request(self) -> None:
try:
'''
# 使用 url_adapter.match(即 werkzeug.routing.map.py/MapAdapter.match)匹配当前请求的URL。
match方法中有一句:result = self.map._matcher.match(domain_part, path_part, method, websocket)
用的是routing/matcher.py下的match方法
match里:
parts: 待匹配的路径部分列表(由 path.split("/") 生成)。
静态匹配:优先尝试静态路由(state.static字典):if part in state.static:...
动态匹配:通过正则表达式匹配可变部分(如 <int:id>),
遍历动态路由规则(state.dynamic列表):target = part
match = re.compile(test_part.content).match(target)
如果匹配成功,返回 (Rule 对象, values)。当 URL 包含动态路由部分,values是提取的这部分值
(state是StateMachineMatcher里的一个方法,而StateMachineMatcher又是rule存到map里的中间对象。)
'''
result = self.url_adapter.match(return_rule=True)
self.request.url_rule, self.request.view_args = result # type: ignore
except HTTPException as e:
self.request.routing_exception = e
# flask/ctx.py AppContext类中的push 即上边的app_ctx.push()
def push(self) -> None:
"""
💎_cv_app.set(self),把app_ctx(封装了app,g内容)放进ContextVar中
首先是`_cv_app.set(self)`,这是把`self`也就是`app_ctx`实例存入`Contextvar`中。
并返回一个`token`。`token`可以通过`reset`来重置,让`Contextvar`设置成上一个值。`get`获取值。
"""
self._cv_tokens.append(_cv_app.set(self))
appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync)
可以看到这一步,就将Rule
类以及请求view_args
绑定在request
上面,然后就是上面的获取到。那么关键在url_adapter.match
这边。
# flask/ctx.py
class RequestContext:
def __init__(
self,
app: Flask,
environ: WSGIEnvironment,
request: Request | None = None,
session: SessionMixin | None = None,
) -> None:
self.url_adapter = None
try:
self.url_adapter = app.create_url_adapter(self.request)
except HTTPException as e:
self.request.routing_exception = e
....
# flask/app.py
def create_url_adapter(self, request: Request | None) -> MapAdapter | None:
.......
if self.config["SERVER_NAME"] is not None:
return self.url_map.bind(
self.config["SERVER_NAME"],
script_name=self.config["APPLICATION_ROOT"],
url_scheme=self.config["PREFERRED_URL_SCHEME"],
)
return None
# werkzeug/routing/map.py
def bind_to_environ(
self,
environ: WSGIEnvironment | Request,
server_name: str | None = None,
subdomain: str | None = None,
) -> MapAdapter:
....
def _get_wsgi_string(name: str) -> str | None:
val = env.get(name)
if val is not None:
return _wsgi_decoding_dance(val)
return None
script_name = _get_wsgi_string("SCRIPT_NAME")
path_info = _get_wsgi_string("PATH_INFO")
query_args = _get_wsgi_string("QUERY_STRING")
return Map.bind(
self,
server_name,
script_name,
subdomain,
scheme,
env["REQUEST_METHOD"],
path_info,
query_args=query_args,
)
def bind(
self,
server_name: str,
script_name: str | None = None,
subdomain: str | None = None,
url_scheme: str = "http",
default_method: str = "GET",
path_info: str | None = None,
query_args: t.Mapping[str, t.Any] | str | None = None,
) -> MapAdapter:
......
return MapAdapter(
self,
f"{server_name}{port_sep}{port}",
script_name,
subdomain,
url_scheme,
path_info,
default_method,
query_args,
)
可以看到,最后返回的是一个MapAdapter
对象。它的match
函数最后会调用StateMachineMatcher
的match
函数,返回Rule
对象和view_args
:
# routing/matcher.py
def match(
self, domain: str, path: str, method: str, websocket: bool
) -> tuple[Rule, t.MutableMapping[str, t.Any]]:
# To match to a rule we need to start at the root state and
# try to follow the transitions until we find a match, or find
# there is no transition to follow.
have_match_for = set()
websocket_mismatch = False
def _match(
state: State, parts: list[str], values: list[str]
) -> tuple[Rule, list[str]] | None:
.......
if parts == [""]:
for rule in state.rules:
if rule.strict_slashes:
continue
if rule.methods is not None and method not in rule.methods:
have_match_for.update(rule.methods)
elif rule.websocket != websocket:
websocket_mismatch = True
else:
return rule, values
return None
所以,endpoint
、Rule
、Map
三者关系可以这么理解:
- 程序启动的时候,通过
add_url_rule
做了这几件事: -
- 初始化
Rule
对象,这里Rule
实例保存了endpoint
- 初始化
-
Rule
进行compile
,存入StateMachineMatcher
,StateMachineMatcher
存在Map
中
- 请求过来的时候:
-
- 通过请求上下文获取到请求信息,包括url path
-
Map
根据url path找到Rule
。
-
- 通过
Rule
对象的endpoint
去匹配对应的视图函数
- 通过
所以三者的关系是:通过Map
找到Rule
,通过Rule
找到endpoint
,通过endpoint
找到定义好的视图函数。
✅总结
本篇主要进行了flask的路由分析,从最开始的@app.route
到最后请求的过程。其中涉及到的其他的内容,比如上下文,具体如何解析url和请求参数,并不详细展开。上下文的内容会在下一篇目开始分析。flask路由系统的其他细节,包括解析url,匹配Rule
,正则转换器等等会在系列最后进行补充。
flask的路由复杂的地方在于Rule
类和Map
类的关系。从分析结果来看,endpoint
和Rule
的匹配是由flask完成的,而Rule
和Map
类的匹配是由werkzeug提供的。
4.上下文
在一段文字或者对话中,如果不知道上下文,可能就会对这段文字感到疑惑,因为你缺少了一些信息。 比如文字中出现了【他】,【她】,我们得从上下文中才能知道指代的是谁。在程序里面也是如此,所谓的上下文,就是包含了程序的各种变量信息。 例如一次API请求包含了这次请求的请求头、请求地址、参数等等。 一个函数可能需要调用另外一个函数,第二个函数需要知道这次请求的某些信息,这些就是上下文。
请求上下文
在上一篇中,我们看到,请求进来之后,会根据【path】找到已经绑定好的视图函数,最后返回视图函数的调用结果。这里不再累述,可以去看一下上一篇。现在主要看一下,在调用视图函数的过程中,发生了什么。
# flask/app.py
def wsgi_app(
self, environ: WSGIEnvironment, start_response: StartResponse
) -> cabc.Iterable[bytes]:
ctx = self.request_context(environ)
error: BaseException | None = None
try:
try:
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except: # noqa: B001
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if "werkzeug.debug.preserve_context" in environ:
environ["werkzeug.debug.preserve_context"](_cv_app.get())
environ["werkzeug.debug.preserve_context"](_cv_request.get())
if error is not None and self.should_ignore_error(error):
error = None
ctx.pop(error)
ctx = self.request_context(environ)
是传入environ
,生成一个请求对象。现在不相信分析具体是做什么的,只要知道ctx
包含了一个请求所有的内容,请求地址,请求头,请求参数等等。
接着重点来了,在调用full_dispatch_request
之前,进行了ctx.push()
,在整个过程处理完之后,进行了ctx.pop(error)
。
这个push和pop看起来是入栈和出栈。 也就是在处理请求之前,flask会将包含了请求的上下文ctx
入栈,处理完请求之后,进行出栈。
这样做的目的是什么? 由上面的例子可以看到,只要导入了全局变量request
,那么就可以直接使用这次请求的上下文,所以这个入栈和出栈,或许是将请求上下文存到全局变量request
中,这样就可以全局调用。
# flask/app.py
def dispatch_request(self) -> ft.ResponseReturnValue:
'''globals.py中
_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx") #ctx在这里边
request_ctx: RequestContext = LocalProxy( _cv_request, unbound_message=_no_req_msg)
request_ctx.request就相当于LocalProxy( _cv_request,request),也就是全局变量里去找request。
'''
req = request_ctx.request
if req.routing_exception is not None:
self.raise_routing_exception(req)
rule: Rule = req.url_rule # type: ignore[assignment]
# if we provide automatic options for this URL and the
# request came with the OPTIONS method, reply automatically
if (
getattr(rule, "provide_automatic_options", False)
and req.method == "OPTIONS"
):
return self.make_default_options_response()
# otherwise dispatch to the handler for that endpoint
view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment]
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
在调用dispatch_request
的时候,可以看到,并不是将上面的ctx
传入dispatch_request
,而是通过request_ctx.request
,也就是说,在这里,和在外部视图函数使用的方式是一样的,使用request
全局变量,获取上下文。
push和pop
python 体验AI代码助手 代码解读复制代码# flask/globals.py
# ....
_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx")
request_ctx: RequestContext = LocalProxy( # type: ignore[assignment]
_cv_request, unbound_message=_no_req_msg
)
request: Request = LocalProxy( # type: ignore[assignment]
_cv_request, "request", unbound_message=_no_req_msg
)
这些是flask提供的全局变量。request
和request_ctx
都可以全局使用,只是封装的程度不一样。
上文提到,在wsgi_app
函数中,请求到来的时候,调用了ctx.push
入栈,处理完成之后,进行出栈。 这是push
的源码:
python 体验AI代码助手 代码解读复制代码# flask/ctx.py
def push(self) -> None:
# Before we push the request context we have to ensure that there
# is an application context.
app_ctx = _cv_app.get(None)
if app_ctx is None or app_ctx.app is not self.app:
app_ctx = self.app.app_context()
app_ctx.push()
else:
app_ctx = None
self._cv_tokens.append((_cv_request.set(self), app_ctx))
if self.session is None:
session_interface = self.app.session_interface
self.session = session_interface.open_session(self.app, self.request)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
if self.url_adapter is not None:
self.match_request()
进行push
的时候,先获取应用上下文,并且入栈,这里先不分析应用上下文,后面会讲。
关键的是self._cv_tokens.append((_cv_request.set(self), app_ctx))
这里做了两件事,首先是_cv_request.set(self)
,这是把self
也就是RequestContext
实例存入Contextvar
中。
Contextvar是python3.7加入的一个模块,详情看文档:contextvars --- 上下文变量 — Python 3.12.3 文档
这里不展开如何实现,主要知道几个内容就行:
python 体验AI代码助手 代码解读复制代码# 协程
import asyncio
from contextvars import ContextVar
# 定义一个 ContextVar
cv = ContextVar('example_var')
async def coroutine(identifier):
# 设置 ContextVar 的值,并获取返回的 Token
token = cv.set(f'value for {identifier}')
print(f'Coroutine {identifier} set value to: {cv.get()}')
# 模拟异步操作
await asyncio.sleep(1)
# 获取当前 ContextVar 的值
print(f'Coroutine {identifier} sees value: {cv.get()}')
# 重置 ContextVar 的值
cv.reset(token)
print(f'Coroutine {identifier} reset value, now: {cv.get(None)}')
async def main():
await asyncio.gather(coroutine('A'), coroutine('B'))
asyncio.run(main())
# 多线程
import threading
from contextvars import ContextVar
# 定义一个 ContextVar
cv = ContextVar('example_var')
def thread_function(identifier):
# 设置 ContextVar 的值,并获取返回的 Token
token = cv.set(f'value for {identifier}')
print(f'Thread {identifier} set value to: {cv.get()}')
# 模拟一些操作
threading.Event().wait(1)
# 获取当前 ContextVar 的值
print(f'Thread {identifier} sees value: {cv.get()}')
# 重置 ContextVar 的值
cv.reset(token)
print(f'Thread {identifier} reset value, now: {cv.get(None)}')
# 创建并启动线程
threads = []
for i in range(2):
thread = threading.Thread(target=thread_function, args=(f'Thread-{i}',))
threads.append(thread)
thread.start()
# 等待所有线程完成
for thread in threads:
thread.join()
set
接收一个值,存入Contextvar
实例,并返回一个token
。token
可以通过reset
来重置,让Contextvar
设置成上一个值。get
获取值。
因此,self._cv_tokens.append((_cv_request.set(self), app_ctx))
的作用是把RequestContext
实例存入_cv_request
,将返回的token
和app_ctx
应用上下文存到self._cv_tokens
。
接着是pop
出栈:
python 体验AI代码助手 代码解读复制代码# flask/ctx.py
def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
"""Pops the request context and unbinds it by doing that. This will
also trigger the execution of functions registered by the
:meth:`~flask.Flask.teardown_request` decorator.
.. versionchanged:: 0.9
Added the `exc` argument.
"""
clear_request = len(self._cv_tokens) == 1 #判断是否等于1如果等于1,clear_request=True,否则False
try:
if clear_request:
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_request(exc)
request_close = getattr(self.request, "close", None)
if request_close is not None:
request_close()
finally:
ctx = _cv_request.get()
token, app_ctx = self._cv_tokens.pop()
_cv_request.reset(token)
# get rid of circular dependencies at the end of the request
# so that we don't require the GC to be active.
if clear_request:
ctx.request.environ["werkzeug.request"] = None
if app_ctx is not None:
app_ctx.pop(exc)
if ctx is not self:
raise AssertionError(
f"Popped wrong request context. ({ctx!r} instead of {self!r})"
)
这里会判断self._cv_tokens
,如果请求结束了,会执行通过@app.teardown_request
绑定的内容。teardown_request
装饰器会注册一个函数,在请求结束后会执行。通常用来释放资源。
然后通过获取到的token
,进行reset
。
这是push
和pop
的过程。
LocalProxy
# flask/globals.py
_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx")
request_ctx: RequestContext = LocalProxy( # type: ignore[assignment]
_cv_request, unbound_message=_no_req_msg
)
request: Request = LocalProxy( # type: ignore[assignment]
_cv_request, "request", unbound_message=_no_req_msg
)
session: SessionMixin = LocalProxy( # type: ignore[assignment]
_cv_request, "session", unbound_message=_no_req_msg
)
可以看到_cv_request
是一个ContextVar
,作用如上文所说的,用来储存RequestContext
实例。
request_ctx
和request
都是LocalProxy
的实例,它们的作用是什么?
*这里有一个细节,源码中用了# type: ignore[assignment] 这是为了让ide在不出现warning,因为request
类型推导是'Request',但是值却是LocalProxy
。
看一下RequestContext
的构造:
python 体验AI代码助手 代码解读复制代码flask/ctx.py
class RequestContext:
def __init__(
self,
app: Flask,
environ: WSGIEnvironment,
request: Request | None = None,
session: SessionMixin | None = None,
) -> None:
self.app = app
if request is None:
request = app.request_class(environ)
request.json_module = app.json
self.request: Request = request
self.url_adapter = None
try:
self.url_adapter = app.create_url_adapter(self.request)
except HTTPException as e:
self.request.routing_exception = e
self.flashes: list[tuple[str, str]] | None = None
self.session: SessionMixin | None = session
# Functions that should be executed after the request on the response
# object. These will be called before the regular "after_request"
# functions.
self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = []
self._cv_tokens: list[
tuple[contextvars.Token[RequestContext], AppContext | None]
] = []
RequestContext
有个属性:request
,它是通过environ
构造的。
试着对比两个对象是否相等:
python 体验AI代码助手 代码解读复制代码from flask import Flask,request
from flask.globals import request_ctx
app = Flask(__name__)
@app.get('/')
def post():
print(request is request_ctx)
print(request.headers is request_ctx.request.headers)
return 'hello world'
if __name__ == '__main__':
app.run(port='8080')
# 结果:
# False
# True
这是很显然的,因为request
和request_ctx
都是LocalProxy
的实例,并不是同一个对象。 但是内容都是一样的。现在问题来了:LocalProxy做了什么?
python 体验AI代码助手 代码解读复制代码# werkzeug/local.py
Class LocalProxy(t.Generic[T]):
__slots__ = ("__wrapped", "_get_current_object")
_get_current_object: t.Callable[[], T]
def __init__(
self,
local: ContextVar[T] | Local | LocalStack[T] | t.Callable[[], T],
name: str | None = None,
*,
unbound_message: str | None = None,
) -> None:
if name is None:
get_name = _identity
else:
get_name = attrgetter(name) # type: ignore[assignment]
if unbound_message is None:
unbound_message = "object is not bound"
if isinstance(local, Local):
if name is None:
raise TypeError("'name' is required when proxying a 'Local' object.")
def _get_current_object() -> T:
try:
return get_name(local) # type: ignore[return-value]
except AttributeError:
raise RuntimeError(unbound_message) from None
elif isinstance(local, LocalStack):
def _get_current_object() -> T:
obj = local.top
if obj is None:
raise RuntimeError(unbound_message)
return get_name(obj)
elif isinstance(local, ContextVar):
def _get_current_object() -> T:
try:
obj = local.get()
except LookupError:
raise RuntimeError(unbound_message) from None
return get_name(obj)
elif callable(local):
def _get_current_object() -> T:
return get_name(local())
else:
raise TypeError(f"Don't know how to proxy '{type(local)}'.")
object.__setattr__(self, "_LocalProxy__wrapped", local)
object.__setattr__(self, "_get_current_object", _get_current_object)
flask使用了werkzeug中的LocalProxy
,代理了RequestContext
和requests
对象。可以看到local
过去是使用了threading.local
实现,现在用的这个版本是Contextvar
实现。
python 体验AI代码助手 代码解读复制代码request_ctx: RequestContext = LocalProxy( # type: ignore[assignment]
_cv_request, unbound_message=_no_req_msg
)
request: Request = LocalProxy( # type: ignore[assignment]
_cv_request, "request", unbound_message=_no_req_msg
)
如果local
是Contextvar
,则用get
返回一个RequestContext
对象。二者的差别在于传入了一个name
,如果没有传入name,get_name
返回的是对象本身,也就是request_ctx在这里会返回RequestContext
,而request会返回RequestContext.request
,所以上文提到的,两者内容是一样的。
接下来LocalProxy
把一些魔术方法交给了_ProxyLookup
。有很多魔术方法,这边只关注__getattr__
:
python 体验AI代码助手 代码解读复制代码....
__getattr__ = _ProxyLookup(getattr)
....
python 体验AI代码助手 代码解读复制代码# werkzeug/local.py
class _ProxyLookup:
__slots__ = ("bind_f", "fallback", "is_attr", "class_value", "name")
def __init__(
self,
f: t.Callable[..., t.Any] | None = None,
fallback: t.Callable[[LocalProxy[t.Any]], t.Any] | None = None,
class_value: t.Any | None = None,
is_attr: bool = False,
) -> None:
bind_f: t.Callable[[LocalProxy[t.Any], t.Any], t.Callable[..., t.Any]] | None
if hasattr(f, "__get__"):
# A Python function, can be turned into a bound method.
def bind_f(
instance: LocalProxy[t.Any], obj: t.Any
) -> t.Callable[..., t.Any]:
return f.__get__(obj, type(obj)) # type: ignore
elif f is not None:
# A C function, use partial to bind the first argument.
def bind_f(
instance: LocalProxy[t.Any], obj: t.Any
) -> t.Callable[..., t.Any]:
return partial(f, obj)
else:
# Use getattr, which will produce a bound method.
bind_f = None
self.bind_f = bind_f
self.fallback = fallback
self.class_value = class_value
self.is_attr = is_attr
def __set_name__(self, owner: LocalProxy[t.Any], name: str) -> None:
self.name = name
def __get__(self, instance: LocalProxy[t.Any], owner: type | None = None) -> t.Any:
if instance is None:
if self.class_value is not None:
return self.class_value
return self
try:
obj = instance._get_current_object()
except RuntimeError:
if self.fallback is None:
raise
fallback = self.fallback.__get__(instance, owner)
if self.is_attr:
# __class__ and __doc__ are attributes, not methods.
# Call the fallback to get the value.
return fallback()
return fallback
if self.bind_f is not None:
return self.bind_f(instance, obj)
return getattr(obj, self.name)
def __repr__(self) -> str:
return f"proxy {self.name}"
def __call__(
self, instance: LocalProxy[t.Any], *args: t.Any, **kwargs: t.Any
) -> t.Any:
"""Support calling unbound methods from the class. For example,
this happens with ``copy.copy``, which does
``type(x).__copy__(x)``. ``type(x)`` can't be proxied, so it
returns the proxy type and descriptor.
"""
return self.__get__(instance, type(instance))(*args, **kwargs)
过程是这样的:
- 在调用
request.headers
的时候,调用了LocalProxy.__getattr__
LocalProxy.__getattr__
交给_ProxyLookup
,调用_ProxyLookup.__get__
._ProxyLookup.__get__
内部调用instance._get_current_object()
获取实际对象,也就是这一段:
python 体验AI代码助手 代码解读复制代码elif isinstance(local, ContextVar):
def _get_current_object() -> T:
try:
obj = local.get()
except LookupError:
raise RuntimeError(unbound_message) from None
return get_name(obj)
这里将会返回request
。如果是request_ctx将会返回RequestContext
。
- 接着
_ProxyLookup
会返回一个偏函数:
python 体验AI代码助手 代码解读复制代码elif f is not None:
# A C function, use partial to bind the first argument.
def bind_f(
instance: LocalProxy[t.Any], obj: t.Any
) -> t.Callable[..., t.Any]:
return partial(f, obj)
这里的f
就是__getattr__ = _ProxyLookup(getattr)
中的getattr
- 最后,这个偏函数接收
headers
,使用getattr
,返回结果。
可以做这样的实验,把这段源码改一下:
python 体验AI代码助手 代码解读复制代码
if self.bind_f is not None:
# return self.bind_f(instance, obj)
func = self.bind_f(instance, obj)
print(func('headers',obj)) # 这里的obj就是request或者RequestContext对象了。
return func
总结
上下文管理是flask的一个亮点,可以直接导入全局变量来获取上下文的内容。上文主要分析了请求上下文,还有应用上下文current_app
,以及session
和g
。 实现方式都是差不多的。后续遇到再详细展开。
flask通过Contextvar
(旧版本是通过threading.local
)来实现线程/协程安全,让不同的请求互相隔离,又通过全局变量的方式简化上下文管理,不需要一直传参。
那么现在一次请求的过程就是这样的:
- 请求来了,实例化
RequestContext
,实例对象push
,入栈。 - 再
push
中,将RequestContext
对象存入Contextvar。 - 派发请求
full_dispatch_request
、dispatch_request
等等,再函数内部,全局变量,获取请求上下文,还有请求前,请求后的各种处理。这里就体现了flask上下文管理的精妙,不需要传递请求上下文了。 - 请求处理结束,通过
pop
从Contextvar
中重置数据。
过程差不多这样:
总结
1.falsk实现流程:
- 前期准备:
- 1.
__init__
方法对静态文件、模板、主机等进行了存放地址和值及路径的默认定义 - app.add_url_rule函数将路由和视图函数绑定后放入Rule类中,
Rule
进行compile
,存入StateMachineMatcher
,StateMachineMatcher
存在Map
中。同时将endpoint
作为key视图函数作为value储存在app.add_url_rule函数的view_functions
这个字典中。
- 1.
- run_simple中的srv.serve_forever()进行监听,外部请求来了之后,run_simple调用
app.__call__
. app.__call__中会执行wsgi_app函数
,wsgi_app函数会建立以内存为基础的request_context上下文变量ctx,通过ctx.push将包含url在内的请求信息push进栈。wsgi_app函数中的full_dispatch_request()函数会执行preprocess_request()方法来执行before_request钩子。之后再执行full_dispatch_request()函数中的dispatch_request()方法- dispatch_request()方法会根据req = request_ctx.request rule: Rule = req.url_rule这些参数获得rule.endpoint,然后根据endpoint,获取视图函数self.view_functions[rule.endpoint],然后执行视图函数。
- req = request_ctx.request rule: Rule = req.url_rule参数怎么获取的呢?
- 首先看刚才wsgi_app()中的ctx.push实现了什么:1.解析url;2. 调用self.match_request() 函数,会根据请求的URL和HTTP方法,确定匹配的视图函数和路由参数。
- match_request() 函数的实现:它使用 url_adapter(即 werkzeug.routing.MapAdapter)的match方法匹配当前请求的URL。返回值为(rule, view_args)。self.request.url_rule, self.request.view_args = result。这样上边就可以得到 rule=request_ctx.request.url_rule了。
- url_adapter.match是怎么匹配的呢?通过map._matcher.match()进行匹配
- map._matcher.match()又通过什么呢?通过 Map对象中的StateMachineMatcher中的state函数来判断rule in state.rules,进而找到rule的endpoint。
好的,我重新绘制了Flask WSGI请求响应流程图,这次使用更可靠的序列图格式:
Flask WSGI请求响应流程图
流程说明与关键方法解析
-
入口点 (
__call__
):- Web服务器调用
Flask.__call__(environ, start_response)
- 实际转发到
wsgi_app()
方法处理
- Web服务器调用
-
上下文创建 (
request_context()
):ctx = self.request_context(environ) # 创建RequestContext对象 ctx.push() # 激活上下文
-
核心处理 (
full_dispatch_request()
):preprocess_request()
: 执行所有@app.before_request
注册的函数dispatch_request()
: 核心路由分发方法# 伪代码 rule = request.url_rule # 获取匹配的路由规则 return self.view_functions[rule.endpoint](**request.view_args)
-
响应生成 (
make_response()
):- 将视图返回值转换为标准
Response
对象 - 支持多种返回类型:字符串、元组、Response对象等
- 将视图返回值转换为标准
-
后处理 (
process_response()
):- 执行所有
@app.after_request
注册的函数 - 逆序执行(最后注册的函数最先执行)
- 执行所有
-
上下文清理 (
auto_pop()
):finally: ctx.auto_pop(exc) # 确保始终执行
- 执行
@app.teardown_request
注册的函数 - 弹出请求上下文(
_request_ctx_stack.pop()
)
- 执行
-
响应返回:
- 调用
response(environ, start_response)
生成WSGI响应 - 返回给Web服务器发送给客户端
- 调用
关键类与方法总结表
阶段 | 关键类/方法 | 作用描述 |
---|---|---|
入口 | Flask.__call__ |
WSGI接口入口点 |
上下文 | Flask.request_context() |
创建请求上下文 |
RequestContext.push() |
激活上下文到栈中 | |
预处理 | Flask.preprocess_request() |
执行before_request钩子 |
路由 | Flask.dispatch_request() |
路由匹配与视图函数调用 |
响应 | Flask.make_response() |
转换视图返回值 |
后处理 | Flask.process_response() |
执行after_request钩子 |
清理 | RequestContext.auto_pop() |
执行teardown_request钩子并清理上下文 |
错误 | Flask.handle_exception() |
异常处理(图中未展示) |
这个流程图清晰地展示了请求从进入Flask应用到返回响应的完整生命周期,包含了所有关键的方法调用和钩子函数的执行时机。
RequestContext
、request_ctx
、request
和 LocalProxy
区别联系
在 Flask 中,RequestContext
、request_ctx
、request
和 LocalProxy
是紧密相关的概念,它们共同构成了 Flask 的上下文管理机制。以下是它们的定义、关系和区别:
1. RequestContext
(请求上下文)
-
定义:
RequestContext
是 Flask 的 请求上下文对象,封装了与当前 HTTP 请求相关的所有数据(如request
对象、session
、g
等)。- 每个 HTTP 请求都会创建一个新的
RequestContext
实例,并将其推入 Flask 的请求上下文栈(_request_ctx_stack
)中。
-
作用:
- 提供请求级别的数据隔离,确保每个请求的数据独立(例如不同用户的请求不会互相干扰)。
- 在请求处理过程中,Flask 通过
RequestContext
访问request
、session
、g
等对象。
-
生命周期:
- 当请求到达时自动创建,请求结束后自动销毁。
-
示例:
from flask import Flask, request app = Flask(__name__) @app.route('/') def index(): # request 对象来自当前的 RequestContext print(request.method) # GET return "Hello, World!"
2. request_ctx
(请求上下文栈)
-
定义:
request_ctx
是 Flask 内部用于管理请求上下文的 栈结构(_request_ctx_stack
),它是werkzeug.local.LocalStack
的一个实例。- 每个请求的
RequestContext
会被推入request_ctx
栈中,确保在请求处理过程中可以随时访问当前的上下文。
-
作用:
- 通过栈结构管理多个请求上下文(例如嵌套请求或异步请求),确保线程安全。
- Flask 的
LocalProxy
(如request
、session
)通过request_ctx
栈动态获取当前请求的上下文。
-
关键代码:
# Flask 源码中的 _request_ctx_stack(简化版) from werkzeug.local import LocalStack _request_ctx_stack = LocalStack()
3. request
(请求对象)
-
定义:
request
是 Flask 提供的 全局对象,用于访问当前请求的数据(如request.method
、request.args
等)。- 实际上,
request
是一个LocalProxy
对象,它会动态地从request_ctx
栈中获取当前请求的RequestContext.request
属性。
-
作用:
- 提供便捷的接口访问请求数据(如表单、JSON、文件上传等)。
- 仅在请求上下文内有效(即只能在视图函数或请求处理逻辑中使用)。
-
示例:
from flask import request @app.route('/data', methods=['POST']) def handle_data(): # 通过 request 访问请求数据 data = request.json return jsonify(data)
4. LocalProxy
(本地代理)
-
定义:
LocalProxy
是 Werkzeug 提供的一个 代理类,用于在全局范围内安全地访问线程局部数据(如request
、current_app
)。- 它的核心作用是:动态绑定当前线程的上下文对象,避免显式传递上下文。
-
作用:
- 允许开发者通过全局变量(如
request
、current_app
)访问当前请求或应用的上下文数据。 - 通过
LocalProxy
,Flask 实现了上下文感知的全局对象,确保线程安全。
- 允许开发者通过全局变量(如
-
实现原理:
LocalProxy
会调用__getattr__
方法,从request_ctx
或app_ctx
栈中获取当前线程的上下文对象。- 例如,
request
的底层实现是:request = LocalProxy(partial(_lookup_req_object, "request"))
-
示例:
from flask import request, current_app # request 和 current_app 都是 LocalProxy 对象 print(request.method) # 动态获取当前请求的 method print(current_app.name) # 动态获取当前应用的 name
5. 关系与区别总结
概念 | 作用 | 生命周期 | 线程安全 | 是否全局对象 |
---|---|---|---|---|
RequestContext |
封装请求数据(如 request 、session ),管理请求上下文的生命周期。 |
请求开始时创建,请求结束时销毁。 | ✅ | ❌ |
request_ctx |
Flask 内部的请求上下文栈(_request_ctx_stack ),管理多个 RequestContext 。 |
应用启动时初始化,持续存在。 | ✅ | ❌ |
request |
通过 LocalProxy 动态访问当前请求的数据(如 request.method )。 |
请求开始时可用,请求结束时失效。 | ✅ | ✅ |
LocalProxy |
代理类,用于全局访问当前线程的上下文对象(如 request 、current_app )。 |
应用启动时初始化,持续存在。 | ✅ | ✅ |
6. 典型流程
- 请求到达:
- Flask 创建
RequestContext
,并将其推入request_ctx
栈。
- Flask 创建
- 处理请求:
- 视图函数中通过
request
(LocalProxy
)访问请求数据。 LocalProxy
从request_ctx
栈中获取当前的RequestContext.request
。
- 视图函数中通过
- 请求结束:
RequestContext
从request_ctx
栈中弹出并销毁。
7. 常见问题
-
为什么
request
是全局的?request
是LocalProxy
,通过线程局部存储(threading.local
)实现线程隔离,确保每个请求的request
独立。
-
如何在非请求环境中使用
request
?- 必须手动推送请求上下文:
with app.test_request_context('/path'): print(request.path) # /path
- 必须手动推送请求上下文:
-
current_app
和request
的区别?current_app
是应用上下文的代理(绑定到 Flask 应用实例),而request
是请求上下文的代理(绑定到当前 HTTP 请求)。
通过理解这些概念,可以更高效地开发 Flask 应用,并避免常见的上下文相关错误(如在非请求环境中访问 request
)。