关于我
一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。
Github: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
代表了用户会话信息。