Werkzeug(Flask)之Local、LocalStack和LocalProxy

在我们使用Flask以及Werkzeug框架的过程中,经常会遇到如下三个概念:Local、LocalStack和LocalProxy。尤其在学习Flask的Request Context和App Context的过程中,这几个概念出现的更加频繁,另外很多Flask插件都会使用这三个概念对应的技术。那么这三个东西到底是什么?我们为什么需要它们?以及如何使用呢?本篇文章主要就是来解答这些问题。

Local

这部分我们重点介绍Local概念,主要分为以下几个部分:

  • 为什么需要Local?
  • Local的使用
  • Local的实现

为什么需要Local?

在Python的标准库中提供了thread local对象用于存储thread-safethread-specific的数据,通过这种方式存储的数据只在本线程中有效,而对于其它线程则不可见。正是基于这样的特性,我们可以把针对线程全局的数据存储进thread local对象,举个简单的例子

1
2
3
4
5
>>from threading import local
>>thread_local_data = local()
>>thread_local_data.user_name="Jim"
>>thread_local_data.user_name
'Jim'

使用thread local对象虽然可以基于线程存储全局变量,但是在Web应用中可能会存在如下问题:

  1. 有些应用使用的是greenlet协程,这种情况下无法保证协程之间数据的隔离,因为不同的协程可以在同一个线程当中。
  2. 即使使用的是线程,WSGI应用也无法保证每个http请求使用的都是不同的线程,因为后一个http请求可能使用的是之前的http请求的线程,这样的话存储于thread local中的数据可能是之前残留的数据。

为了解决上述问题,Werkzeug开发了自己的local对象,这也是为什么我们需要Werkzeug的local对象

Local的使用

先举一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
from werkzeug.local import Local, LocalManager

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

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

# make_middleware会确保当request结束时,所有存储于local中的对象的reference被清除
application = local_manager.make_middleware(application)

  • 首先Local对象需要通过LocalManager来管理,初次生成LocalManager对象需要传一个list类型的参数,list中是Local对象,当有新的Local对象时,可以通过local_manager.locals.append()来添加。而当LocalManager对象清理的时候会将所有存储于locals中的当前context的数据都清理掉
  • 上例中当local.request被赋值之后,其可以在当前context中作为全局数据使用
  • 所谓当前context(the same context)意味着是在同一个greenlet(如果有)中,也就肯定是在同一个线程当中

那么Werkzeug的Local对象是如何实现这种在相同的context环境下保证数据的全局性和隔离性的呢?

Local的实现

我们先来看下源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 在有greenlet的情况下,get_indent实际获取的是greenlet的id,而没有greenlet的情况下获取的是thread id
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident

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())

# 当调用Local对象时,返回对应的LocalProxy
def __call__(self, proxy):
"""Create a proxy for a name."""
return LocalProxy(self, proxy)

# Local类中特有的method,用于清空greenlet id或线程id对应的dict数据
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)

  • 这段代码实际是对__storage__ dict的封装,而这个dict中的key使用的就是get_indent函数获取的id(当有greenlet时使用greenlet id,没有则使用thread id)
  • __storage__ dict中的value也是一个dict,这个dict就是该greenlet(或者线程)对应的local存储空间
  • 通过重新实现__getattr__, __setattr__等魔术方法,我们在greenlet或者线程中使用local对象时,实际会自动获取greenlet id(或者线程id),从而获取到对应的dict存储空间,再通过name key就可以获取到真正的存储的对象
  • 当我们需要释放local数据的内存时,可以通过调用release_local()函数来释放当前context的local数据,如下
    1
    2
    3
    4
    5
    >>> loc = Local()
    >>> loc.foo = 42
    >>> release_local(loc) # release_local实际调用local对象的__release_local__ method
    >>> hasattr(loc, 'foo')
    False

LocalStack

LocalStack与Local对象类似,都是可以基于Greenlet协程或者线程进行全局存储的存储空间(实际LocalStack是对Local进行了二次封装),区别在于其数据结构是栈的形式。示例如下:

1
2
3
4
5
6
7
8
9
10
11
>>> ls = LocalStack()
>>> ls.push(42)
>>> ls.top
42
>>> ls.push(23)
>>> ls.top
23
>>> ls.pop()
23
>>> ls.top
42

  • 从示例看出Local对象存储的时候是类似字典的方式,需要有key和value,而LocalStack是基于栈的,通过push和pop来存储和弹出数据
  • 另外,当我们想释放存储空间的时候,也可以调用release_local()

LocalStack在Flask框架中会频繁的出现,其Request Context和App Context的实现都是基于LocalStack,具体可以参考Github上的Flask源码

LocalProxy

LocalProxy用于代理Local对象和LocalStack对象,而所谓代理就是作为中间的代理人来处理所有针对被代理对象的操作,如下图所示:

proxy.jpg

接下来我们将重点讲下如下内容:

  • LocalProxy的使用
  • LocalProxy代码解析
  • 为什么要使用LocalProxy

LocalProxy的使用

初始化LocalProxy有三种方式:

  1. 通过Local或者LocalStack对象的__call__ method
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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()

上述代码直接将对象像函数一样调用,这是因为Local和LocalStack都实现了__call__ method,这样其对象就是callable的,因此当我们将对象作为函数调用时,实际调用的是__call__ method,可以看下本文开头部分的Local的源代码,会发现__call__ method会返回一个LocalProxy对象

  1. 通过LocalProxy类进行初始化
    1
    2
    l = Local()
    request = LocalProxy(l, 'request')

实际上这段代码跟第一种方式是等价的,但这种方式是最'原始'的方式,我们在Local的源代码实现中看到其__call__ method就是通过这种方式生成LocalProxy的

  1. 使用callable对象作为参数
    1
    request = LocalProxy(get_current_request())

通过传递一个函数,我们可以自定义如何返回Local或LocalStack对象

那么LocalProxy是如何实现这种代理的呢?接下来看下源码解析

LocalProxy代码解析

下面截取LocalProxy的部分代码,我们来进行解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# LocalProxy部分代码

@implements_bool
class LocalProxy(object):
__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)

def _get_current_object(self):
"""Return the current object. This is useful if you want the real
object behind the proxy at a time for performance reasons or because
you want to pass the object into a different context.
"""
# 由于所有Local或LocalStack对象都有__release_local__ method, 所以如果没有该属性就表明self.__local为callable对象
if not hasattr(self.__local, '__release_local__'):
return self.__local()
try:
# 此处self.__local为Local或LocalStack对象
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError('no object bound to %s' % self.__name__)

@property
def __dict__(self):
try:
return self._get_current_object().__dict__
except RuntimeError:
raise AttributeError('__dict__')

def __getattr__(self, name):
if name == '__members__':
return dir(self._get_current_object())
return getattr(self._get_current_object(), name)

def __setitem__(self, key, value):
self._get_current_object()[key] = value

def __delitem__(self, key):
del self._get_current_object()[key]

if PY2:
__getslice__ = lambda x, i, j: x._get_current_object()[i:j]

def __setslice__(self, i, j, seq):
self._get_current_object()[i:j] = seq

def __delslice__(self, i, j):
del self._get_current_object()[i:j]

# 截取部分操作符代码
__setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
__delattr__ = lambda x, n: delattr(x._get_current_object(), n)
__str__ = lambda x: str(x._get_current_object())
__lt__ = lambda x, o: x._get_current_object() < o
__le__ = lambda x, o: x._get_current_object() <= o
__eq__ = lambda x, o: x._get_current_object() == o

  • 首先在__init__ method中传递的local参数会被赋予属性_LocalProxy__local,该属性可以通过self.local进行访问,关于这一点可以看StackOverflow的问题回答
  • LocalProxy通过_get_current_object来获取代理的对象。需要注意的是当初始化参数为callable对象时,则直接调用以返回Local或LocalStack对象,具体看源代码的注释。
  • 重载了绝大多数操作符,以便在调用LocalProxy的相应操作时,通过_get_current_object method来获取真正代理的对象,然后再进行相应操作

为什么要使用LocalProxy

可是说了这么多,为什么一定要用proxy,而不能直接调用Local或LocalStack对象呢?这主要是在有多个可供调用的对象的时候会出现问题,如下图:

multiple objects

我们再通过下面的代码也许可以看出一二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# use Local object directly
from werkzeug.local import LocalStack
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'})

def get_user():
# do something to get User object and return it
return user_stack.pop()


# 直接调用函数获取user对象
user = get_user()
print user['name']
print user['name']

打印结果是:

1
2
John
John

再看下使用LocalProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# use LocalProxy
from werkzeug.local import LocalStack, LocalProxy
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'})

def get_user():
# do something to get User object and return it
return user_stack.pop()

# 通过LocalProxy使用user对象
user = LocalProxy(get_user)
print user['name']
print user['name']

打印结果是:

1
2
John
Bob

怎么样,看出区别了吧,直接使用LocalStack对象,user一旦赋值就无法再动态更新了,而使用Proxy,每次调用操作符(这里[]操作符用于获取属性),都会重新获取user,从而实现了动态更新user的效果。见下图:

proxy auto select object

Flask以及Flask的插件很多时候都需要这种动态更新的效果,因此LocalProxy就会非常有用了。

至此,我们针对Local、LocalStack和LocalProxy的概念已经做了详细阐释,如果你觉得文章对你有帮助,不妨点个赞吧!