(由 DeepSeek 辅助翻译)
你是否曾觉得你的 Python 代码可以更优雅或更高效?装饰器可能是你正在寻找的改变游戏规则的工具。装饰器可以看作是一种特殊的修饰符,它们包裹在你的函数周围,以最小的努力添加功能。
这些强大的工具可以改变你的函数和类的行为,而无需修改其核心代码。Python 自带了一些内置的装饰器,可以提高你的代码质量、可读性和性能。
在本文中,我们将介绍一些 Python 中最实用的内置装饰器,你可以在日常开发中使用它们——用于优化性能、创建更简洁的 API、减少样板代码等等。这些装饰器大多属于 Python 内置的 functools 模块。
▶️ 你可以在 GitHub 上找到所有代码。
1. @property - 干净的属性访问
@property
装饰器将方法转换为属性,允许你在保持干净接口的同时添加验证逻辑。
在这里,我们创建了一个 Temperature
类,其中包含 celsius
和 fahrenheit
属性,它们会自动处理单位之间的转换。当你设置温度时,它会执行验证以防止物理上不可能的值(低于绝对零度)。
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")
|
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
不适用于需要实时计算的动态属性)
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))
|
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。
这个装饰器从你定义的最小集合中生成所有比较方法。
在这里,我们创建了一个具有完整比较功能的语义版本控制类。通过仅定义 __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
|
在编写自定义装饰器时,@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) 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
| help(add) print(f"Function name: {add.__name__}")
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 还创建了引人入胜的资源概述和编码教程。