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