0%

【译】提升编程技巧的7个强大的Python装饰器

(由 DeepSeek 辅助翻译)

你是否曾觉得你的 Python 代码可以更优雅或更高效?装饰器可能是你正在寻找的改变游戏规则的工具。装饰器可以看作是一种特殊的修饰符,它们包裹在你的函数周围,以最小的努力添加功能。

这些强大的工具可以改变你的函数和类的行为,而无需修改其核心代码。Python 自带了一些内置的装饰器,可以提高你的代码质量、可读性和性能。

在本文中,我们将介绍一些 Python 中最实用的内置装饰器,你可以在日常开发中使用它们——用于优化性能、创建更简洁的 API、减少样板代码等等。这些装饰器大多属于 Python 内置的 functools 模块。

▶️ 你可以在 GitHub 上找到所有代码。

1. @property - 干净的属性访问

@property 装饰器将方法转换为属性,允许你在保持干净接口的同时添加验证逻辑。

在这里,我们创建了一个 Temperature 类,其中包含 celsiusfahrenheit 属性,它们会自动处理单位之间的转换。当你设置温度时,它会执行验证以防止物理上不可能的值(低于绝对零度)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius

@property
def celsius(self):
return self._celsius

@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value

@property
def fahrenheit(self):
return self._celsius * 9/5 + 32

@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9

getter 和 setter 工作得非常顺畅,因此访问 temp.celsius 实际上会调用一个方法,但接口感觉就像一个常规属性。

输出:

1
2
3
temp = Temperature()
temp.celsius = 25 # 带有验证的干净属性式访问
print(f"{temp.celsius}°C = {temp.fahrenheit}°F")

2. @functools.cached_property - 延迟计算的属性

functools 模块中的 @cached_property 装饰器将 @property 与缓存结合,仅计算一次值,然后将其存储直到实例被删除。

这个例子展示了 @cached_property 如何提高昂贵计算的性能。

1
2
3
4
5
6
7
8
9
10
11
12
from functools import cached_property
import time

class DataAnalyzer:
def __init__(self, dataset):
self.dataset = dataset

@cached_property
def complex_analysis(self):
print("Running expensive analysis...")
time.sleep(2) # 模拟繁重的计算
return sum(x**2 for x in self.dataset)

第一次访问 complex_analysis 时,它会执行计算并缓存结果。所有后续访问都会立即返回缓存的值,而无需重新计算。

1
2
3
4
5
6
7
8
9
10
11
12
analyzer = DataAnalyzer(range(1000000))
print("First access:")
t1 = time.time()
result1 = analyzer.complex_analysis
t2 = time.time()
print(f"Result: {result1}, Time: {t2-t1:.2f}s")

print("\nSecond access:")
t1 = time.time()
result2 = analyzer.complex_analysis
t2 = time.time()
print(f"Result: {result2}, Time: {t2-t1:.2f}s")

对于这个例子,你会看到以下输出:

1
2
3
4
5
6
First access:
Running expensive analysis...
Result: 333332833333500000, Time: 2.17s

Second access:
Result: 333332833333500000, Time: 0.00s

这使得它非常适合可能多次引用相同计算的数据分析管道。

译注cached_property不适用于需要实时计算的动态属性)

3. @functools.lru_cache - 记忆化

lru_cache 装饰器根据参数缓存函数结果,有助于加速昂贵的计算。

这是一个递归的斐波那契序列计算器,通常效率低下。但 @lru_cache 装饰器通过存储先前计算的值使其变得高效。

1
2
3
4
5
6
7
from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)

每当函数使用之前见过的参数调用时,它会立即返回缓存的结果。

1
2
3
4
5
6
7
8
import time
start = time.time()
result = fibonacci(35)
end = time.time()
print(f"Fibonacci(35) = {result}, calculated in {end-start:.6f} seconds")

# 检查缓存统计信息
print(f"Cache info: {fibonacci.cache_info()}")

缓存统计信息显示了多少次调用被避免,展示了递归算法的巨大性能提升。

1
2
Fibonacci(35) = 9227465, calculated in 0.000075 seconds
Cache info: CacheInfo(hits=33, misses=36, maxsize=128, currsize=36)

4. @contextlib.contextmanager - 自定义上下文管理器

contextlib 中的 contextmanager 装饰器让你可以创建自己的上下文管理器,而无需实现完整的 __enter__/__exit__ 协议。

让我们看看如何用最少的代码创建自定义上下文管理器。file_manager 函数确保即使在发生异常时文件也能正确关闭(译注open函数本身就可以当作上下文管理器来使用):

1
2
3
4
5
6
7
8
9
from contextlib import contextmanager

@contextmanager
def file_manager(filename, mode):
try:
f = open(filename, mode)
yield f
finally:
f.close()

timer 函数测量任何代码块的执行时间:

1
2
3
4
5
6
7
@contextmanager
def timer():
import time
start = time.time()
yield
elapsed = time.time() - start
print(f"Elapsed time: {elapsed:.6f} seconds")

@contextmanager 装饰器处理了上下文管理协议的所有复杂性,让你可以专注于设置和清理逻辑。yield 语句标记了执行转移到 with 块内代码的位置。

以下是你可以如何使用这些上下文管理器。

1
2
3
4
5
6
with file_manager('test.txt', 'w') as f:
f.write('Hello, context managers!')

with timer():
# 要计时的代码
sum(i*i for i in range(1000000))

5. @functools.singledispatch - 函数重载

functools 中的 @singledispatch 装饰器实现了单分派泛型函数,允许根据参数类型选择不同的实现。

让我们创建一个灵活的格式化系统,适当地处理不同的数据类型。@singledispatch 装饰器让你可以为 format_output 函数定义一个默认实现,然后为不同类型注册专门的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from functools import singledispatch
from datetime import date, datetime

@singledispatch
def format_output(obj):
return str(obj)

@format_output.register
def _(obj: int):
return f"INTEGER: {obj:+d}"

@format_output.register
def _(obj: float):
return f"FLOAT: {obj:.2f}"

@format_output.register
def _(obj: date):
return f"DATE: {obj.strftime('%Y-%m-%d')}"

@format_output.register(list)
def _(obj):
return f"LIST: {', '.join(format_output(x) for x in obj)}"

当你调用该函数时,Python 会根据参数类型自动选择正确的实现。

1
2
3
4
5
6
7
8
9
10
results = [
format_output("Hello"),
format_output(42),
format_output(-3.14159),
format_output(date(2025, 2, 21)),
format_output([1, 2.5, "three"])
]

for r in results:
print(r)

输出:

1
2
3
4
5
Hello
INTEGER: +42
FLOAT: -3.14
DATE: 2025-02-21
LIST: INTEGER: +1, FLOAT: 2.50, three

这将以一种干净、可扩展的方式将基于类型的方法分派(常见于 Java 等语言)引入 Python。

6. @functools.total_ordering - 完整的比较操作

这个装饰器从你定义的最小集合中生成所有比较方法。

在这里,我们创建了一个具有完整比较功能的语义版本控制类。通过仅定义 __eq____lt__@total_ordering 装饰器会自动生成 __le____gt____ge__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from functools import total_ordering

@total_ordering
class Version:
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch

def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)

def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)

def __repr__(self):
return f"v{self.major}.{self.minor}.{self.patch}

这节省了大量的样板代码,同时确保所有比较操作的一致性。Version 类现在可以完全排序、与所有运算符进行比较,并用于任何需要有序对象的上下文中。

1
2
3
4
5
6
7
8
9
10
11
versions = [
Version(2, 0, 0),
Version(1, 9, 5),
Version(1, 11, 0),
Version(2, 0, 1)
]

print(f"Sorted versions: {sorted(versions)}")
print(f"v1.9.5 > v1.11.0: {Version(1, 9, 5) > Version(1, 11, 0)}")
print(f"v2.0.0 >= v2.0.0: {Version(2, 0, 0) >= Version(2, 0, 0)}")
print(f"v2.0.1 <= v2.0.0: {Version(2, 0, 1) <= Version(2, 0, 0)}")

输出:

1
2
3
4
Sorted versions: [v1.9.5, v1.11.0, v2.0.0, v2.0.1]
v1.9.5 > v1.11.0: False
v2.0.0 >= v2.0.0: True
v2.0.1 <= v2.0.0: False

7. @functools.wraps - 保留元数据

在编写自定义装饰器时,@wraps(同样来自 functools 模块)会保留原始函数的元数据,使调试更加容易。

这段代码展示了如何创建保留包装函数身份的适当装饰器。log_execution 装饰器在函数调用前后添加调试输出,同时保留原始函数的特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import functools

def log_execution(func):
@functools.wraps(func) # 保留 func 的名称、文档字符串等
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper

@log_execution
def add(a, b):
"""Add two numbers and return the result."""
return a + b

如果没有 @functools.wraps,装饰后的函数将丢失其名称、文档字符串和其他元数据。但保留元数据对于创建与文档生成器等正确配合的生产级装饰器至关重要。

1
2
3
4
5
# 没有 @wraps,help(add) 会显示 wrapper 的信息
help(add) # 显示原始文档字符串
print(f"Function name: {add.__name__}") # 显示 "add",而不是 "wrapper"

result = add(5, 3)

输出:

1
2
3
4
5
6
7
8
Help on function add in module __main__:

add(a, b)
Add two numbers and return the result.

Function name: add
Calling add with args: (5, 3), kwargs: {}
add returned: 8

总结

以下是我们学到的装饰器的快速总结:

  • @property 用于干净的属性接口
  • @cached_property 用于延迟计算和缓存
  • @lru_cache 用于性能优化
  • @contextmanager 用于资源管理
  • @singledispatch 用于基于类型的方法选择
  • @total_ordering 用于完整的比较运算符
  • @wraps 用于保留函数元数据

你还会在列表中添加什么?在评论中告诉我们。

Bala Priya C**** 是来自印度的开发者和技术作家。她喜欢在数学、编程、数据科学和内容创作的交叉领域工作。她的兴趣和专长领域包括 DevOps、数据科学和自然语言处理。她喜欢阅读、写作、编码和咖啡!目前,她正在通过编写教程、操作指南、观点文章等来学习和分享她的知识,与开发者社区交流。Bala 还创建了引人入胜的资源概述和编码教程。

扫码加入技术交流群🖱️
QR code