在httpbin源代码中遇到这么一个路由:

1
2
3
4
5
6
7
@app.route('/gzip')
@filters.gzip
def view_gzip_encoded_content():
"""Returns GZip-Encoded Data."""
return jsonify(get_dict(
'origin', 'headers', method=request.method, gzipped=True))

其中gzip是这么实现的:

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
@decorator
def gzip(f, *args, **kwargs):
"""GZip Flask Response Decorator."""
data = f(*args, **kwargs)
if isinstance(data, Response):
content = data.data
else:
content = data
gzip_buffer = BytesIO()
gzip_file = gzip2.GzipFile(
mode='wb',
compresslevel=4,
fileobj=gzip_buffer
)
gzip_file.write(content)
gzip_file.close()
gzip_data = gzip_buffer.getvalue()
if isinstance(data, Response):
data.data = gzip_data
data.headers['Content-Encoding'] = 'gzip'
data.headers['Content-Length'] = str(len(data.data))
return data
return gzip_data

从这个函数来看,gzip函数做的工作就是将路由函数执行一遍,取的结果后,将回应压缩,然后再返回发送响应。然后gzip函数是作为装饰器使用的。那么上面那个decorator装饰器做了什么工作呢,可以看出gzip目前的功能和普通的装饰器函数中内层函数功能基本上是一致的,所以调用这个view_gzip_encoded_content这个视图函数就相当于调用gzip函数。

那么decorator到底做了什么呢,怎么做的呢?在这里详细拆解一番。

以下内容部分翻译自decorator document

文档上说,如果你想以在Python版本中以一致性的方式保存被装饰函数的签名,这是你最好的选择。

装饰器的用处

Python中的装饰器是一个为什么语法糖很重要的有趣的例子。装饰器做到了以下的事情:

  • 装饰器减少了样板代码
  • 装饰器帮助分离关注点(模块化开发就起到了分离关注点的作用)
  • 装饰器增强了可读性和可维护性
  • 装饰器很明显

但是经典的装饰器实现需要嵌套函数,但是我们知道扁平是比嵌套要好得多。

所以decorator模块的目标是简化装饰器的使用,当然对于所有的技术,都有可能被滥用,你不应该在解决任何问题的时候都试图使用装饰器,虽然你可以这么做。

定义

一般来说,任何可以被以一个参数调用的Python对象都可以被用作装饰器。然而,这种定义太大以至于没太多用。将装饰器这个一般的类分成两种子类会更加方便:

  • 保留签名的装饰器

    接受一个函数作为参数的可调用对象会返回一个函数作为输出,并且有相同的签名

  • 改变签名的装饰器

    装饰器会改变给它们输入的函数的签名,或者装饰器会返回一个非调用对象

签名改变的装饰器有它们自己的用处:比如,内置的staticmethodclassmethod就是这种,它们接受函数然后返回描述符对象,既不是函数,也不是可调用对象。

当然,保留签名的装饰器更加普遍,更加容易去实现。特别的,它们可以组合在一起,但是其他装饰器一般不可以。

直接写一个保留签名的装饰器并不是那么显而易见,比如想定义一个合适的装饰器,接受有任意签名的函数作为输入。一个简单的例子将会澄清这个问题。

问题说明

一个装饰器通常的使用例子是将一个函数进行缓存。一个memorize装饰器通过将函数的返回结果存储在字典中起到缓存的作用。

这里有很多memorize的实现例子,但是它们都不保留签名。在最近的Python版本中你可以找到一个复杂的lru_cache装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
def memoize_uw(func):
func.cache = {}
def memoize(*args, **kw):
if kw: # frozenset is used to ensure hashability
key = args, frozenset(kw.items())
else:
key = args
if key not in func.cache:
func.cache[key] = func(*args, **kw)
return func.cache[key]
return functools.update_wrapper(memoize, func)

这里使用了functools.update_wrapper,这样你就不用从原来函数复制__name__, __doc__, __module____dict__等属性了,等同于调用functools.wraps(func)装饰器

这是一个使用例子

1
2
3
4
5
@memoize_uw
def f1(x):
"Simulate some long computation"
time.sleep(1)
return x

但是这个不是一个保留签名的装饰器,因为memoize_uw返回了一个和原始函数签名不同的函数。原先的函数只有一个参数x,但是装饰后的函数可以获得任意个参数和关键字参数。

1
2
3
>>> from decorator import getargspec # akin to inspect.getargspec
>>> print(getargspec(f1))
ArgSpec(args=[], varargs='args', varkw='kw', defaults=None)

这就意味着内省工具,比如pydoc会提供关于f1签名的错误信息-除非你是用Python3.5。这样会非常不好,pydoc说你可以接收一般的签名*args, **kw,但是这样调用就会出错。因为它真的只接收一个参数。

解决方法

解决方式就是提供一个一般的生成器工厂,其隐藏了生成一个保留签名的装饰器的复杂性。decorator函数在decorator模块就是这样一个工厂函数from decorator import decorate

decorator接受两个参数

  • 一个函数描述了装饰器的功能的调用者
  • 被装饰的函数

调用者必须有(f, *args, **kw)这样的签名,而且它必须使用argskw来调用原始函数f

1
2
3
4
5
6
7
8
9
def _memoize(func, *args, **kw):
if kw: # frozenset is used to ensure hashability
key = args, frozenset(kw.items())
else:
key = args
cache = func.cache # attribute added by memoize
if key not in cache:
cache[key] = func(*args, **kw)
return cache[key]

这样你就可以像下面这样定义你的装饰器

1
2
3
def memoize(f):
f.cache = {}
return decorate(f, _memoize)

和嵌套函数的方式不同的地方在于decorator模块强制让你将内层函数放在了上一层。而且,你也必须明确的传递你想装饰的函数,没有闭包。

1
2
3
4
5
6
7
8
9
10
>>> @memoize
... def heavy_computation():
... time.sleep(2)
... return "done"
>>> print(heavy_computation()) # the first time it will take 2 seconds
done
>>> print(heavy_computation()) # the second time it will be instantaneous
done

这样函数的签名是这样的:

1
2
>>> print(getargspec(heavy_computation))
ArgSpec(args=[], varargs=None, varkw=None, defaults=None)

所以一开始那个例子,就是这样的,先由decorator装饰了gzip,那么def decorator(caller, _func=None)中的caller就是gzip,在decorator中最后返回了FunctionMaker.create创建的一个对象,这个就是gzip的真正面目(在这个过程中,就处理了签名以及装饰器等问题),然后再由gzip去装饰视图函数,那么视图函数执行后的结果就会在gzip中处理,然后返回。