在 Python 的 Web 开发框架 flask 中有这样的四个全局变量,我们经常会使用它们来存取数据,在处理请求过程中它们是非常方便的,因为它们的实现方法是相同的,所以这里我们重点探究一下 request 的使用,其它的都是类似的。下面是 flask.globals 的代码(我删减了一部分):
python">"""
删减了一部分代码,这里的关注重点是 request 变量。
"""
# -*- coding: utf-8 -*-
"""
flask.globals
"""
from functools import partial
from werkzeug.local import LocalStack, LocalProxy
_request_ctx_err_msg = '''\
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.\
'''
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
# 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'))
从这里我们可以看到 current_app
, request
, session
, g
的定义是类似的,所以我们这里只探究 request 即可。这几个变量都是全局变量,但是我们在使用时并不需要关心这点,因为框架已经帮我们做好了封装。每个线程或者协程只能访问自己特定的资源,不同线程或线程之间不会互相冲突。
1.1 Local 类
我们直接跳转到 LocalProxy
所在的文件,这个文件里面定义了 Local
, LocalStack
, LocalProxy
类。
python">"""
Local 类是 flask 中实现类似于 threadlocal 存储的类,只不过 threadlocal 是绑定了线程,
而它可以绑定线程、协程等,如果理解了前者,对于后者也会很容易掌握的。
简单来说,它是一个全局字典,每个线程或者协程会在其中存储它们各自的数据,数据的键是它们
的标识符(唯一性),这样存取数据就不会冲突了,从用户的角度来看一切都是透明的,他们只是从
它里面存取数据,而不用考虑多线程数据的冲突。
它只有两个属性:
- __storage__
- __ident_func__
`__storage__` 是用来存储数据的,`__ident_func__` 是线程或者协程的唯一标识符,用于区分是哪一个具体的线程或者协程。
关于它的具体实现,会使用一些高级的 Python 语法,不过对于我们用户来说知道它的作用即可,具体的实现就超出
理解的范围了,学习一定要有取舍。
这里还要关注一下 `__call__` 方法,它会传入当前实例和一个 proxy 参数,创建一个 LocalProxy 实例,这里这个 proxy 是代理的实例的名字。
"""
class Local(object):
__slots__ = ("__storage__", "__ident_func__")
def __init__(self):
object.__setattr__(self, "__storage__", {})
object.__setattr__(self, "__ident_func__", get_ident)
def __iter__(self):
return iter(self.__storage__.items())
def __call__(self, proxy):
"""Create a proxy for a name."""
return LocalProxy(self, proxy)
def __release_local__(self):
self.__storage__.pop(self.__ident_func__(), None)
def __getattr__(self, name):
try:
return self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value):
ident = self.__ident_func__()
storage = self.__storage__
try:
storage[ident][name] = value
except KeyError:
storage[ident] = {name: value}
def __delattr__(self, name):
try:
del self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
1.2 LocalStack 类
python">"""
这个类和 Local 类很相似,不过它是数据是存储在一个栈中的,也就是说值的部分是一个列表,
按照栈的方式进行存取数据。Flask 中也是直接用的 LocalStack 类,而不是 Local 类。
"""
class LocalStack(object):
"""This class works similar to a :class:`Local` but keeps a stack
of objects instead. This is best explained with an example::
>>> ls = LocalStack()
>>> ls.push(42)
>>> ls.top
42
>>> ls.push(23)
>>> ls.top
23
>>> ls.pop()
23
>>> ls.top
42
They can be force released by using a :class:`LocalManager` or with
the :func:`release_local` function but the correct way is to pop the
item from the stack after using. When the stack is empty it will
no longer be bound to the current context (and as such released).
By calling the stack without arguments it returns a proxy that resolves to
the topmost item on the stack.
.. versionadded:: 0.6.1
"""
def __init__(self):
self._local = Local()
def __release_local__(self):
self._local.__release_local__()
def _get__ident_func__(self):
return self._local.__ident_func__
def _set__ident_func__(self, value):
object.__setattr__(self._local, "__ident_func__", value)
__ident_func__ = property(_get__ident_func__, _set__ident_func__)
del _get__ident_func__, _set__ident_func__
def __call__(self):
def _lookup():
rv = self.top
if rv is None:
raise RuntimeError("object unbound")
return rv
return LocalProxy(_lookup)
def push(self, obj):
"""Pushes a new item to the stack"""
rv = getattr(self._local, "stack", None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv
def pop(self):
"""Removes the topmost item from the stack, will return the
old value or `None` if the stack was already empty.
"""
stack = getattr(self._local, "stack", None)
if stack is None:
return None
elif len(stack) == 1:
release_local(self._local)
return stack[-1]
else:
return stack.pop()
@property
def top(self):
"""The topmost item on the stack. If the stack is empty,
`None` is returned.
"""
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
1.3 LocalProxy 类
python">@implements_bool
class LocalProxy(object):
"""Acts as a proxy for a werkzeug local. Forwards all operations to
a proxied object. The only operations not supported for forwarding
are right handed operands and any kind of assignment.
Example usage::
from werkzeug.local import Local
l = Local()
# these are proxies
request = l('request')
user = l('user')
from werkzeug.local import LocalStack
_response_local = LocalStack()
# this is a proxy
response = _response_local()
Whenever something is bound to l.user / l.request the proxy objects
will forward all operations. If no object is bound a :exc:`RuntimeError`
will be raised.
To create proxies to :class:`Local` or :class:`LocalStack` objects,
call the object as shown above. If you want to have a proxy to an
object looked up by a function, you can (as of Werkzeug 0.6.1) pass
a function to the :class:`LocalProxy` constructor::
session = LocalProxy(lambda: get_current_request().session)
"""
__slots__ = ("__local", "__dict__", "__name__", "__wrapped__")
def __init__(self, local, name=None):
object.__setattr__(self, "_LocalProxy__local", local)
object.__setattr__(self, "__name__", name)
if callable(local) and not hasattr(local, "__release_local__"):
# "local" is a callable that is not an instance of Local or
# LocalManager: mark it as a wrapped function.
object.__setattr__(self, "__wrapped__", local)
1.4 总结:Local, LocalStack, LocalProxy 的简单使用
为了便于理解它们几个的作用,这里提供一个示例用于演示:
python">from werkzeug import LocalStack, LocalProxy
class Request:
def __init__(self, method: str, url: str, query: str):
self.method = method
self.url = url
self.query = query
def __repr__(self):
return f'{">"*30}\nmethod: {self.method}\nurl: {self.url}\nquery: {self.query}\n{"<"*30}\n'
class RequestContext:
def __init__(self, request): # request 是 RequestContext 的属性
self.request = request
def _lookup_req_object(name):
top = request_stack.top
if top is None:
raise RuntimeError("Working outside of request context.")
return getattr(top, name)
request_stack = LocalStack()
request = LocalProxy(lambda: _lookup_req_object('request')) # 源码里面是偏函数,这里我用lambda 代替
if __name__ == "__main__":
# 原始对象测试
print("原始对象测试:")
req = Request("GET", "/v1/user", {"name": "peter"})
req_ctx = RequestContext(req)
print("method: ", req.method)
print("url: ", req.url)
print("query: ", req.query)
print("original request", req)
print('-'*30)
print("代理对象测试:") # 将 RequestContext 对象推送 LocalStack 实例中,
request_stack.push(req_ctx) # 然后我们直接访问 request 对象(代理对象)
print("method: ", request.method)
print("url: ", request.url)
print("query: ", request.query)
print("request: ", request)
print("proxied object: \n", request._get_current_object())
target_func = lambda r: print(r.method)
# 在当前线程中访问全局 request 对象
print("*"*30)
target_func(request)
print("*"*30)
# 尝试在另一个线程中访问全局 request 对象,guess what will happen?
from threading import Thread
t = Thread(target=target_func, args=(request,))
t.start()
t.join()
Copy and Paste the demo code, Run it, try it out!
当尝试在当前线程之外访问 request 中的内容,将会导致一个运行时异常,因为 request 是绑定到特定线程或者协程的,如果使用一个新的线程或者协程,它在全局 request_stack 中是没有数据的,因此会异常。所以一定要注意,不要在变量的上下文范围之外去读写数据,除非手动推入数据。