理解 Python 装饰器

PythonBeginner
立即练习

介绍

在这个实验中,你将全面了解 Python 中的装饰器(decorators),这是一项用于修改或增强函数和方法的强大功能。我们将从介绍装饰器的基本概念开始,并通过实际示例探索其基本用法。

在此基础上,你将学习如何有效地使用 functools.wraps 来保留被装饰函数的重要元数据(metadata)。然后,我们将深入探讨像 property 装饰器这样的特定装饰器,理解它在管理属性访问中的作用。最后,本实验将阐明实例方法(instance methods)、类方法(class methods)和静态方法(static methods)之间的区别,并演示在这些场景中如何使用装饰器来控制类中方法的行为。

这是一个实验(Guided Lab),提供逐步指导来帮助你学习和实践。请仔细按照说明完成每个步骤,获得实际操作经验。根据历史数据,这是一个 初级 级别的实验,完成率为 93%。获得了学习者 100% 的好评率。

理解基础装饰器

在这一步,我们将介绍装饰器的概念及其基本用法。装饰器是一个接受另一个函数作为参数,添加一些功能,并返回另一个函数的函数,所有这些操作都不改变原始函数的源代码。

首先,在 WebIDE 左侧的文件浏览器中找到文件 decorator_basics.py。双击打开它。我们将在该文件中编写第一个装饰器。

将以下代码复制并粘贴到 decorator_basics.py 中:

import datetime

def log_activity(func):
    """A simple decorator to log function calls."""
    def wrapper(*args, **kwargs):
        print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
        result = func(*args, **kwargs)
        print(f"Function '{func.__name__}' finished.")
        return result
    return wrapper

@log_activity
def greet(name):
    """A simple function to greet someone."""
    print(f"Hello, {name}!")

## Call the decorated function
greet("Alice")

## Let's inspect the function's metadata
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")

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

  • 我们定义了一个装饰器函数 log_activity,它接受一个函数 func 作为参数。
  • log_activity 内部,我们定义了一个嵌套函数 wrapper。这个函数将包含新的行为。它打印一条日志消息,调用原始函数 func,然后打印另一条日志消息。
  • log_activity 函数返回 wrapper 函数。
  • @log_activity 语法位于 greet 函数上方,是 greet = log_activity(greet) 的简写形式。它将我们的装饰器应用于 greet 函数。

现在,保存文件(你可以使用 Ctrl+SCmd+S)。要运行脚本,请打开 WebIDE 底部的集成终端并执行以下命令:

python ~/project/decorator_basics.py

你将看到以下输出。请注意,日期时间(datetime)会有所不同。

Calling function 'greet' at 2023-10-27 10:30:00.123456
Hello, Alice!
Function 'greet' finished.

Function name: wrapper
Function docstring: None

请注意输出中的两点。首先,我们的 greet 函数现在被包装了日志消息。其次,函数的名称和文档字符串(docstring)已被 wrapper 函数的名称和文档字符串所取代。这对于调试和内省(introspection)来说可能是有问题的。在下一步中,我们将学习如何解决这个问题。

使用 functools.wraps 保留函数元数据

在上一步中,我们观察到装饰函数会用包装器函数的元数据(如 __name____doc__)替换原始函数的元数据。Python 的 functools 模块为此提供了一个解决方案:wraps 装饰器。

wraps 装饰器用于你自己的装饰器内部,目的是将原始函数的元数据复制到包装器函数上。

让我们修改 decorator_basics.py 中的代码。在 WebIDE 中打开该文件,并更新它以使用 functools.wraps

import datetime
from functools import wraps

def log_activity(func):
    """A simple decorator to log function calls."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
        result = func(*args, **kwargs)
        print(f"Function '{func.__name__}' finished.")
        return result
    return wrapper

@log_activity
def greet(name):
    """A simple function to greet someone."""
    print(f"Hello, {name}!")

## Call the decorated function
greet("Alice")

## Let's inspect the function's metadata again
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")

唯一的改动是:

  1. 我们从 functools 模块导入了 wraps
  2. 我们在定义 wrapper 函数的正上方添加了 @wraps(func)

保存文件,然后从终端再次运行它:

python ~/project/decorator_basics.py

现在,输出将会不同:

Calling function 'greet' at 2023-10-27 10:35:00.543210
Hello, Alice!
Function 'greet' finished.

Function name: greet
Function docstring: A simple function to greet someone.

正如你所见,函数名称现在正确地报告为 greet,并且其原始文档字符串得到了保留。使用 functools.wraps 是一种最佳实践,它能使你的装饰器更加健壮和专业。

使用 @property 实现受管属性

Python 提供了几种内置装饰器。其中最有用的是 @property,它允许你将一个类方法转换为一个“受管属性”(managed attribute)。这非常适合在不改变用户与类交互方式的情况下,为属性访问添加验证或计算等逻辑。

让我们通过创建一个 Circle 类来探索这一点。从文件浏览器中打开文件 property_decorator.py

将以下代码复制并粘贴到 property_decorator.py 中:

import math

class Circle:
    def __init__(self, radius):
        ## 实际值存储在“私有”属性中
        self._radius = radius

    @property
    def radius(self):
        """The radius property."""
        print("Getting radius...")
        return self._radius

    @radius.setter
    def radius(self, value):
        """The radius setter with validation."""
        print(f"Setting radius to {value}...")
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        """A read-only computed property for the area."""
        print("Calculating area...")
        return math.pi * self._radius ** 2

## --- Let's test our Circle class ---
c = Circle(5)

## Access the radius like a normal attribute (triggers the getter)
print(f"Initial radius: {c.radius}\n")

## Change the radius (triggers the setter)
c.radius = 10
print(f"New radius: {c.radius}\n")

## Access the computed area property
print(f"Circle area: {c.area:.2f}\n")

## Try to set an invalid radius (triggers the setter's validation)
try:
    c.radius = -2
except ValueError as e:
    print(f"Error: {e}")

在这段代码中:

  • radius 方法上的 @property 定义了一个“getter”(获取器)。当你访问 c.radius 时,它会被调用。
  • @radius.setterradius 属性定义了一个“setter”(设置器)。当你赋值时,例如 c.radius = 10,它会被调用。我们在这里添加了验证逻辑,以防止负值。
  • area 方法也使用了 @property,但没有定义 setter,因此它是一个只读属性。每次访问时都会计算其值。

保存文件,然后从终端运行它:

python ~/project/property_decorator.py

你应该会看到以下输出,它演示了 getter、setter 和验证逻辑是如何被自动调用的:

Getting radius...
Initial radius: 5

Setting radius to 10...
Getting radius...
New radius: 10

Calculating area...
Circle area: 314.16

Setting radius to -2...
Error: Radius cannot be negative

区分实例方法、类方法和静态方法

在 Python 类中,方法可以绑定到实例、类,或者不绑定。装饰器用于定义这些不同的方法类型。

  • 实例方法 (Instance Methods):默认类型。它们接收实例作为第一个参数,约定命名为 self。它们操作特定于实例的数据。
  • 类方法 (Class Methods):使用 @classmethod 标记。它们接收类作为第一个参数,约定命名为 cls。它们操作类级别的数据,通常用作替代构造函数。
  • 静态方法 (Static Methods):使用 @staticmethod 标记。它们不接收任何特殊的第一个参数。它们本质上是命名空间位于类中的常规函数,无法访问实例或类状态。

让我们看看这三种方法是如何实际应用的。从文件浏览器中打开文件 class_methods.py

将以下代码复制并粘贴到 class_methods.py 中:

class MyClass:
    class_variable = "I am a class variable"

    def __init__(self, instance_variable):
        self.instance_variable = instance_variable

    ## 1. Instance Method
    def instance_method(self):
        print("\n--- Calling Instance Method ---")
        print(f"Can access instance data: self.instance_variable = '{self.instance_variable}'")
        print(f"Can access class data: self.class_variable = '{self.class_variable}'")

    ## 2. Class Method
    @classmethod
    def class_method(cls):
        print("\n--- Calling Class Method ---")
        print(f"Can access class data: cls.class_variable = '{cls.class_variable}'")
        ## Note: Cannot access instance_variable without an instance
        print("Cannot access instance data directly.")

    ## 3. Static Method
    @staticmethod
    def static_method(a, b):
        print("\n--- Calling Static Method ---")
        print("Cannot access instance or class data directly.")
        print(f"Just a utility function: {a} + {b} = {a + b}")

## --- Let's test the methods ---
## Create an instance of the class
my_instance = MyClass("I am an instance variable")

## Call the instance method (requires an instance)
my_instance.instance_method()

## Call the class method (can be called on the class or an instance)
MyClass.class_method()
my_instance.class_method() ## Also works

## Call the static method (can be called on the class or an instance)
MyClass.static_method(10, 5)
my_instance.static_method(20, 8) ## Also works

保存文件,然后从终端运行它:

python ~/project/class_methods.py

仔细检查输出。它清晰地展示了每种方法类型的能力和局限性。

--- Calling Instance Method ---
Can access instance data: self.instance_variable = 'I am an instance variable'
Can access class data: self.class_variable = 'I am a class variable'

--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.

--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.

--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 10 + 5 = 15

--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 20 + 8 = 28

这个例子根据方法是否需要访问实例状态、类状态或两者都不需要,为何时使用每种类型的方法提供了清晰的参考。

总结

在这个实验(Lab)中,你对 Python 中的装饰器获得了实践性的理解。你首先学习了如何创建和应用一个基本的装饰器,为函数添加功能。然后,你看到了使用 functools.wraps 来保留原始函数的元数据(metadata)的重要性,这是编写清晰且可维护的装饰器的关键最佳实践。

此外,你还探索了强大的内置装饰器。你学会了使用 @property 装饰器来创建带有自定义 getter 和 setter 逻辑的受管属性,从而实现输入验证等功能。最后,你区分了实例方法、类方法 (@classmethod) 和静态方法 (@staticmethod),理解了它们如何根据对实例状态和类状态的访问权限,在类结构中发挥不同的作用。