跳转至

Python 进阶教程系列 2:装饰器

本文是 Python 进阶教程系列 2,主要介绍了装饰器的机制和用法。

Python 是一种功能强大的编程语言,其灵活性和可扩展性使得开发者能够创造出各种强大且高效的应用程序。其中一个让 Python 如此受欢迎的特性就是装饰器(Decorators)。

装饰器是一种可以动态地修改某个类或函数的行为的函数,它们在不修改源代码的情况下为已经存在的函数或类添加额外的功能。

我对装饰器的理解是:装饰器即为“传入一个函数,传出一个被加工后的函数”的函数。

装饰器的概念和用法

假设我们有一个函数,用于计算两个数字的和:

Python
def add_numbers(a, b):
    return a + b

result = add_numbers(3, 5)
print(result)  # 输出:8

现在假设我们的需求是在函数执行前后打印出一些额外的信息,如参数和结果。我们可以通过修改原始函数来实现这一功能,但这不是一个好的实践,特别是当我们需要在多个函数中复用该功能时。

这时就可以使用装饰器来解决这个问题。下面是一个装饰器函数的简单示例:

Python
def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function {func.__name__} with args {args} and kwargs {kwargs}")
        result = func(*args, **kwargs)
        print(f"Function {func.__name__} returned {result}")
        return result
    return wrapper

在上面的示例中,我们定义了一个名为 logger 的装饰器函数,该函数接受一个函数作为参数,并返回一个新的函数 wrapperwrapper 函数包装了原始函数,打印出额外的信息,并调用原始函数本身。最后,我们返回 wrapper 函数作为装饰器的结果。

现在,我们可以将装饰器应用到我们的 add_numbers 函数上:

Python
@logger
def add_numbers(a, b):
    return a + b

result = add_numbers(3, 5)
print(result)

image-20230727200403956

通过在 add_numbers 函数的定义之前添加 @logger,我们将 add_numbers 函数作为参数传递给了 logger 装饰器。这样,每当我们调用 add_numbers 函数时,实际上是调用了 logger 装饰器返回的 wrapper 函数。

这样,每当我们调用 add_numbers 函数时,会自动打印出关于函数调用的信息。我们不需要修改 add_numbers 的定义,只需要通过装饰器来实现这一功能。

装饰器是 Python 中一种非常强大且灵活的语法,它可以用于许多场景,如日志记录、计时、缓存等。通过使用装饰器,我们可以轻松地为已存在的代码添加功能,而无需修改源代码。

使用 functools.wraps() 保持被装饰函数的元信息

装饰器在实现的时候,被装饰后的函数其实已经是另外一个函数了(函数名等函数元信息会发生改变)。为了不影响原函数的元信息,Python 的functools包中提供了一个叫wraps的装饰器来消除这样的副作用。写一个装饰器的时候,最好在 def wrapper 之前加上 @functools.wraps(func),它能保留原有函数的元信息。

不加 @functools.wraps(func)

Python
def my_decorator(func):
    def wrapper(*args, **kwargs):
        '''decorator'''
        print('Calling decorated function...')
        return func(*args, **kwargs)

    return wrapper


@my_decorator
def example():
    """Docstring"""
    print('Called example function')


print('name: {}'.format(example.__name__))
print('docstring: {}'.format(example.__doc__))

运行结果:

Text Only
name: wrapper
docstring: decorator

加上 @functools.wraps(func)

Python
import functools


def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        '''decorator'''
        print('Calling decorated function...')
        return func(*args, **kwargs)

    return wrapper


@my_decorator
def example():
    """Docstring"""
    print('Called example function')


print('name: {}'.format(example.__name__))
print('docstring: {}'.format(example.__doc__))

运行结果:

Text Only
name: example
docstring: Docstring

类装饰器

相比函数装饰器,类装饰更灵活,也更强大。在 Python 类中可以定义 __call__ 方法,使其在无需实例化的情况下自身可以被调用,而此时就会执行 __call__ 内部的代码。

Python
class Log(object):
    def __init__(self, func):
        self._func = func

    def __call__(self):
        print('before')
        self._func()
        print('after')

@Log
def hello():
    print('hello world!')

hello()

装饰器装饰顺序

一个函数其实可以同时被多个装饰器所装饰,那么多个装饰器的装饰顺序是怎样的呢?下面我们就来探索一下。

Python
def a(func):
    def wrapper():
        print('a before')
        func()
        print('a after')
    return wrapper

def b(func):
    def wrapper():
        print('b before')
        func()
        print('b after')
    return wrapper

def c(func):
    def wrapper():
        print('c before')
        func()
        print('c after')
    return wrapper

@a
@b
@c
def hello():
    print('Hello World!')

hello()

以上代码运行结果:

Text Only
a before
b before
c before
Hello World!
c after
b after
a after

多装饰的语法等效于 hello = a(b(c(hello)))。根据打印结果不难发现这段代码的执行顺序。如果你了解过 Node.js 的 Koa2 框架的中间件机制,那么你一定不会陌生以上代码的执行顺序,实际上 Python 装饰器同样遵循 洋葱模型。多装饰器的代码执行顺序就像剥洋葱一样,先由外到内进入,然后再由内到外。

一些实用的装饰器

代码计时

Python
import time
from functools import wraps

def timeit(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f'{func.__name__} 函数调用耗时:{end - start:.6f}')
        return result
    return wrapper

@timeit
def long_running_task():
    time.sleep(1)

long_running_task()
# long_running_task 函数调用耗时:1.001168

记录函数运行次数

参考代码

Ctrl+C 终止程序需二次验证

参考代码

评论