什么是 Python 装饰器

如果你能够认识到函数是一等公民(First-class)的话,那么你理解 Python 装饰器应该没有什么困难。函数是一等公民(First-class)就意味着:函数也是值,和其他基本类型(int, str, float, etc)等一样,都可以作为函数的入参和返回值

如果一个函数的入参是某函数,或者返回值是某函数,这样的函数也叫做高阶函数,装饰器就是这样子的

在我看来,装饰器的好处是不需要修改原有的函数的情况下修改函数的功能,这也合理,毕竟它叫做装饰

在详细了解 Python 装饰器之前,我想告诉你的是:Python 装饰器其实是语法糖,比如下面这段代码

python

@some_decorator
def foobar():
    ...

其实等价于下面这段代码

python

def foobar():
    ...

foobar = some_decorator(foobar)

如果有多个装饰器,也是一样的道理,越靠近被装饰的函数的装饰器越早起作用

python

@foo
@bar
def hello():
    ...

# ------------------

def hello():
    ...

hello = foo(bar(hello))

前面提到,装饰器不过就是输入为函数,输出也为函数的高阶函数,拆解一下

  • 输入为函数:函数是装饰器的入参
  • 输出为函数:装饰器返回的是函数

注意,一个函数可能有参数也可能没有参数,可能有 positional argument 还可能有 keyword argument。因此,为了能够表示各种可能性,可以用 *args, **kwargs表示。因此,经过整理,我们就能够推导出装饰器该怎么写了

python

def some_decorator(func):
    def inner(*args, **kwargs):
        # do something before the function call
        func(*args, **kwargs)
        # do something after the function call
    return inner

让我们考虑一个更复杂的情况,如果装饰器本身有参数呢?我们可以语法糖的角度反推一下

python

@some_decorator(k1=v1, k2=v2)
def foobar():
    ...

等价于

python

def foobar():
    ...
some_decorator(k1=v1, k2=v2)(foobar)

在前面的装饰器模板中,some_decorator 的入参是被装饰的函数 func,即 some_decorator(func),但现在变成了 some_decorator(k1=v1, k2=v2),所以我们知道:some_decorator(k1=v1, k2=v2)应该返回一个函数。那就是多嵌套一个函数,如下所示

python

def some_decorator(k1, k2):
    def wrapper(func):
        def inner(*args, **kwargs):
            # do something before the function call
            func(*args, **kwargs)
            # do something after the function call
        return inner
    return wrapper

面向对象编程里面会对属性(或称成员字段)进行封装,外界只能通过 getter/setter 进行访问。@property 正如它的名字一样,可以方便地访问/删除/修改属性,就好像这个属性不是私有属性,并且可以直接访问一样。比如

python

class Name:
    def __init__(self, x: str | None = None):
        self.__first_name = x

    @property
    def first_name(self):
        return self.__first_name

    @first_name.setter
    def first_name(self, value: str):
        self.__first_name = value

    @first_name.deleter
    def first_name(self, value: str):
        del self.__first_name


me = Name()
print(me.first_name)  # without parenthesis
me.first_name = "Martin"
print(me.first_name)

普通的装饰器存在一个缺陷,被装饰函数的名字会被修改,以前面的 some_decorator 这个装饰器为例

python

@some_decorator
def hello():
    print("hello")


print(hello.__name__)
# inner

可以看到,被装饰的函数 hello 的名字变成了 innersome_decorator 里面的函数),这在 Debug 的时候不大友好,因此,可以用 @functools.wraps

python

import functools


def some_decorator(func):
    @functools.wraps(func)   # <------------
    def inner(*args, **kwargs):
        # do something before the function call
        print("Before")
        func(*args, **kwargs)
        print("After")
        # do something after the function call

    return inner


@some_decorator
def hello():
    print("hello")


print(hello.__name__)
# hello

可以看到,函数 hello 的名字被保留了

在写算法题的时候,如果我们想要缓存一个函数的计算结果,就可以用 @cache。可以理解为它帮我们维护了一个字典,字典的 Key 是函数入参,字典的 Value 是函数的返回值

python

from functools import cache


@cache
def fib(n: int) -> int:
    if n == 0:
        return 0
    elif n == 1 or n == 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

这个装饰器也很常用,写起来也很简单,这是用来测量函数耗时的

python

import time
import functools


def timeit(func):
    @functools.wraps(func)
    def inner(*args, **kwargs):
        start = time.time()
        func(*args, **kwargs)
        end = time.time()
        print(f"Execute {func.__name__} in {end - start:.2f} seconds")

    return inner


@timeit
def foobar():
    ans = 0
    for i in range(10000000):
        ans += i

    return ans


foobar()

到这里本文就结束了,其实 Python 装饰器还可以用来装饰一个类,但在本文里面没有提及,因为我面向对象编程用得并不多。不过我相信,理解了 Python 装饰器如何装饰一个函数,那么你也能够理解 Python 装饰器是如何装饰一个类的