Loading

hellohelp

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.小知识点

  1. 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__去实现响应)。
  • 它提供了自动重载、调试器等有用的功能:如参数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的要求,接收特定的参数和返回特定的值。 值得关注的是内部的requestresponse以及请求上下文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__ 方法,它接受两个参数 nameage,并在创建 Dog 的实例时将这些值赋给实例的 nameage 属性。这样每个 Dog 实例都会有自己的 nameage 属性。

  • 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_funcrequired_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对象。 RuleMap后续会展开分析。

  • 这里会将视图函数通过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好像和路由有着什么关系,这个后续会对比RuleMapendpoint三者的关系和作用。

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类匹配endpointendpoint匹配视图函数。这是在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)
    .......

它需要ruleurl 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_matcherStateMachineMatcher的实例。调用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时候的全部过程了。但是现在还不是很明确,RuleMap的关系,要结合一下请求进来的时候发生了什么。

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函数最后会调用StateMachineMatchermatch函数,返回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

所以,endpointRuleMap三者关系可以这么理解:

  • 程序启动的时候,通过add_url_rule做了这几件事:
    • 初始化Rule对象,这里Rule实例保存了endpoint
    • Rule进行compile,存入StateMachineMatcherStateMachineMatcher存在Map
  • 请求过来的时候:
    • 通过请求上下文获取到请求信息,包括url path
    • Map根据url path找到Rule
    • 通过Rule对象的endpoint去匹配对应的视图函数

所以三者的关系是:通过Map找到Rule,通过Rule找到endpoint,通过endpoint找到定义好的视图函数。

✅总结

本篇主要进行了flask的路由分析,从最开始的@app.route到最后请求的过程。其中涉及到的其他的内容,比如上下文,具体如何解析url和请求参数,并不详细展开。上下文的内容会在下一篇目开始分析。flask路由系统的其他细节,包括解析url,匹配Rule,正则转换器等等会在系列最后进行补充。

flask的路由复杂的地方在于Rule类和Map类的关系。从分析结果来看,endpointRule的匹配是由flask完成的,而RuleMap类的匹配是由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提供的全局变量。requestrequest_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实例,并返回一个tokentoken可以通过reset来重置,让Contextvar设置成上一个值。get获取值。

因此,self._cv_tokens.append((_cv_request.set(self), app_ctx))的作用是把RequestContext实例存入_cv_request,将返回的tokenapp_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

这是pushpop的过程。

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_ctxrequest都是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

这是很显然的,因为requestrequest_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,代理了RequestContextrequests对象。可以看到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
)

如果localContextvar,则用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,以及sessiong。 实现方式都是差不多的。后续遇到再详细展开。

flask通过Contextvar(旧版本是通过threading.local)来实现线程/协程安全,让不同的请求互相隔离,又通过全局变量的方式简化上下文管理,不需要一直传参。

那么现在一次请求的过程就是这样的:

  • 请求来了,实例化RequestContext,实例对象push,入栈。
  • push中,将RequestContext对象存入Contextvar。
  • 派发请求full_dispatch_requestdispatch_request等等,再函数内部,全局变量,获取请求上下文,还有请求前,请求后的各种处理。这里就体现了flask上下文管理的精妙,不需要传递请求上下文了。
  • 请求处理结束,通过popContextvar中重置数据。

过程差不多这样:

图片.png


总结

1.falsk实现流程:

  • 前期准备:
    • 1.__init__方法对静态文件、模板、主机等进行了存放地址和值及路径的默认定义
    • app.add_url_rule函数将路由和视图函数绑定后放入Rule类中,Rule进行compile,存入StateMachineMatcherStateMachineMatcher存在Map中。同时将endpoint作为key视图函数作为value储存在app.add_url_rule函数的view_functions这个字典中。
  • 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请求响应流程图

sequenceDiagram participant Web Server participant Flask App participant RequestContext participant Dispatcher participant View Function participant Response Web Server->>Flask App: 调用 __call__(environ, start_response) activate Flask App Flask App->>Flask App: wsgi_app(environ, start_response) Note right of Flask App: 创建请求上下文 Flask App->>RequestContext: 创建(ctx = request_context(environ)) activate RequestContext RequestContext->>RequestContext: push() activate RequestContext RequestContext-->>Flask App: 上下文激活完成 deactivate RequestContext Note right of Flask App: 请求处理阶段 Flask App->>Flask App: full_dispatch_request() activate Flask App Flask App->>Flask App: preprocess_request()<br>执行 @before_request 钩子 alt 有提前返回? Flask App-->>Response: 创建响应 else Flask App->>Dispatcher: dispatch_request() activate Dispatcher Dispatcher->>View Function: 路由匹配并调用视图函数 activate View Function View Function-->>Dispatcher: 返回结果 deactivate View Function Dispatcher-->>Flask App: 视图结果 deactivate Dispatcher Flask App->>Flask App: make_response()<br>包装响应对象 end Flask App->>Flask App: process_response(response)<br>执行 @after_request 钩子 Flask App-->>Response: 最终响应对象 deactivate Flask App Note right of Flask App: 清理阶段 Flask App->>RequestContext: auto_pop(exc) activate RequestContext RequestContext->>RequestContext: 执行 @teardown_request 钩子<br>清理上下文 RequestContext-->>Flask App: 完成清理 deactivate RequestContext Flask App->>Web Server: response(environ, start_response) deactivate Flask App Web Server->>Client: 返回HTTP响应

流程说明与关键方法解析

  1. 入口点 (__call__):

    • Web服务器调用Flask.__call__(environ, start_response)
    • 实际转发到wsgi_app()方法处理
  2. 上下文创建 (request_context()):

    ctx = self.request_context(environ)  # 创建RequestContext对象
    ctx.push()  # 激活上下文
    
  3. 核心处理 (full_dispatch_request()):

    • preprocess_request(): 执行所有@app.before_request注册的函数
    • dispatch_request(): 核心路由分发方法
      # 伪代码
      rule = request.url_rule  # 获取匹配的路由规则
      return self.view_functions[rule.endpoint](**request.view_args)
      
  4. 响应生成 (make_response()):

    • 将视图返回值转换为标准Response对象
    • 支持多种返回类型:字符串、元组、Response对象等
  5. 后处理 (process_response()):

    • 执行所有@app.after_request注册的函数
    • 逆序执行(最后注册的函数最先执行)
  6. 上下文清理 (auto_pop()):

    finally:
        ctx.auto_pop(exc)  # 确保始终执行
    
    • 执行@app.teardown_request注册的函数
    • 弹出请求上下文(_request_ctx_stack.pop()
  7. 响应返回:

    • 调用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应用到返回响应的完整生命周期,包含了所有关键的方法调用和钩子函数的执行时机。

RequestContextrequest_ctxrequestLocalProxy区别联系

在 Flask 中,RequestContextrequest_ctxrequestLocalProxy 是紧密相关的概念,它们共同构成了 Flask 的上下文管理机制。以下是它们的定义、关系和区别:


1. RequestContext(请求上下文)

  • 定义

    • RequestContext 是 Flask 的 请求上下文对象,封装了与当前 HTTP 请求相关的所有数据(如 request 对象、sessiong 等)。
    • 每个 HTTP 请求都会创建一个新的 RequestContext 实例,并将其推入 Flask 的请求上下文栈(_request_ctx_stack)中。
  • 作用

    • 提供请求级别的数据隔离,确保每个请求的数据独立(例如不同用户的请求不会互相干扰)。
    • 在请求处理过程中,Flask 通过 RequestContext 访问 requestsessiong 等对象。
  • 生命周期

    • 当请求到达时自动创建,请求结束后自动销毁。
  • 示例

    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(如 requestsession)通过 request_ctx 栈动态获取当前请求的上下文。
  • 关键代码

    # Flask 源码中的 _request_ctx_stack(简化版)
    from werkzeug.local import LocalStack
    _request_ctx_stack = LocalStack()
    

3. request(请求对象)

  • 定义

    • request 是 Flask 提供的 全局对象,用于访问当前请求的数据(如 request.methodrequest.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 提供的一个 代理类,用于在全局范围内安全地访问线程局部数据(如 requestcurrent_app)。
    • 它的核心作用是:动态绑定当前线程的上下文对象,避免显式传递上下文。
  • 作用

    • 允许开发者通过全局变量(如 requestcurrent_app)访问当前请求或应用的上下文数据。
    • 通过 LocalProxy,Flask 实现了上下文感知的全局对象,确保线程安全。
  • 实现原理

    • LocalProxy 会调用 __getattr__ 方法,从 request_ctxapp_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 封装请求数据(如 requestsession),管理请求上下文的生命周期。 请求开始时创建,请求结束时销毁。
request_ctx Flask 内部的请求上下文栈(_request_ctx_stack),管理多个 RequestContext 应用启动时初始化,持续存在。
request 通过 LocalProxy 动态访问当前请求的数据(如 request.method)。 请求开始时可用,请求结束时失效。
LocalProxy 代理类,用于全局访问当前线程的上下文对象(如 requestcurrent_app)。 应用启动时初始化,持续存在。

6. 典型流程

  1. 请求到达
    • Flask 创建 RequestContext,并将其推入 request_ctx 栈。
  2. 处理请求
    • 视图函数中通过 requestLocalProxy)访问请求数据。
    • LocalProxyrequest_ctx 栈中获取当前的 RequestContext.request
  3. 请求结束
    • RequestContextrequest_ctx 栈中弹出并销毁。

7. 常见问题

  • 为什么 request 是全局的?

    • requestLocalProxy,通过线程局部存储(threading.local)实现线程隔离,确保每个请求的 request 独立。
  • 如何在非请求环境中使用 request

    • 必须手动推送请求上下文:
      with app.test_request_context('/path'):
          print(request.path)  # /path
      
  • current_apprequest 的区别?

    • current_app 是应用上下文的代理(绑定到 Flask 应用实例),而 request 是请求上下文的代理(绑定到当前 HTTP 请求)。

通过理解这些概念,可以更高效地开发 Flask 应用,并避免常见的上下文相关错误(如在非请求环境中访问 request)。

posted @ 2025-07-15 13:07  HordorZzz  阅读(10)  评论(0)    收藏  举报