装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
在装饰器中,通常使用下面这些python技巧
- *args和**kwargs
- 闭包
- 作为参数的函数
使用*收集位置参数
当参数被用在函数内部时,*将一组可变数量的位置参数集合成参数值的元组
def print_args(*args):
print("Positional argument tuple:", args)
>>>print_args()
Positional argument tuple: ()
>>>print_args(3, 2, 1, "hello", "world")
Positional argument tuple: (3, 2, 1, 'hello', 'world')
使用**收集关键字参数
使用**可以将参数收集到一个字典中,参数的名字是字典的键,对应参数的值是字典的值
def print_kwargs(**kwargs):
print("Keyword argument:", kwargs)
>>>print_kwargs()
Keyword argument: {}
>>>print_kwargs(key_1="value_1", key_2="value_2", key_3="value_3")
Keyword argument: {'key_1': 'value_1', 'key_2': 'value_2', 'key_3': 'value_3'}
闭包
在python中,可以在函数中定义另外一个函数(内部函数)。当需要在函数内部多次执行复杂的任务时,内部函数是非常有用的,从而避免了循环和代码的堆叠重复
def knights(saying):
def inner(quote):
return "We are the knights who say: '%s'" % quote
return inner(saying)
>>>knights("Ni!")
We are the knights who say: 'Ni!'
内部函数可以看作一个闭包。闭包是一个可以由另一个函数动态生成的函数,并且可以改变和存储函数外创建的变量的值
def knights2(saying):
def inner2():
return "We are the knights who say: '%s'" % saying
return inner2
a = knights2("Duck")
>>>type(a)
<class 'function'>
>>>a
<function knights2.<locals>.inner2 at 0x109c15598>
>>>a()
We are the knights who say: 'Duck'
- inner2()直接使用外部的saying参数,而不是通过另外一个参数获取。
- knights2()返回值为inner2函数对象,而不是调用它
inner2()函数可以得到saying参数的值并且记录下来。return inner2这行返回的是inner2函数的复制(没有直接调用)。所以它就是一个闭包:一个被动态创建的可以记录外部变量的函数
怎么写一个装饰器
在早些时候 (Python Version < 2.4,2004年以前),为一个函数添加额外功能的写法是这样的。
def debug(fun):
def wrapper():
print("[DEBUG]: enter {}():".format(fun.__name__))
return fun()
return wrapper
def say_hello():
print("hello world")
hello = debug(say_hello)
>>>hello()
[DEBUG]: enter say_hello():
hello world
None
上面的debug函数其实已经是一个装饰器了,它对原函数做了包装并返回了另外一个函数,额外添加了一些功能。因为这样写实在不太优雅,在后面版本的Python中支持了@语法糖,下面代码等同于早期的写法。
def debug(fun):
def wrapper():
print("[DEBUG]: enter {}():".format(fun.__name__))
return fun()
return wrapper
@debug
def say_hello():
print("hello world")
>>>say_hello()
[DEBUG]: enter say_hello():
hello world
被装饰的函数带参数
使用位置参数*和关键字参数**
def debug(fun):
def wrapper(*args, **kwargs):
print("[DEBUG]: enter {}():".format(fun.__name__))
return fun(*args, **kwargs)
return wrapper
@debug
def say_hello(name):
print("hello", name)
>>>say_hello("tan")
[DEBUG]: enter say_hello():
hello tan
带参数的装饰器
当带参数的装饰器被打在某个函数上时,比如@logging(level='INFO')
,它其实是一个函数,会马上被执行,它返回的结果是一个装饰器.
def logging(level):
def outer_wrapper(func):
def inner_wrapper(*args, **kwargs):
print("[{level}]: enter {func}()".format(level=level, func=func.__name__))
return func(*args, **kwargs)
return inner_wrapper
return outer_wrapper
@logging(level="INFO")
def say_hello(name):
print("hello world:", name)
# 如果没有使用@语法,等同于
# say = logging(level='INFO')(say_hello)
>>>say_hello("tan")
[INFO]: enter say_hello()
hello world: tan
基于类实现的装饰器
装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例
外。只要某个对象重载了__call__()
方法,那么这个对象就是callable的。
class Test():
def __call__(self):
print 'call me!'
t = Test()
>>>t()
call me
像__call__
这样前后都带下划线的方法在Python中被称为内置方法,有时候也被称为魔法方法。重载这些魔法方法一般会改变对象的内部行为。上面这个例子就让一个类对象拥有了被调用的行为。
回到装饰器上的概念上来,装饰器要求接受一个callable对象,并返回一个callable对象.那么用类来实现也是也可以的.我们可以让类的构造函数__init__()
接受一个函数,然后重载__call__()
并返回一个函数,也可以达到装饰器函数的效果.
class logging:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("[INFO]: enter {func}()".format(func=self.func.__name__))
return self.func(*args, **kwargs)
@logging
def say_hello(name):
print("hello world", name)
>>>say_hello("tan")
[INFO]: enter say_hello()
hello world tan
带参数的类装饰器
如果需要通过类形式实现带参数的装饰器,那么会比前面的例子稍微复杂一点. 那么在构造函数里接受的就不是一个函数,而是传入的参数。通过类把这些参数保存起来. 然后在重载__call__
方法是就需要接受一个函数并返回一个函数
class logging:
def __init__(self, level):
self.level = level
def __call__(self, func):
def wrapper(*args, **kwargs):
print("[{level}]: enter {func}()".format(level=self.level, func=func.__name__))
return func(*args, **kwargs)
return wrapper
@logging(level="INFO")
def say_hello(name):
print("hello world", name)
>>>say_hello("tan")
[INFO]: enter say_hello()
hello world tan
使用decorator优化装饰器
嵌套的装饰函数不太直观,我们可以使用第三方包类改进这样的情况,让装饰器函数可读性更好. decorator.py 是一个非常简单的装饰器加强包。你可以很直观的先定义包装函数wrapper()
,再使用decorate(func, wrapper)
方法就可以完成一个装饰器。
from decorator import decorate
from datetime import datetime
def wrapper(func, *args, **kwargs):
"""print log before a function."""
print("[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__))
return func(*args, **kwargs)
def logging(func):
return decorate(func, wrapper) # 用wrapper装饰func
@logging
def say_hello(name):
print("hello world", name)
>>>say_hello("tan")
[DEBUG] 2018-11-26 11:01:21.879185: enter say_hello()
hello world tan
你也可以使用它自带的@decorator
装饰器来完成你的装饰器。
from decorator import decorator
@decorator
def logging(func, *args, **kwargs):
"""print log before a function."""
print("[DEBUG]: enter {}()".format(func.__name__))
return func(*args, **kwargs)
@logging
def say_hello(name):
print("hello world", name)
>>>say_hello("tan")
[DEBUG]: enter say_hello()
hello world tan
decorator.py
实现的装饰器能完整保留原函数的name
,doc
和args
,唯一有问题的就是inspect.getsource(func)
返回的还是装饰器的源代码,你需要改成inspect.getsource(func.__wrapped__)
使用wrapt优化装饰器
wrapt是一个功能非常完善的包,用于实现各种你想到或者你没想到的装饰器。使用wrapt实现的装饰器你不需要担心inspect中遇到的所有问题,因为它都帮你处理了,甚至inspect.getsource(func)
也准确无误。
import wrapt
@wrapt.decorator
def logging(wrapped, instance, args, kwargs):
print("[INFO]: enter {}()".format(wrapped.__name__))
return wrapped(*args, **kwargs)
@logging
def say_hello(name):
print("hello world", name)
say_hello("tan")
使用wrapt你只需要定义一个装饰器函数,但是函数签名是固定的,必须是(wrapped, instance, args, kwargs)
,注意第二个参数instance
是必须的,就算你不用它。当装饰器装饰在不同位置时它将得到不同的值,比如装饰在类实例方法时你可以拿到这个类实例。根据instance
的值你能够更加灵活的调整你的装饰器。另外,args
和kwargs
也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。
使用wrapt写一个带参数的装饰器
import wrapt
def logging(level):
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
print("[{}]: enter {}()".format(level, wrapped.__name__))
return wrapped(*args, **kwargs)
return wrapper
@logging(level="INFO")
def say_hello(name):
print("hello world", name)
>>>say_hello("tan")
[INFO]: enter say_hello()
hello world tan