1、在函数上添加包装器
【问题】
你想在函数上添加一个包装器,增加额外的操作处理(比如日志、计时等)
【解决方法】
如果你想使用额外的代码包装一个函数,可以定义一个装饰器,如下:
def timethis(func):
@wraps(func)
def wrapper(*args,**kwargs):
start = time.time()
result = func(*args,**kwargs)
end = time.time()
print(func.__name__,end-start)
return result
return wrapper
@timethis
def countdown(n):
while n > 0:
n -= 1
countdown(1000000)
# countdown 0.044847965240478516
一个装饰器就是一个函数,它接受一个函数作为参数并返回一个新的函数。当你像下面这样写:
@timethis
def countdown(n):
pass
就跟下面这样写其实效果是一样的:
def countdown(n):
pass
countdown = timethis(countdown)
顺便说一下,内置的装饰器比如 @staticmethod,@classmethod,@property原理也是一样的。例如,下面这两个代码片段是等价的:
class A:
@classmethod
def method(cls):
pass
class B:
def method(cls):
pass
method = classmethod(method)
在上面的wapper()
函数中,装饰器内部定义了一个使用*args
和**kwargs
来接受任意参数的函数。在这个函数里面调用了原始函数并将其结果返回,不过你还可以添加其他额外代码。然后这个新的函数包装器作为返回结果返回来替代原始函数。
2、创建装饰器时保留函数元信息
【问题】
你写了一个装饰器作用在某函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了。
【解决方案】
任何时候你定义装饰器的时候,都应该使用 functools 库中的 @wraps 装饰器来注解底层包装函数。例如:
def timethis(func):
@wraps(func)
def wrapper(*args,**kwargs):
start = time.time()
result = func(*args,**kwargs)
end = time.time()
print(func.__name__,end-start)
return result
return wrapper
下面我们使用这个被包装后的函数并检查它的元信息
@timethis
def countdown(n):
while n > 0:
n -= 1
countdown(1000000)
print(countdown.__name__)
print(countdown.__doc__)
#countdown 0.04883718490600586
#countdown
#None
在编写装饰器的时候复制元信息是一个非常重要的部分,如果你忘记了使用@wraps,那么你会发现被装饰函数丢失了所有有用的信息。
3、解除一个装饰器
【问题】
一个装饰器已经作用在一个函数上,你想撤销它,直接访问原始的未包装的那个函数。
【解决方案】
假设装饰器是通过@wraps来实现的,那么你可以通过访问__wrapped__
属相来访问原始函数
@somedecorator
def add(x,y):
return x,y
orig_add = add.__wrapped__
orig_add(3,4)
# 7
直接访问未包装的原始函数在调试、内省和其他函数操作时很有用的。但是我们这里的方案仅仅用于在包装器正确使用了 @wraps 或者直接设置了__wrapped__
属性的情况。
如果有多个包装器,那么访问__wrapped__
属性的行为是不可预知的,应该避免这样做。
3、定义一个带参数的装饰器
【问题】
你想定义一个可以接受参数的装饰器
【解决方案】
我们用一个例子详细阐述接受参数的处理过程。假设你想写一个装饰器,给函数添加日志功能,当时允许用户指定日志的级别和其他的选项。下面这个装饰器的定义和使用示例:
from functools import wraps
import logging
def logged(level,name=None,message=None):
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
@logged(logging.DEBUG)
def add(x,y):
return x + y
@logged(logging.CRITICAL,'example')
def spam():
print('Spam!')
初看起来,这种实现看上去很复杂,但是核心思想很简单。最外层的函数 logged()接受参数并将他们作用在内部的装饰器函数上面。内层的函数 decorate() 接受一个函数作为参数,然后再函数上面放置一个装饰器。这里的关键点是包装器是可以传递给logged()的参数的。
定义一个接受参数的包装器看上去比较复杂主要原因是底层的调用序列。特别的,如果你有如下这个代码:
@decorator(x,y,z)
def func(a,b):
pass
装饰器处理过程跟下面的调用是等效的:
def func(a,b):
pass
func = decorator(x,y,z)(func)
decorator(x,y,z)的返回结果必须是一个可调用的对象,他接受一个函数作为参数并包装它。