装饰器(Decorators)是 Python 的一个重要部分。简单地说:他们是修改其他函数的功能的函数。也有利于代码解耦。他们有助于让我们的代码更简短。装饰器让你在一个函数的前后去执行代码。

装饰器的底层实现here是基于闭包。

1. 装饰器基础--函数装饰函数

1.1. 函数、对象和变量

由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。

def now():
    print("2020-01-30")

f = now    # 函数赋值给变量
f()        # 通过变量调用函数

1.2. 一个简单的装饰器

装饰器就是一个返回函数的高阶函数。也就是接收一个函数作为参数。
log是一个装饰器。now是被装饰的函数,log可以用来给now增加功能。

def log(func):    # 接受函数作为参数
    def wrapper(*args, **kwargs):
        print("call %s" % func.__name__)
        return func(*args, **kwargs)
    return wrapper     # 返回一个函数

@log
def now():
    print("2020-01-30")
now()
out:
call now
2020-01-30

@log放在now函数上面,相当于执行了now = log(now)

1.3. 带参数的装饰器

如果装饰器需要传入参数,此时就需要先传入参数,之后传入函数,因此需要再封装一层。

def log(text):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print("%s, %s" % (text, func.__name__))
            return func(*args, **kwargs)
        return wrapper 
    return decorator

@log('execute')   # 这里传入参数
def now():
    print("2020-01-30")
now()

此时,相当于now = log('execute')(now)

1.4. 被装饰的函数带参数的时候

def log(func):    # 接受函数作为参数
    def wrapper(name, age):
        print("call %s" % func.__name__)
        print(name, age)
        return func()
    return wrapper     # 返回一个函数

@log
def now():
    print("2020-01-30")
now('Tom', 20) # 此时会调用wrapper函数,然后将参数传递进去。接着执行wrapper方法体,然后执行func()函数,然后返回func()函数的返回值
call now
Tom 20
2020-01-30

1.5. 解决函数签名变化的问题

上面不带参数和带参数的装饰器中,通过调用now.name发现返回值为wrapper,这是因为now=log(now),now=log('execute')(now)的返回值是wrapper函数,因此,有些依赖函数签名的代码执行会发生错误。
functools.wraps可以解决这个问题。

import functools

def log(func):    # 接受函数作为参数
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("call %s" % func.__name__)
        return func(*args, **kwargs)
    return wrapper     # 返回一个函数

@log
def now():
    print("2020-01-30")
now()

now.__name__
import functools
def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print("%s, %s" % (text, func.__name__))
            return func(*args, **kwargs)
        return wrapper 
    return decorator

@log('execute')
def now():
    print("2020-01-30")
now()
now.__name__

functools.wraps 旨在消除装饰器对原函数造成的影响,即对原函数的相关属性进行拷贝,已达到装饰器不修改原函数的目的。

2. 函数装饰类

使用函数装饰类实现单实例模式。

from functools import wraps
def singleton(cls):
    instances = {}
    @wraps(cls)
    def wrapper(*args, **kargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kargs)
        return instances[cls]
    return wrapper

@singleton
class Foo():
    pass

foo1 = Foo()
foo2 = Foo()

print(id(foo1))
print(id(foo2))

其中,@singleton相当于Foo = singleton(Foo),函数装饰类的参数是类。id() 函数用于获取对象的内存地址,通过id可以获取对象的内存地址,从而判断出是否是同一个对象。上面的两个输出结果是一样的。

3. 类装饰函数

class Counter:

    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

@Counter   # foo = Counter(foo),此时的foo相当于类Counter的实例。
def foo():
    pass

for i in range(5):
    foo()     # 调用实例对象,此时会执行__call__()方法。

print(foo.count)

4. 参考

  1. 廖雪峰装饰器讲解here
  2. functools.wraps()讲解here