Python 装饰器

PythonBeginner
立即练习

介绍

在这个 Lab 中,你将学习 Python 装饰器(Decorators),这是一项强大的功能,它提供了一种简洁且可重用的方式来扩展函数或类的功能,而无需永久修改它们。

你将从构建一个简单的装饰器开始,然后学习如何处理函数参数,使用 functools.wraps 遵循最佳实践,创建接受自己参数的装饰器,最后,实现使用类来管理状态的装饰器。

基础装饰器

在这一步中,你将学习 Python 装饰器的基本结构。装饰器最简单的形式是一个函数,它接收另一个函数作为参数,添加一些功能,然后返回另一个函数。

注意: 如果你不熟悉使用 WebIDE(类似 VS Code),请先在 这个 Lab 学习。

首先,从左侧的文件浏览器中打开文件 step1.py。

现在,将以下代码复制到 step1.py 文件中。这段代码定义了一个简单的装饰器并将其应用于一个函数。

## 装饰器:一个接收函数并返回一个包装器(wrapper)的函数
def your_decorator(func):
  def wrapper():
    ## 在 func 之前执行操作...
    print("Before func!")
    func()  ## 调用原始函数
    ## 在 func 之后执行操作...
    print("After func!")
  return wrapper  ## 返回包装器函数

## @your_decorator 是以下语法的语法糖:foo = your_decorator(foo)
@your_decorator
def foo():
  print("Hello World!")

foo()  ## 调用 wrapper,wrapper 以额外的行为调用 foo

我们来分解一下这段代码:

  • your_decorator(func):这是装饰器函数。它接收一个参数 func,即要被装饰的函数。
  • wrapper():这是一个“包装”原始函数的内部函数。它包含你想添加的新行为。在这里,它在调用原始函数之前和之后打印消息。
  • @your_decorator:这是应用装饰器的语法糖。它等同于在函数定义后写 foo = your_decorator(foo)。

现在,在终端中运行脚本以查看装饰器的实际效果。

python3 step1.py

你应该会看到以下输出,这表明 wrapper 函数被执行了,在原始 foo 函数周围添加了行为。

Before func!
Hello World!
After func!

带参数函数的装饰器

在这一步,你将扩展你的装饰器,使其能够处理接受参数的函数。实际应用中的函数通常带有参数,我们的装饰器需要能够正确处理它们。

为此,你可以在包装器(wrapper)函数的签名中使用 *args 和 **kwargs。这使得包装器能够接受任意数量的位置参数和关键字参数,并将它们传递给原始函数。

打开文件 step2.py 并添加以下代码:

## 适用于带参数函数的装饰器
def your_decorator(func):
  def wrapper(*args,**kwargs):  ## 接受任意参数
    ## 在 func 之前执行操作...
    print("Before func!")
    func(*args,**kwargs)  ## 将参数传递给原始函数
    ## 在 func 之后执行操作...
    print("After func!")
  return wrapper

@your_decorator
def foo(bar):
  print("My name is " + bar)

foo("Jack")  ## 参数通过 wrapper 传递

在这段代码中,wrapper 函数使用 *args 和 **kwargs 定义。当调用 foo("Jack") 时,参数 "Jack" 被 wrapper 中的 *args 捕获,然后在执行 func(*args, **kwargs) 时传递给原始函数 func。

运行脚本查看结果。

python3 step2.py

输出显示参数已成功通过装饰器传递给了原始函数。

Before func!
My name is Jack
After func!

基础装饰器模板

在这一步,你将发现创建健壮装饰器的最佳实践模板。虽然前面的装饰器有效,但它们有一个副作用:它们会丢失原始函数的元数据(如其名称 __name__ 和文档字符串 __doc__)。

为了解决这个问题,Python 的 functools 模块提供了一个名为 wraps 的辅助装饰器。此外,一个好的装饰器应该能够处理返回值的函数。

此模板适用于大多数装饰器用例。它对有参数或无参数、有返回值或无返回值的函数都有效。

打开文件 step3.py 并添加以下代码。

import functools

## 最佳实践装饰器模板:保留函数元数据和返回值
def your_decorator(func):
  @functools.wraps(func)  ## 保留函数名称、文档字符串等
  def wrapper(*args,**kwargs):
    print("Before function execution...")
    result = func(*args,**kwargs)  ## 调用函数并捕获返回值
    print("After function execution...")
    return result  ## 返回原始函数的返回值
  return wrapper

@your_decorator
def add(a, b):
  """This function adds two numbers."""
  return a + b

sum_result = add(5, 3)
print(f"Function name: {add.__name__}")
print(f"Docstring: {add.__doc__}")
print(f"Result: {sum_result}")

此模板中的关键改进点:

  • import functools: 导入所需的模块。
  • @functools.wraps(func): 这是一个应用于 wrapper 函数的装饰器。它将原始 func 的元数据(如 __name__ 和 __doc__)复制到 wrapper 函数上。
  • result = func(*args, **kwargs): 原始函数的返回值被捕获到 result 变量中。
  • return result: 包装器函数返回捕获到的结果,使得被装饰的函数表现符合预期。

执行脚本:

python3 step3.py

输出证明了函数的名称和文档字符串被保留了,并且返回值得到了正确处理。

Before function execution...
After function execution...
Function name: add
Docstring: This function adds two numbers.
Result: 8

带参数的装饰器

在这一步,你将学习如何创建可以使用自身参数进行定制的装饰器。这使得装饰器用法更加灵活和可配置,例如 @your_decorator(arg='value')。

要实现这一点,你需要创建一个“装饰器工厂”(decorator factory)——一个接受参数并返回一个装饰器的函数。这为我们的结构增加了一个额外的嵌套层级。

让我们创建一个接受一个 prefix 字符串并在函数调用前后打印它的装饰器。打开 step4.py 并添加以下代码:

import functools

## 装饰器工厂:根据参数返回一个装饰器
def prefix_decorator(prefix):
  def decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
      ## 使用来自工厂的 'prefix' 参数
      print(f"{prefix} Before function call")
      result = func(*args, **kwargs)
      print(f"{prefix} After function call")
      return result
    return wrapper
  return decorator  ## 返回实际的装饰器函数

## 使用带参数的装饰器
@prefix_decorator(prefix="LOG:")
def say_hello(name):
  print(f"Hello, {name}!")

say_hello("Alice")

工作原理如下:

  1. 首先调用 prefix_decorator(prefix="LOG:")。它是工厂函数。
  2. 它返回 decorator 函数,该函数现在可以通过闭包(closure)访问 prefix 变量("LOG:")。
  3. 然后,返回的这个 decorator 应用于 say_hello,对其进行装饰。

运行脚本查看参数是如何被使用的。

python3 step4.py

输出显示了作为参数传递给装饰器的 "LOG:" 前缀。

LOG: Before function call
Hello, Alice!
LOG: After function call

基于类的装饰器

在这一步,你将探索如何使用类来实现装饰器。这种方法在维护函数调用之间的状态时特别有用。我们将介绍两种模式:装饰类中的一个方法,以及将装饰器本身定义为一个类。

打开文件 step5.py 开始操作。

模式 1:装饰类方法

你可以将一个装饰器定义为一个类中的方法,并将其应用于该类的其他方法。这对于创建与类上下文相关的特定行为非常有用。

将以下代码添加到 step5.py 中。此示例定义了一个装饰器来处理类方法的异常。

## 模式 1:装饰类中的一个方法
class DecorateMyMethod:

  ## 静态方法装饰器,适用于仅包含 'self' 参数的方法
  def decorator_for_class_method_with_no_args(method):
    def wrapper_for_class_method(self):  ## 只接收 self
      try:
        return method(self)  ## 调用原始方法
      except Exception as e:
        print(f"\nWARNING: An exception occurred: {e}\n")
    return wrapper_for_class_method

  def __init__(self,succeed:bool):
    self.succeed = succeed

  @decorator_for_class_method_with_no_args
  def class_action(self):
    if self.succeed:
      print("You succeeded by choice.")
    else:
      raise Exception("Epic fail of your own creation.")

print("--- Testing Pattern 1 ---")
test_succeed = DecorateMyMethod(True)
test_succeed.class_action()

test_fail = DecorateMyMethod(False)
test_fail.class_action()
print("-" * 25)

模式 2:将装饰器作为类

一个更常见且强大的模式是将装饰器本身定义为一个类。当装饰器需要维护状态时,这种方法非常理想。通过实现 __init__() 和 __call__() 方法,你可以使类的实例表现得像一个函数。

让我们创建一个装饰器来计算一个函数被调用了多少次。将以下代码添加到 step5.py 的末尾。

## 模式 2:用于维护状态的基于类的装饰器
class CountCallNumber:

  def __init__(self, func):
    self.func = func  ## 存储要装饰的函数
    self.call_number = 0  ## 初始化调用计数器

  def __call__(self, *args, **kwargs):  ## 使实例可被调用
    self.call_number += 1  ## 增加计数器
    print("This is execution number " + str(self.call_number))
    return self.func(*args, **kwargs)  ## 调用原始函数

@CountCallNumber
def say_hi(name):
  print("Hi! My name is " + name)

print("\n--- Testing Pattern 2 ---")
say_hi("Jack")
say_hi("James")
  • __init__(self, func): 这是构造函数。当应用装饰器(@CountCallNumber)时调用它。它接收要被装饰的函数(say_hi)并将其存储起来。
  • __call__(self, *args, **kwargs): 此方法使类实例可被调用。当你调用被装饰的函数(say_hi("Jack"))时,你实际上是在调用此 __call__ 方法。它会增加计数器,然后调用原始存储的函数。

现在,运行整个脚本。

python3 step5.py

你将看到两种模式的输出。注意第二部分中调用计数器是如何递增的。

--- Testing Pattern 1 ---
You succeeded by choice.

WARNING: An exception occurred: Epic fail of your own creation.

-------------------------

--- Testing Pattern 2 ---
This is execution number 1
Hi! My name is Jack
This is execution number 2
Hi! My name is James

总结

恭喜你完成了本次实验!你已经对 Python 装饰器及其各种形式有了扎实的理解。

在本次实验中,你学会了如何:

  • 创建一个基础装饰器并理解 @ 语法。
  • 设计使用 *args 和 **kwargs 来处理接受任意数量参数的函数的装饰器。
  • 使用带有 functools.wraps 的最佳实践模板来保留函数元数据并处理返回值。
  • 构建装饰器工厂(decorator factories),允许使用参数来自定义你的装饰器。
  • 实现将装饰器作为类,以便在函数调用之间维护状态,例如在调用计数器中。

装饰器是编写干净、可重用和可扩展的 Python 代码的关键特性。你现在可以将这些模式应用到你自己的项目中,以增强代码质量。