一、创建装饰器时保留函数元信息
当装饰器作用在某个函数上时,这个函数的重要元信息:名字,文档,注解和参数签名都会丢失。
可以使用functools库中的@wraps装饰器来注解底层包装函数。
eg:
import time
from functools import 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
@wraps有一个重要的特征是它能让你通过属性__wrapped__直接访问被包装函数。
如果有多个装饰器,那么访问__wrapped__属性的行为是不可预知的,应避免这样做。
二、带参数的装饰器
例如:想写一个装饰器添加日志功能,同时允许用户指定日志的级别和其他选项,
from functools import wraps
import loging
def logged(level,name=None,message=None):
def decorate(func):
logname=name if name else func.__module__
log=logging.getLogger(logname)
logmesg=message if message else func.__name
@wraps(func)
def wrapper(*args,**kwargs):
log.log(level,logmesg)
return func(*arg,**kwargs)
return wrapper
return decorate
这段代码看上去很复杂,其实核心思想很简单:
最外层函数logged()接受参数并将他们作用在内部的装饰器函数上面,内层函数decorate()接受一个函数作为参数,然后在函数上面放置一个包装器,
三、可自定义属性的装饰器
场景:
如果想写一个装饰器来包装函数,并且允许用户提供参数在运行时控制装饰器行为。
解决方案:
引入访问函数,使用nonlocal来修改内部变量,然后这个访问函数被作为一个属性复制给包装函数。
from functools import wraps,partial
import logging
def attach_wrapper(obj,func=None):
if func is None:
return partial(attach_wrapper,obj)
setattr(obj,func.__name__,func)
return func
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)
log.log(level,logmsg)
return func(*args,**kwargs)
@attach_wrapper(wrapper)
def set_level(newlevel):
nonlocal level
level=newlevel
@attach_wrapper(wrapper)
def set_message(newmsg):
nonlocal logmsg
logmsg=newmsg
return wrapper
return decorate
其中set_level/set_message作为属性赋给包装器,每个访问函数允许使用nonlocal来修改函数内部的变量。
四、带可选参数的装饰器
场景:
如果想写一个装饰器,既可以不传参数比如:@decorator,也可以传递可选参数,比如:@decorator(x,y,z)
解决方案:
from functools import wraps,partial
import logging
def logged(func=None,*,level=loging.DEBUG,name=None,message=None):
if func is None:
return partial(logged,level=level,name=name,message=message)
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
#example
@logged
def add(x,y):
return x+y
@logged(level=loggin.CRITICAL,name='example')
def spam():
print ('test')
一般当我们使用装饰器的时候,要么不给他们传递参数,要么传递确切的参数。
要理解代码是如何工作的,得非常熟悉装饰器是如何作用到函数上以及它的调用规则。
上面的add函数,可以等价于:add=logged(add)
这时候函数会作为第一个参数直接传递给logged装饰器。因此logged()中的第一个参数就是被包装函数本身,所有其他参数都必须有默认值。
上面的spam函数,可以等价于:spam=logged(level=loging.CRITICAL,name='example)(func)
初始调用logged函数时,被包装函数并没有被传递进来,因此在装饰器中,它必须是可选的,这反过来会迫使其他参数必须使用关键字来指定。并且,这些参数被传递进来后,装饰器要返回一个接受一个函数参数并包装它的函数。为了这样做,可以使用functools.partial,它会返回一个未完全初始花的自身。除了被包装函数外其他参数都已经确定下来了。
五、利用装饰器强制函数上的类型检查
应用场景:
如果想对函数参数进行强制类型检查。
解决方案:
from inspect import signature
from functools import wraps
def typeassert(*ty_args,**ty_kwargs):
def decorate(func):
if not __debug__:
return func
sig=signature(func)
bound_types=sig.bind_partial(*ty_args,**ty_kwargs).arguments
@wraps(func)
def wrapper(*args,**kwargs):
bound_values=sig.bind(*args,**kwargs)
for name,value in bound_values.arguments.items():
if name in bound_types:
if not isinstance(value,bound_types[name]):
raise TypeError('argument {} must be {}'.format(name,bound_types[name]))
return func(*args,**kwargs)
return wrapper
return decorate
装饰器只会在函数定义时被调用一次,有时候你想去掉装饰器的功能,那么只需要简单的返回被装饰函数即可,如果全局变量__debug__被设置为False那么就会直接返回函数本身。
示例中还对被装饰函数的参数签名进行了检查,使用inspect.signature()函数。
六、装饰器为被包装函数增加参数
应用场景:
如果想在装饰器中给被包装函数增加额外的参数,但不能影响这个函数现有的调用。
解决方案
可使用关键字参数来给被包装函数增加额外参数。
from functools import wraps
def optional_debug(func):
@wraps(func)
def wrapper(*args,debug=False,**kwargs):
if debug:
print('calling',func.__name__)
return func(*args,**kwargs)
return wrapper
为了避免关键字参数与被包装函数的关键字参数重复,可以这样检查关键字参数:
from functools import wraps
import inspect
def optional_debug(func):
if 'debug' in inspect.getargspec(func).args:
raise TypeError('debug argument already defined')
@wraps(func)
def wrapper(*args,debug=False,**kwargs):
if debug:
print('calling',func.__name__)
return func(*args,**kwargs)
return wrapper
这么做之所以可以,是因为强制关键字参数很容易被添加到接受*args和kwargs参数的函数中。通过使用强制关键字参数,它被作为一个特殊情况被挑选出来,并接下来仅仅使用剩余的位置和关键字参数去调用这个函数时,这个特殊参数会被排除在外,也就是说它不会被纳入kwargs中去。