Python 装饰器

Python 中的装饰器本质上就是一个函数(或者类,因为也存在类装饰器)装饰器最经典的应用场景就是在不改变原函数(类)的情况下增加其他功能或代码,或者说可以抽离大量与新函数功能无关的可复用代码。

装饰器由来

先看个例子:

def origin():
    print('This is the origin function')

origin()

>>> This is the origin function

现在有一个新的需求,要记录下函数执行日志,可以直接再函数定义中添加代码

def origin():
    print('This is the origin function')
    logging.info('origin is running')

>>> This is the origin function

但这样做有一个缺点就是如果有很多类似函数也要增加日志,那么每一函数都要相应修改很麻烦,为了复用代码可以这样

def use_logging(func):
    logging.warning('%s is running' % func.__name__)
    func()

def origin():
    print('This is the origin function')

use_logging(origin)

>>> WARNING:root:origin is running
This is the origin function

但这样破坏了原来的代码结构,每次都要将需要记录日志的函数作为参数传递给 use_logging, 这时候装饰器就应运而生

简单装饰器

def use_logging(func):
    def wrapper():
        logging.warning("%s is running" % func.__name__)
        return func()   # 把 origin 当做参数传递进来时,执行func()就相当于执行foo(), 虽然 func 可能没有返回值但这样写还是更安全
    return wrapper

def origin():
    print('This is the origin function')

origin = use_logging(origin)
origin()

>>> WARNING:root:origin is running
This is the origin function

这时,use_logging 就像是把真正的函数 origin 包裹起来了一样,所以叫做装饰器。这个例子中,函数进入和推出时被称为一个切面,所以这也叫做面向切面编程。

@语法糖

上面的例子中最后一步赋值可以被省略

origin = use_logging(foo)
origin()

在业务函数定义前面加上 @use_logging, 就可以省略最后一步的赋值过程

def use_logging(func):
    def wrapper():
        logging.warning("%s is running" % func.__name__)
        return func()   # 把 origin 当做参数传递进来时,执行func()就相当于执行foo(), 虽然 func 可能没有返回值但这样写还是更安全
    return wrapper
@use_logging
def origin():
    print('This is the origin function')

origin()

>>> WARNING:root:origin is running
This is the origin function

带参数的装饰器

有时装饰器可能也需要参数,可以给装饰器提供很大的灵活性,比如在装饰器中可以制定日志的等级

def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warning("%s is running" % func.__name__)
            elif level == "info":
                logging.info("%s is running" % func.__name__)
            return func(*args)
        return wrapper
    return decorator

@use_logging(level="warn")
def origin(name='origin'):
    print("This is %s" % name)

origin()

>>> WARNING:root:origin is running
This is origin

这就是允许参数的装饰器,实际上时对原有装饰器函数的再封装并返回一个装饰器,本质上这是一个含参数的闭包。当使用 @use_logging(level='warn') 调用的时候,Python 能发现这一层封装,并把参数传递到装饰器环境中。

@use_logging(level='warn') 等价于 @decorator(带参数)

类装饰器

装饰器不仅可以是函数,也可以是类,相比函数装饰器,类装饰器实际上具有更高的灵活性,内聚性和封装性。类装饰器主要依靠类的 call() 方法,当使用 @ 将装饰器附加到函数上的时候,就会调用此方法。

class test_decorator(object):
    def __init__(self, func):
        self._func = func

    def __call__(self):
        print ('class decorator runing')
        self._func()
        print ('class decorator ending')

@test_decorator
def origin():
    print ('This is the origin function')

origin()

>>> class decorator runing
This is the origin function
class decorator ending

functools.wraps

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的信息不见了,比如函数的docstring、name、参数列表,先看例子:

# 装饰器
def logged(func):
    def with_logging(*args, **kwargs):
        '''doc for with_logging'''
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   '''doc for f'''
   return x + x * x

>>> f.__name__
>>> with_logging
>>> f.__doc__
>>> doc for with_logging

函数 f 的信息被 with_logging 的信息取代了,于是它的 docstring, name 就变成了 with_logging 函数的信息了。 好在有 functools.wraps,wraps 本身也是一个装饰器,它能把原函数的信息拷贝到装饰器的 func 函数中,这就使得装饰器里的 func 具有和原函数一样的信息了。

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        '''doc for with_logging'''
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   '''doc for f'''
   return x + x * x

>>> f.__name__
>>> f
>>> f.__doc__
>>> doc for f

装饰器作用的顺序

一个函数可以同时被多个装饰器修饰,装饰器执行的顺序是从内到外的,最先调用内层的装饰器,然后依次到外层

@a
@b
@c
def f(x):
    return x+1
# 等价于
f = a(b(c(f)))