装饰器(Decorators)是 Python 的一个重要部分。其功能主要为:在不修改函数定义的基础下,增加或修改函数的功能;有助于让我们的代码更简短,也更Pythonic(Python范儿)。

预备知识

  1. 一切皆对象
    在python中一切皆对象,函数也是对象,因此函数可以作为 变量函数参数函数返回值;有些类似C语言中的函数指针;

函数作为变量

点击查看代码
def hi(name="Bob"):
    return "hi " + name

print(hi()) # 直接调用 
# [out] hi Bob

greet = hi # 作为变量赋值给 greet
print(greet())
# [out] hi Bob

del hi # 删除hi函数(此时应当是hi函数的实例对象)
print(hi())
# [out] NameError: name 'hi' is not defined
print(greet()) # 输出正常,
# [out] hi Bob
# 可以推测 greet = hi 的过程是将 hi函数实对象的引用赋给了greet,
# del hi是删除了hi变量和hi实例对象的引用关系,
# greet还可以正常引用hi对象实例

函数作为参数

点击查看代码
def funcFather(f):
    print("I'm funcFather")
    if callable(f):
        f()

def funcSon():
    print("I'm funcSon")

funcFather(funcSon)
# I'm funcFather
# I'm funcSon

函数作为返回值

点击查看代码
def funcFather():
    print("I'm funcFather")
    return funcSon

def funcSon():
    print("I'm funcSon")

f = funcFather()
f()
# I'm funcFather
# I'm funcSon

嵌套函数

在Python中,我们可以在一个函数funcA 中定义另一个函数funcB;

点击查看代码
def funcFather():
    print("I'm funcFather")

    def funcSon():
        print("I'm funcSon")

    funcSon()

funcFather()
# I'm funcFather
# I'm funcSon

funcB中可以继续嵌套funcC,funcD一直套娃;

闭包

闭包:如果在funcA中定义的局部变量var1,在funcA外部无法引用var1;
如果在函数funcA内定义funcB,在funcB中访问var1,此时在funcA中将funcB作为返回值,则可以达到在funcA外部使用var1;
所以在函数内定义函数,引用外部函数的变量,并以内部函数作为返回值,称为闭包;
以下是一个例子:

点击查看代码
def outer(x):
    def inner(y):
        return x + y

    return inner


funcIn = outer(5)
print(funcIn(6))
# 11

装饰器

其实装饰器就是一个闭包,装饰器是闭包的一种应用。用于拓展原来函数功能的一种函数,这个函数的特殊之处在于它的返回值也是一个函数,使用python装饰器的好处就是在不用更改原函数的代码前提下给函数增加新的功能。使用时,再需要的函数前加上@demo即可。

基础装饰器

点击查看代码
def log(func):
    def wrapper():
        print(f"[INFO]:  enter {func.__name__}")
        return func()

    return wrapper

@log
def hello(name="Bob"):
    print(f"Hi {name}")

hello()
# [INFO]:  enter hello
# Hi Bob

以上例说明装饰器执行过程:将被装饰函数(hello)作为变量传给装饰器函数log),
进入log函数,发现其返回了wrapper函数,而wrapper函数则是打印了提示信息后,执行了hello函数;因此以上以上装饰器等价于

点击查看代码
log_hello = log(hello)
log_hello()

functools.wraps

以上装饰器存在什么问题呢?当我们使用
print(hello.__name__)时,得到的是 wrapper,而不是我们期待的hello
回到装饰器原理,我们说加了装饰器的函数相当于被包裹了一层,其装饰器函数中的return wrapper才是我们实际得到的函数对象,它重写了我们函数的名字和注释文档(docstring),因此__name__被改变了。
幸运的是Python提供给我们一个简单的函数来解决这个问题,那就是functools.wraps。我们修改上一个例子来使用functools.wraps

点击查看代码
from functools import wraps


def log(f):
    @wraps(f)
    def wrapper():
        print("Enter wrapper")
        f()
        print("Leave wrapper")

    return wrapper


@log
def hello():
    print("Hi")
print(hello.__name__)
# hello

根据以上,一般装饰器定义大致如下:

点击查看代码
def decorator(func):
    @wraps(func)
    def wrapper():
        ...
    return wrapper

使用原函数参数的装饰器

如果需要在装饰器中使用被修饰函数的参数,通常如下定义:

点击查看代码
def show_func_args_and_ret(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"funcName:{func.__name__}\n"
              f"args:    {args}\n"
              f"kwargs:  {kwargs}\n")
        res = func(*args, **kwargs)
        print(f"return:  {res}\n")

    return wrapper


@show_func_args_and_ret
def hello(hour, name="Bob"):
    print(f"{hour}: Hi,{name}")
    return "ok"


hello(12, name="Ben")
# funcName:hello
# args:    (12,)
# kwargs:  {'name': 'Ben'}
#
# 12: Hi,Ben
# return:  ok

wrapper函数定义为wrapper(*args, **kwargs)以便接受任意格式的参数;由上面装饰器原理我们知道,传递给被修饰函数的参数,都会先传入wrapper函数,因此可以在wrapper 使用参数来显示函数参数,返回值等;在web编程中可以通过获取函数的request参数,完成鉴权等;

带参数装饰器

装饰器也是可以带参数的,但是刚刚演示的装饰器的参数被修饰函数,该如何使得装饰器有自己的参数呢,答案是再套一层;

点击查看代码
def logging(level):
    def out_wrapper(func):
        def wrapper(*args, **kwargs):
            print("[{0}]: enter {1}()".format(level, func.__name__))
            return func(*args, **kwargs)

        return wrapper

    return out_wrapper


@logging(level="INFO")
def hello(hour, name="Bob"):
    print(f"{hour}: Hi,{name}")
    return "ok"
# [INFO]: enter hello()
# 12: Hi,Ben

至于为什么带参数装饰器,就不会将第一层解释为函数指针,可能得了解一下装饰器的实现细节。

类装饰器

不咋用,先摘抄一段吧:
装饰器也不一定只能用函数来写,也可以使用类装饰器,用法与函数装饰器并没有太大区别,实质是使用了类方法中的call魔法方法来实现类的直接调用。

点击查看代码
class logging(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("[DEBUG]: enter {}()".format(self.func.__name__))
        return self.func(*args, **kwargs)

@logging
def hello(a, b, c):
    print(a, b, c)

hello("hello,","good","morning")
-----------------------------
>>>[DEBUG]: enter hello()
>>>hello, good morning

类装饰器也是可以带参数的,如下实现

点击查看代码
class logging(object):
    def __init__(self, level):
        self.level = level

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print("[{0}]: enter {1}()".format(self.level, func.__name__))
            return func(*args, **kwargs)
        return wrapper

@logging(level="TEST")
def hello(a, b, c):
    print(a, b, c)

hello("hello,","good","morning")
-----------------------------
>>>[TEST]: enter hello()
>>>hello, good morning

[========]
参考
python 装饰器详解
Python 函数装饰器