关于我
一个有思想的程序猿, 终身学习实践者, 目前在一个创业团队任 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/
来源: https://www.cnblogs.com/angrycode/p/11456932.html