Python Web Flask源码解读(四)——全局变量

系统 1190 0

关于我
一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。
Github:https://github.com/hylinux1024
微信公众号:终身开发者(angrycode)

Flask 中全局变量有 current_app request g session 。不过需要注意的是虽然标题是写着全局变量,但实际上这些变量都跟当前请求的上下文环境有关,下面一起来看看。

current_app 是当前激活程序的应用实例; request 是请求对象,封装了客户端发出的 HTTP 请求中的内容; g 是处理请求时用作临时存储的对象,每次请求都会重设这个变量; session 是用户会话,用于存储请求之间需要保存的值,它是一个字典。

0x00 current_app

应用程序上下文可用于跟踪一个请求过程中的应用程序实例。可以像使用全局变量一样直接导入就可以使用 (注意这个变量并不是全局变量)
Flask 实例有许多属性,例如 config 可以 Flask 进行配置。

一般在创建 Flask 实例时

          
            from flask import Flask
app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
...
          
        

通常不会直接导入 app 这个变量,而是使用通过导入 current_app 这个应用上下文实例代理

          
            from flask import current_app
          
        
current_app 的生命周期

Flask 应用在处理客户端请求( request )时,会在当前处理请求的线程中推送( push )一个上下文实例和请求实例( request ),请求结束时就会弹出( pop )请求实例和上下文实例,所以 current_app request 是具有相同的生命周期的,且是绑定在当前处理请求的线程上的。

如果一个没有推送上下文实例就直接使用 current_app ,会报错

          
            RuntimeError: Working outside of application context.

This typically means that you attempted to use functionality that
needed to interface with the current application object in some way.
To solve this, set up an application context with app.app_context().
          
        

如果要直接使用 current_app 就要手动推送( push )应用上下文实例,从上面的错误信息可以知道,可以使用 with 语句,帮助我们 push 一个上下文实例

          
            def create_app():
    app = Flask(__name__)

    with app.app_context():
        init_db()

    return app
          
        

需要注意的是 current_app 是“线程”本地变量,所以 current_app 需要在视图函数或命令行函数中使用,否则也会报错。

要理解这一点就要对服务器程序工作机制有所了解。一般服务器程序都是多线程程序,它会维护一个线程池,对于每个请求,服务器会从线程池中获取一个线程用于处理这个客户端的请求,而应用的 current_app request 等变量是“线程”本地变量,它们是绑定在“线程”中的(相当于线程自己独立的内存空间),所以也在线程环境下才能够使用。

Flask 中是否也是通过线程本地变量来实现的呢? 这个问题我们在后面的 工作原理 一节会给出答案。

0x01 g

若要在应用上下文中存储数据, Flask 提供了 g 这个变量为我们达到这个目的。 g 其实就是 global 的缩写,它的生命周期是跟应用上下文的生命周期是一样的。

例如在一次请求中会多次查询数据库,可以把这个数据库连接实例保存在当次请求的 g 变量中,在应用上下文生命周期结束关闭连接。

          
            from flask import g

def get_db():
    if 'db' not in g:
        g.db = connect_to_database()

    return g.db

@app.teardown_appcontext
def teardown_db():
    db = g.pop('db', None)

    if db is not None:
        db.close()
          
        

0x02 request

request 封装了客户端的 HTTP 请求,它也是一个线程本地变量。

没有把这个变量放在处理 api 请求的函数中,而是通过线程本地变量进行封装,极大地方便使用,以及也使得代码更加简洁。

request 的生命周期是跟 current_app 是一样的,从请求开始时创建到请求结束销毁。同样地 Flask 在处理请求时就会 push 一个 request 和应用上下文的代理实例,然后才可以使用。如果没有 push 就使用就会报错

          
            RuntimeError: Working outside of request context.

This typically means that you attempted to use functionality that
needed an active HTTP request. Consult the documentation on testing
for information about how to avoid this problem.
          
        

通常这个错误在测试代码中会经常遇到,如果需要在单元测试中使用 request ,可以使用 test_client 或者在 with 语句中使用 test_requet_context() 进行模拟

          
            def generate_report(year):
    format = request.args.get('format')
    ...

with app.test_request_context(
        '/make_report/2017', data={'format': 'short'}):
    generate_report()
          
        

0x03 session

前面讲到如果在一个请求期间共享数据,可以使用 g 变量,但如果要在不同的请求( request )之间共享数据,那就需要使用 session ,这是一个私有存储的字典类型。可以像操作字典一样操作 session
session 是用户会话,可以保存请求之间的数据。例如在使用 login 接口进行用户登录之后,把用户登录信息保存在 session 中,然后访问其它接口时就可以通过 session 获取到用户的登录信息。

          
            
@app.route('/login')
def login():
    # 省略登录操作
    ...
    session['user_id']=userinfo
    
@app.route('/show')
def showuser():
    # 省略其它操作
    ...
    userid = request.args.get('user_id')
    userinfo = session.get(userid)
          
        

0x04 工作原理

我们知道 Flask 在处理一个请求时, wsgi_app() 这个方法会被执行。而在 Flask 的源码内部 request current_app 是通过 _request_ctx_stack 这个栈结构来保存的,分别为

          
            # context locals
_request_ctx_stack = LocalStack()
current_app = LocalProxy(lambda: _request_ctx_stack.top.app)
request = LocalProxy(lambda: _request_ctx_stack.top.request)
session = LocalProxy(lambda: _request_ctx_stack.top.session)
g = LocalProxy(lambda: _request_ctx_stack.top.g)
          
        

需要注意 最新的版本源码会有些不同 request current_app 分别是有两个栈结构来存储: _request_ctx_stack _app_ctx_stack 。但新旧代码思路是差不多的。

最新的源码里,全局变量的定义

          
            # context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, "request"))
session = LocalProxy(partial(_lookup_req_object, "session"))
g = LocalProxy(partial(_lookup_app_object, "g"))
          
        

其中 _find_app _lookup_app_object 方法是这样定义的

          
            def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app
    
def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)


def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)
          
        

可以看到 current_app g LocalProxy 通过 _app_ctx_stack.top 进行封装的。 request session _request_ctx_stack 的封装。 LocalProxy werkzeug 库中 local 对象的代理。 LocalStack 顾名思义是一个实现了栈的数据结构。
前面提到全局变量是跟线程绑定的,每个线程都有一个独立的内存空间,在 A 线程设置的变量,在 B 线程是无法获取的,只有在 A 线程中才能获取到这个变量。这个在 Python 的标准库有 thread locals 的概念。
然而在 Python 中除了线程外还有进程和协程可以处理并发程序的技术。所以为了解决这个问题 Flask 的依赖库 werkzeug 就实现了自己的本地变量 werkzeug.local 。它的工作机制跟线程本地变量( thread locals )是类似的。

要使用 werkzug.local

          
            from werkzeug.local import Local, LocalManager

local = Local()
local_manager = LocalManager([local])

def application(environ, start_response):
    local.request = request = Request(environ)
    ...

application = local_manager.make_middleware(application)
          
        

application(environ,start_response) 方法中就把封装了请求信息的 request 变量绑定到了local变量中。然后在相同的上下文下例如在一次请求期间,就可以通过 local.request 来获取到这个请求对应的 request 信息。

同时还可以看到 LocalManager 这个类,它是本地变量管理器,它可以确保在请求结束之后及时的清理本地变量信息。

在源码中对 LocalManager 是这样注释的

Local objects cannot manage themselves. For that you need a local
manager. You can pass a local manager multiple locals or add them later
by appending them to manager.locals . Every time the manager cleans up,
it will clean up all the data left in the locals for this context.

Local 不能自我管理,需要借助 LocalManager 这个管家来实现请求结束后的清理工作。

0x05 总结

current_app g request session Flask 中常见4个全局变量。 current_app 是当前 Flask 服务运行的实例, g 用于在应用上下文期间保存数据的变量, request 封装了客户端的请求信息, session 代表了用户会话信息。

0x06 学习资料

  • https://werkzeug.palletsprojects.com/en/0.15.x/local/

更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论