本页目录

Python装饰器

引入

这是一个greet函数:

Python
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")  # Hello, Alice!

下面定义了一个高阶函数add_goodbye,它接收一个函数fn作为参数,并返回另一个新函数wrapper_fn。新函数的功能是先执行原函数fn,再打印Goodbye!

Python
def add_goodbye(fn):
    def wrapper_fn(*args):
        fn(*args)
        print("Goodbye!")
    return wrapper_fn

add_goodbye(greet)返回的新函数在被调用时,会先执行greet函数,然后打印Goodbye!

Python
greet = add_goodbye(greet)
greet("Bob")
# Hello, Bob!
# Goodbye!

装饰器

上述例子可以用装饰器的语法写为:

Python
def add_goodbye(fn):
    def wrapper_fn(*args):
        fn(*args)
        print("Goodbye!")
    return wrapper_fn

@add_goodbye
def greet(name):
    print(f"Hello, {name}!")

greet("Carl")
# Hello, Carl!
# Goodbye!

装饰器是一种设计模式,能够在不修改原函数代码的情况下,向函数添加额外的功能。@就是Python中的装饰器语法糖,在def fn():前加上@decorator,就相当于执行了fn = decorator(fn)

例:输出函数运行时间

实现一个装饰器log_duration,在执行函数后打印函数运行时间。

Python
import time

def log_duration(fn):
    def wrapper_fn(*args, **kwargs):
        start = time.time()
        fn(*args, **kwargs)
        end = time.time()
        print(f"Function returned after {end - start} seconds.")
    return wrapper_fn

@log_duration
def loop(n):
    for i in range(n):
        pass

loop(n=10 ** 8)
# Function returned after 0.8091847896575928 seconds.

带参装饰器

有时我们希望装饰器能够接收参数,例如,在add_goodbye的例子中,我们希望能够自定义打印的告别语。此时我们需要再嵌套一层装饰器,外层装饰器接收自定义参数,创建不同的内层装饰器;内层装饰器则接收原函数并返回新的函数。

Python
def add_custom_goodbye(message):  # 外层装饰器
    def add_goodbye(fn):  # 内层装饰器
        def wrapper_fn(*args):
            fn(*args)
            print(message)
        return wrapper_fn
    return add_goodbye

@add_custom_goodbye(message="See you later!")
def greet(name):
    print(f"Hello, {name}!")

greet("Donald")
# Hello, Donald!
# See you later!

这里就相当于执行了greet = add_custom_goodbye("See you later!")(greet)

例:retry装饰器

实现一个retry装饰器,接收一个整数参数max_retries,表示最大重试次数。其功能为:如果装饰的函数抛出异常,则重新执行函数,直到达到最大重试次数。

这个功能在处理网络请求时非常有用,本例用一个70%概率抛出异常的函数代替网络请求。

Python
import random

def retry(max_retries):
    def inner_decorator(fn):
        def wrapper(*args):
            count = 0
            while count < max_retries:
                try:
                    return fn(*args)
                except Exception:
                    count += 1
                    print(f"Function failed, retrying {count}/{max_retries}...")
            print(f"Function failed after {count} retries")
        return wrapper
    return inner_decorator

@retry(max_retries=3)
def fake_request():
    if random.random() < 0.7:
        raise Exception
    return {"success": True}

print(fake_request())
# Function failed, retrying 1/3...
# Function failed, retrying 2/3...
# {'success': True}

关于functools.wraps

你可能在很多装饰器教程中见到使用functools.wraps的代码,这是为了保留函数的元信息。

回到最初的例子:

Python
def greet(name):
    print(f"Hello, {name}!")

print(greet)  # <function greet at 0x1025e9280>
print(greet.__name__)  # greet

当我们使用装饰器时,原函数的元信息会被覆盖:

Python
def add_goodbye(fn):
    def wrapper_fn(*args):
        fn(*args)
        print("Goodbye!")
    return wrapper_fn

@add_goodbye
def greet(name):
    print(f"Hello, {name}!")

print(greet)  # <function add_goodbye.<locals>.wrapper_fn at 0x100292430>
print(greet.__name__)  # wrapper_fn

此时可以使用functools.wraps来保留原函数的元信息,具体写法为:

Python
import functools

def add_goodbye(fn):
    @functools.wraps(fn)
    def wrapper_fn(*args):
        fn(*args)
        print("Goodbye!")
    return wrapper_fn

@add_goodbye
def greet(name):
    print(f"Hello, {name}!")

print(greet)  # <function greet at 0x102c0e430>
print(greet.__name__)  # greet