1. 先说一说python中的函数

装饰器在 Python 使用如此方便都要归因于 Python 的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。
所以谈装饰器前,还要先要明白一件事,Python 中的函数和 Java、C++不太一样,Python 中的函数可以像普通变量一样当做参数传递给另外一个函数,函数还可以像整数一样作为函数的参数,例如:

  1. def foo(num):
  2. return num + 1
  3. def bar(fun):
  4. return fun(3)
  5. value = bar(foo)
  6. print(value) # 4

函数 bar 接收一个参数,这个参数是一个可被调用的函数对象,把函数 foo 传递到 bar 中去时,foo 和 fun 两个变量名指向的都是同一个函数对象,所以调用 fun(3) 相当于调用 foo(3):
python-function2.png

2. 引子

先来看一个简单例子,虽然实际代码可能比这复杂很多:

def foo():
    print('i am foo')

我们希望可以记录下函数的执行日志,有经验的程序员当然不会傻乎乎的在每个这样的函数里面都麻烦的打印一句话,这会造成大量的代码冗余,所以为了减少重复写代码,我们可以这样做,重新定义一个新的函数:专门处理日志 ,日志处理完之后再执行真正的业务代码:

def use_logging(func):
    logging.warn("%s is running" % func.__name__)
    func()

def foo():
    print('i am foo')

use_logging(foo)

这样做逻辑上是没问题的,功能是实现了,但是我们调用的时候不再是调用真正的业务逻辑 foo 函数,而是换成了 use_logging 函数,这就破坏了原有的代码结构, 现在我们不得不每次都要把原来的那个 foo 函数作为参数传递给 use_logging 函数,那么有没有更好的方式的呢?当然有,答案就是装饰器。

3. 装饰器

装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

def use_logging(func):
    def wrapper():
        logging.warn("%s is running" % func.__name__)
        return func()   # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()
    return wrapper

def foo():
    print('i am foo')

foo = use_logging(foo)  # 因为装饰器 use_logging(foo) 返回的时函数对象 wrapper,这条语句相当于  foo = wrapper
foo()                   # 执行foo()就相当于执行 wrapper()

use_logging 就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数 func 包裹在其中,看起来像 foo 被 use_logging 装饰了一样,use_logging 返回的也是一个函数,这个函数的名字叫 wrapper。在这个例子中,函数进入和退出时 ,被称为一个横切面,这种编程方式被称为面向切面的编程。

4. 语法糖

大白话,所谓语法糖就相当于汉语里的成语。即用更简练的言语表达较复杂的含义,在得到广泛接受的情况之下,可以提升交流的效率。
截屏2020-03-02上午10.46.17.png
python中@符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。

def use_logging(func):
    def wrapper():
        logging.warn("%s is running" % func.__name__)
        return func()
    return wrapper

@use_logging
def foo():
    print("i am foo")

foo()

如上所示,有了@,我们就可以省去foo = use_logging(foo)这一句了,直接调用 foo() 即可得到想要的结果。

5. 参数问题

如果我们的业务逻辑函数需要导入参数,我们可以利用python中的args和*kwargs来传入不定参数:

def wrapper(*args, **kwargs):
        # args是一个数组,kwargs一个字典(关键字参数)
        logging.warn("%s is running" % func.__name__)
        return func(*args, **kwargs)
    return wrapper

6. 带参数的装饰器

装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数 foo 。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的:

def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%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 foo(name='foo'):
    print("i am %s" % name)

foo()

7. 函数原信息问题

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、name、参数列表等,好在我们有functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的 func 函数也有和原函数 foo 一样的元信息了。

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print func.__name__      # 输出 'f'
        print func.__doc__       # 输出 'does some math'
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

8. 多层装饰器

一个函数还可以同时定义多个装饰器,比如:

@a
@b
@c
def f ():
    pass

它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于:

f = a(b(c(f)))

9. 常用小例子

最常用的不过属我们要查看某一模块(函数)运行时间了,我们可以自己设计一个装饰器:

import time, functools

def exe_time(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kw):
        start = time.time()
        x = fn(*args, **kw)
        end = time.time()
        print('%s exe %fs' %(fn.__name__, end-start))
        return x
    return wrapper


@exe_time
def fast(x, y):
    time.sleep(1)
    return x + y, y;

@exe_time
def slow(x, y, z):
    time.sleep(2)
    return x * y * z;

f = fast(11, 22)
s = slow(11, 22, 33)
print(f, s)

10. python内置装饰器

10.1 @property

对于一个类的属性,python的访问是没有限制的,但有时候我们需要对属性的访问加以限制,property装饰器就是干这个的。
可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:

class Student(object):
    @property
    def birth(self):
        return self._birth

    @birth.setter
    def birth(self, value):
        self._birth = value

    @property
    def age(self):
        return 2014 - self._birth

上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。

@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

10.2 @staticmethod、@classmethod、@abstractmethod

@staticmethod将类中的方法装饰为静态方法,即不与类和实例绑定,可直接调用。
@classmethod将类中的方法装饰为类方法,即不与实例绑定,可以通过类名直接引用。
@abstractmethod将类中的方法装饰为抽象方法。
关于类中不同方法的各自特点,可以参考我的另一篇文章

参考文献

  1. 理解 Python 装饰器看这一篇就够了