Python的装饰器,是Python函数(或者类)功能增强的一种方式。在了解装饰器之前,有必要先了解两个概念:闭包和柯里化。
Python的装饰器,实际上的闭包的应用。了解闭包是什么及其特性,请浏览这篇文章《理解Python闭包概念》。
所谓的柯里化,先来看看它在维基百科上的解释:
柯里化,英语:Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
例子如下:
# add 函数,接收两个参数并返回和
def add(x,y):
return x + y
# 将add函数柯里化
def add(x):
def _add(y):
return x + y
return _add
# 调用:
t = add(4)
t(5) # 输出 9
通过嵌套函数,把函数柯里化了。柯里化函数,其实就是闭包的应用。或者反过来说也成立,利用柯里化机制的函数,就是闭包函数。二者相辅相成。
了解了柯里化和闭包的概念,我们就可以来看看装饰器的概念了。
Python的装饰器是为函数进行功能增强的一种方式。试想下有这么一个需求:
实际业务中,某些关键性函数必须增加打印日志的功能。
为了降低代码的耦合性,打印功能不应该糅合到其他业务功能的代码中,而是作为一个增强性的功能而存在。此时,就不应该侵入到指定的函数增加输出日志的代码,而应该把它抽象出来,作为一个功能增强的装饰而存在。
我们知道,为了实现代码重用,可以把代码抽象为函数的形式。那装饰器是不是也是函数?是的。
本质上,装饰器就是一个函数(或者类),它接收函数作为参数,并返回另一个函数对象。因此,装饰器是一种高阶函数,它实现了对传入函数的功能的增强(装饰)。
下面来看看例子,是如何使用装饰器:
需求:一个加法函数,想增强它的功能,能够输出被调用过以及调用的参数信息。
# 原函数
def add(x,y):
return x + y
# 增加信息输出功能 v1
def add(x,y):
print("call add, x + y") # 日志输出到控制台
return x + y
上述的v1版加法函数满足了需求,但存在以下缺点:
我们只需要,在调用add函数前,把打印语句先打印出来就可以了。比如:
def add(x,y):
return x + y
def logger(fn,x,y):
print('args: {}.{}'.format(x,y))
ret = fn(x,y)
print('call finished!')
return ret
print(logger(add,4,5))
更加通用的实现:
def add(x,y):
return x + y
def logger(fn,*args,**kwargs): # 这里存在多个参数,可以做柯里化
print('args: {},{}'.format(*args,*kwargs))
ret = fn(*args,**kwargs)
print('call finished!')
return ret
print(logger(add,4,5))
对logger柯里化:
def add(x,y):
return x + y
def logger(fn):
def _logger(*args,**kwargs):
print('args: {},{}'.format(*args, *kwargs))
ret = fn(*args,**kwargs)
return ret
print('call finished!')
return _logger
print(logger(add)(4,5))
注意:logger(add)
返回的是_logger
,而_logger
函数返回的是fn
,也就是add
的调用结果。因此,实际上_logger
函数指向了新的add
函数。也就是说logger(add) -> _logger -> add
。
因此,可以如此表示add = logger(add)
,使用语法糖@,变成了以下形式:
def logger(fn):
def _logger(*args,**kwargs):
print('args: {},{}'.format(*args, *kwargs))
ret = fn(*args,**kwargs)
return ret
print('call finished!')
return _logger
@logger # 语法糖,用于装饰器函数。它相当于执行了add = logger(add)
def add(x,y):
return x + y
print(add(4,5))
业务函数好比一幅画,而装饰器则比作画外部的画框,以实现不同的装饰。比如,前置功能增强或者后置功能增强。
装饰器的本质是使用非侵入式代码进行功能增强。当然,此功能即使不使用,也不应该影响原来的业务功能。
经过logger装饰的add函数,实际上已经不是原来的add函数了,它已经变成了装饰器函数。因此带来一些副作用。
看下面的例子:
def logger(fn):
def wrapper(*args,**kwargs):
'I am wrapper'
print('args: {},{}'.format(*args, *kwargs))
ret = fn(*args,**kwargs)
return ret
print('call finished!')
return wrapper
@logger
def add(x,y):
'This is a function for add'
return x + y
print("name={},doc={}".format(add.__name__,add.__doc__))
输出:
call finished!
name=wrapper,doc=I am wrapper
add
函数的文档字符串改变了。原对象的属性都被替换了。实际上,我们关心的是被包装函数的属性(原add函数),而无关装饰器。
因此,需要考虑将需要的属性,从被包装函数,覆盖到包装函数中。
def copy_properties(src,dest):
dest.__name__ = src.__name__
dest.__doc__ = src.__doc__
# ... 还有许多属性
def logger(fn):
def wrapper(*args,**kwargs):
'I am wrapper'
print('args: {},{}'.format(*args, *kwargs))
ret = fn(*args,**kwargs)
return ret
copy_properties(fn,wrapper)
print('call finished!')
return wrapper
@logger
def add(x,y):
'This is a function for add'
return x + y
print("name={},doc={}".format(add.__name__,add.__doc__))
输出:
call finished!
name=add,doc=This is a function for add
我们应该知道,上述的copy_properties
函数,实际上就是wrapper
函数功能的增强,那是否可以把它改造成装饰器?
def copy_properties(src):
def _inner(dest):
dest.__name__ = src.__name__
dest.__doc__ = src.__doc__
return dest
return _inner
def logger(fn):
@copy_properties(fn) # wrapper = copy_properties(fn)(wrapper)
def wrapper(*args,**kwargs):
'I am wrapper'
print('args: {},{}'.format(*args, *kwargs))
ret = fn(*args,**kwargs)
return ret
print('call finished!')
return wrapper
@logger
def add(x,y):
'This is a function for add'
return x + y
print("name={},doc={}".format(add.__name__,add.__doc__))
因为装饰器携带参数,因此成为带参装饰器。上述的例子,是一个应用在装饰器里的装饰器。
functools
模块functools.update_wrapper
上述带参装饰器的例子中,写一个copy_properties
函数来指定原函数需要传递的属性值,这种方式不够优雅。functools.update_wrapper
正好解决了这个问题。
先看看它的构造函数:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
for attr in assigned: # 以元组的形式
try:
value = getattr(wrapped, attr) # 相当于value = wrapped[attr]
except AttributeError:
pass
else:
setattr(wrapper, attr, value) # 把原属性值,追加到wrapper. wrapper[attr] = value
for attr in updated: # 以字典的形式,attr为key
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
wrapper.__wrapped__ = wrapped
return wrapper
因此,可以如此使用它:
import functools
def logger(fn):
def wrapper(*args,**kwargs):
'I am wrapper'
print('args: {},{}'.format(*args, *kwargs))
ret = fn(*args,**kwargs)
return ret
functools.update_wrapper(wrapper,fn) # 更新属性
print('call finished!')
return wrapper
@logger
def add(x,y):
'This is a function for add'
return x + y
print("name={},doc={}".format(add.__name__,add.__doc__))
是否可以把functools.update_wrapper
改造成装饰器?
functools
的另一个工具可以解决这个问题。
functools.wraps
源码:
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
这里简单说明下偏函数的作用
functools.partial
把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。上述例子中,wraps 相当于update_wrapper(wrapped,assigned,updated)。当然它需要传递wrapped函数作为参数,因此它是一个带参装饰器。
偏函数把wrapped=wrapped,assigned=assigned, updated=updated这些参数固定住了,并传递给update_wrapper.
因此,可以如此使用它:
def logger(fn):
@functools.wraps(fn)
def wrapper(*args,**kwargs):
'I am wrapper'
print('args: {},{}'.format(*args, *kwargs))
ret = fn(*args,**kwargs)
return ret
print('call finished!')
return wrapper
此时,add.__wrapped__实际上就是原add,也就是fn了。这保留了wrapped函数。