介绍
在这个实验中,你将全面了解 Python 中的装饰器(decorators),这是一项用于修改或增强函数和方法的强大功能。我们将从介绍装饰器的基本概念开始,并通过实际示例探索其基本用法。
在此基础上,你将学习如何有效地使用 functools.wraps 来保留被装饰函数的重要元数据(metadata)。然后,我们将深入探讨像 property 装饰器这样的特定装饰器,理解它在管理属性访问中的作用。最后,本实验将阐明实例方法(instance methods)、类方法(class methods)和静态方法(static methods)之间的区别,并演示在这些场景中如何使用装饰器来控制类中方法的行为。
理解基础装饰器
在这一步,我们将介绍装饰器的概念及其基本用法。装饰器是一个接受另一个函数作为参数,添加一些功能,并返回另一个函数的函数,所有这些操作都不改变原始函数的源代码。
首先,在 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+S 或 Cmd+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__}")
唯一的改动是:
- 我们从
functools模块导入了wraps。 - 我们在定义
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.setter为radius属性定义了一个“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),理解了它们如何根据对实例状态和类状态的访问权限,在类结构中发挥不同的作用。



