返回介绍

理解 Context

发布于 2025-04-20 18:52:14 字数 6836 浏览 0 评论 0 收藏

Context(上下文)是 Flask 里面非常好的设计,使用 Flask 需要非常理解应用上下文和请求上下文这两个概念。在开始正文之前我们先了解一些必要的知识。

本地线程

本地线程(Thread Local)希望不同的线程对于内容的修改只在线程内发挥作用,线程之间互不影响(local.py):

import threading
mydata=threading.local()
mydata.number=42
print mydata.number
log=[]


def f():
    mydata.number=11
    log.append(mydata.number)


thread=threading.Thread(target=f)
thread.start()
thread.join()
print log
print mydata.number
> python chapter3/local.py
42
[11] #在线程内变成了 mydata.number 其他的值
42 #但是没有影响到开始设置的值

本地线程实现的原理就是:在 threading.current_thread().__dict__里添加一个包含对象 mydata 的 id 值的 key,来保存不同线程的状态。

Werkzeug 的 Local

Werkzeug 自己实现了本地线程。werkzeug.local.Local 和 threading.local 的区别如下:

  • Werkzeug 使用了自定义的__storage__保存不同线程下的状态。
  • Werkzeug 提供了释放本地线程的 release_local 方法。
  • Werkzeug 使用如下方法得到 get_ident 函数,用来获得线程/协程标识符。
try:
    from greenlet import getcurrent as get_ident
except ImportError:
    try:
        from thread import get_ident
    except ImportError:
        from_thread import get_ident

假如已经安装了 Greenlet,会优先选择 Greenlet,否则使用系统线程。Greenlet 是以 C 扩展模块形式接入 Python 的轻量级协程,它运行在操作系统进程的内部,但是会被协作式地调度。Werkzeug 还实现了两种数据结构。

  • LocalStack:基于 werkzeug.local.Local 实现的栈结构,可以将对象推入、弹出,也可以快速拿到栈顶对象。
  • LocalProxy:作用和名字一样,是标准的代理模式。构造此结构时接受一个可以调用的参数(一般是函数),这个函数执行后就是通过 LocalStack 实例化的栈的栈顶对象。对于 LocalProxy 对象的操作实际上都会转发到这个栈顶对象(也就是一个 Thread Local 对象)上面。

flask.request

先看一个极简的示例:

from flask import Flask, request
app=Flask(__name__)

@app.route('/people/')
def people():
    name=request.args.get('name')

仔细想一下,这里先引用了 flask.request,但是直到用户访问/people/的时候才通过 re-quest.args.get('name') 获得请求的参数值。试想,引用的时候还没有发生这个请求,那么请求上下文是怎么获得的呢?

flask.request 就是一个获取名为_request_ctx_stack 的栈顶对象的 LocalProxy 实例:

from functools import partial
from werkzeug.local import LocalProxy


def_lookup_req_object(name):
    top=_request_ctx_stack.top
    if top is None:#所以注意,一定要先把请求推入堆栈再调用
        raise RuntimeError('working outside of request context')
    return getattr(top, name)


request=LocalProxy(partial(_lookup_req_object, 'request'))

上述逻辑能正常使用,是因为其流程是这样的:

  • 用户访问产生请求。
  • 在发生请求的过程中向_request_ctx_stack 推入这个请求上下文的对象,它会变成栈顶。request 就会成为这个请求上下文,也就包含了这次请求相关的信息和数据。
  • 在视图函数中使用 request 就可以使用 request.args.get('name') 了。

设想不使用 LocalStack 和 LocalProxy 的话,要想让视图函数访问到请求对象,就只能将其作为参数,一步步传入视图函数中。这样做的缺点是会让每个视图函数都增加一个 request 参数,而 Flask 巧妙地使用上下文把某些对象变为全局可访问(实际上是特定环境的局部对象的代理),每个线程看到的上下文对象却是不同的,这样就巧妙地解决了这个问题。

使用上下文

应用上下文的典型应用场景是缓存一些在发生请求之前要使用到的资源,比如生成数据库连接和缓存一些对象;请求上下文发生在 HTTP 请求开始,WSGI Server 调用 Flask.__call__() 之后。应用上下文并不是应用启动之后生成的唯一上下文,我们看一下它们的关系:

class RequestContext(object):
    self._implicit_app_ctx_stack=[]

    def push(self):
        # some stuff
        app_ctx=_app_ctx_stack.top
        if app_ctx is None or app_ctx.app !=self.app:
            app_ctx=self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
    # some other stuff

也就是说应用上下文是被动的在推入请求上下文的过程中生成的,在请求结束的时候,也会把请求上下文弹出:

class RequestContext(object):
    def pop(self, exc=_sentinel):
        app_ctx=self._implicit_app_ctx_stack.pop()
        try:
            # some stuff
        finally:
            # some other stuff
            if app_ctx is not None:
                app_ctx.pop(exc)

也就是说,事实上在 Web 应用环境中,请求上下文和应用文是一一对应的。请求上下文和应用上下文都是本地线程的,那么区分它们有什么意义呢?

  • 使用本章稍后介绍的中间件 DispatcherMiddleware,支持多个 app 共存。就像 request 一样,在多 app 情况下之前也要保证 app 之间的隔离。
  • 非 Web 模式下。比如进行测试,一个应用上下文可以有多个请求上下文。但是不能执行 pop 方法,或者使用 with 语句(__exit__中会自动执行 pop 方法)。

Flask 中有 4 个上下文变量。

1.flask.current_app:应用上下文。它是当前 app 实例对象。

2.flask.g:应用上下文。处理请求时用作临时存储的对象。

3.flask.request:请求上下文。它封装了客户端发出的 HTTP 请求中的内容。

4.flask.session:请求上下文。它存储了用户会话。

其中最常见的就是 flask.g 和 flask.request,在前面已经使用过 flask.request,现在对 3.3 节的 app_with_sqlalchemy.py 扩充,添加上下文的钩子等功能(app.py):

import random

from flask import Flask, g, render_template
from ext import db
from users import User
app=Flask(__name__, template_folder='../../templates')
app.config.from_object('config')
db.init_app(app)


def get_current_user():
    users=User.query.all()
    return random.choice(users)


@app.before_first_request
def setup():
    db.drop_all()
    db.create_all()
    fake_users=[
        User('xiaoming', 'xiaoming@dongwm.com'),
        User('dongwweiming', 'dongwm@dongwm.com'),
        User('admin', 'admin@dongwm.com')
    ]
    db.session.add_all(fake_users)
    db.session.commit()


@app.before_request
def before_request():
    g.user=get_current_user()


@app.teardown_appcontext
def teardown(exc=None):
    if exc is None:
        db.session.commit()
    else:
        db.session.rollback()
    db.session.remove()
        g.user=None


@app.context_processor
def template_extras():
    return{'enumerate':enumerate, 'current_user':g.user}


@app.errorhandler(404)
def page_not_found(error):
    return 'This page does not exist', 404


@app.template_filter('capitalize')
def reverse_filter(s):
    return s.capitalize()


@app.route('/users')
def user_view():
    users=User.query.all()
    return render_template('chapter3/section4/user.html', users=users)

if__name__=='__main__':
    app.run(host='0.0.0.0', port=9000)

例子中有 6 个钩子装饰器,被装饰的函数会注册到 app 中,它们将在不同的阶段执行。

  • before_first_request:在处理第一次请求之前执行。
  • before_request:在每次请求前执行。
  • teardown_appcontext:不管是否有异常,注册的函数都会在每次请求之后执行。
  • context_processor:上下文处理的装饰器,返回的字典中的键可以在上下文中使用。
  • template_filter:在使用 Jinja2 模板的时候可以方便地注册过滤器。
  • errorhandler:errorhandler 接收状态码,可以自定义返回这种状态码的响应的处理方法。

我们先详细地分析一下:

  • setup 函数常用来初始化数据,尤其是开发环境下,每次启动应用都会先删掉之前创建的假数据再重新创建。
  • 之前说 flask.g 是一个应用上下文,通常放在 before_request 中对它进行数据的填充。
  • 一般来说,对资源的操作有一个 get_X 和一个 teardown_X 对应,多个资源的使用可以使用同一个 teardown 函数。teardown 通常是做一些环境的清理工作,提交未提交的操作请求等,在本地开发环境和测试时意义较大。
  • 由于 Jinja2 模板的限制,并不能直接使用 enumerate 这样的 Python 自带的函数(虽然 Jinja2 支持在 for 循环中使用 loop.index 和 loop.index0,但是无法满足全部需要),可以使用 context_processor 把要用到的上下文资源传进去。这样在模板中就可以直接使用 enumerate 和 current_user 了。
  • errorhandler 除了可自定义对不同错误状态码的返回内容,还可以传入自定义的异常对象。
  • 虽然 Jinja2 支持了非常多的过滤器,但还是无法满足我们的全部需要。注册一个新的过滤器很方便,这个例子中注册了一个叫作 capitalize 的过滤器,在模板中可以这样使用“{{user.name|capitalize}}”。

使用 LocalProxy 替代 g

g 也是一个被 LocalProxy 包装的对象,而且还需要借助 before_request 这个钩子,可不可以更简化呢?现在我们对 app.py 稍加修改(app_with_local_proxy.py),创建一个全局可访问的 current_user 来直接使用:

from werkzeug.local import LocalProxy
current_user=LocalProxy(get_current_user)

现在就可以去掉 before_request,直接使用 current_user 了:

@app.context_processor
def template_extras():
    return{'enumerate':enumerate, 'current_user':current_user}

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。