0%

Python 装饰器

什么是装饰器 Decorator

在写 Django 代码的时候经常用的装饰器,装饰器可以在不对代码本身该懂的前提下给代码添加更多的功能。

最简单的例子就是我们现在有一个函数 foo

1
2
def foo():
print('foo')

现在我们想给 foo 函数添加日志功能,当然可以直接修改 foo 的代码,但是日志功能这个代码显然是需要复用的,直接修改代码还不够 “抽象”。所以就产生了修饰器,所谓修饰器,就是生成函数的函数。

1
2
3
4
5
6
def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(func.__name__, 'is running')
return func(*args, **kwargs)
return wrapper

log 是一个函数,这个函数接受一个参数,这个参数是一个函数,在 wrapper 里,把原来的参数包装一遍返回,实际上,使用就是这样的过程

1
foo = log(foo)

此时的 foo 就是 wrapper,当我们调用 foo 的时候实际上在调用 wrapper。我们便实现了一次对编程的” 编程”,所以装饰器也叫元编程 Meta Programmming。

不过每次这样重新赋值的使用方法比较繁琐,所以 Python 为我们提供了 @语法糖,我们只需要在函数之前加上 @就可以使用装饰器了。

1
2
3
@log
def foo():
print('foo')

接受参数的修饰器

就按照刚才的 log 修饰器来说,我们可能需要在输出 log 时附带有其他的信息,所以需要一个能接受参数的修饰器,他是长这样的

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
from functools import wraps
import logging

def logged(level, name=None, message=None):
"""
Add logging to a function. level is the logging
level, name is the logger name, and message is the
log message. If name and message aren't specified,
they default to the function's module and name.
"""
def decorate(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__

@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
return decorate

# Example use
@logged(logging.DEBUG)
def add(x, y):
return x + y

@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')

这是 Python Cookbook 上面的例子

而当我们调用 add 时,相当于是

1
add = logged(logging.DEBUG)(add)

所以在函数中多了一层嵌套,logged(logging.DEBUG) 返回一个函数,这个函数接受一个函数,并且将其封装返回。

functools.wraps

在刚才的例子中我们使用了一个 functools.wraps 的修饰器,那么这个修饰器是做什么用的。

我们在创建一个函数的同时也会生成与之关联的元数据,比如说:

1
print(foo.__name__)

但是如果我们使用了装饰器,再去访问__name__属性,会发现函数的名称变成了内层的 wrapper,这是错误的,而 functools.wraps(func) 的作用就是将 func 函数的元信息传递给 wrapper 这个函数。

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
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Update a wrapper function to look like the wrapped function
wrapper is the function to be updated
wrapped is the original function
assigned is a tuple naming the attributes assigned directly
from the wrapped function to the wrapper function (defaults to
functools.WRAPPER_ASSIGNMENTS)
updated is a tuple naming the attributes of the wrapper that
are updated with the corresponding attribute from the wrapped
function (defaults to functools.WRAPPER_UPDATES)
"""
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
# from the wrapped function when updating __dict__
wrapper.__wrapped__ = wrapped
# Return the wrapper so this can be used as a decorator via partial()
return wrapper

def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Decorator factory to apply update_wrapper() to a wrapper function
Returns a decorator that invokes update_wrapper() with the decorated
function as the wrapper argument and the arguments to wraps() as the
remaining arguments. Default arguments are as for update_wrapper().
This is a convenience function to simplify applying partial() to
update_wrapper().
"""
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)

以上是 functools.wraps源码,核心部分是 update_wrapper 中的设置属性,将 wrapped 的属性设置给 wrapper,并且将 wrapper.__wrapped__设置为 wrapped

所以也可以通过 add.__wrapped__来访问原始函数

参考链接

Python Cookbook