























Python 3 装饰器是在不修改原函数代码的前提下,为函数动态添加额外功能的高级工具。其核心基于“闭包”和“函数式编程”思想。
下面我从基础原理到高阶实战,为你构建一套完整的知识体系。
在 Python 中,函数是一等公民,这意味着它可以像变量一样被传递、赋值和返回。
def say_hello():
print("Hello")
# 将函数赋值给变量
greet = say_hello
greet() # 输出 Hello
# 将函数作为参数传入另一个函数
def run(func):
func()
run(say_hello) # 输出 Hello
装饰器正是利用了这一特性:它是一个接收函数作为参数、返回新函数的可调用对象。
最标准的装饰器结构是三层闭包(外层接收函数,内层包装逻辑,返回包装函数)。
import functools
import time
# 定义一个计时装饰器
def timer(func):
@functools.wraps(func) # 关键!保留原函数的元数据(如 __name__,文档字符串)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # 执行原函数
end = time.perf_counter()
print(f"{func.__name__} 执行耗时: {end - start:.4f} 秒")
return result
return wrapper
# 使用语法糖 @
@timer
def long_task(n):
time.sleep(n)
return n * 2
long_task(1) # 输出: long_task 执行耗时: 1.0002 秒
如果需要让装饰器本身接收参数(如重复次数、日志级别),需要再包裹一层“装饰器工厂函数”。
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hi():
print("Hi")
say_hi() # 会连续打印 3 次 "Hi"
(调用顺序:repeat(3) 返回 decorator,@decorator 再接收 say_hi)
__call__)如果装饰逻辑复杂、需要维护状态,可以用类来实现(利用 __init__ 接收函数,__call__ 包装逻辑)。
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func) # 手动拷贝元数据
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"调用第 {self.count} 次")
return self.func(*args, **kwargs)
@CountCalls
def test():
pass
test() # 调用第 1 次
test() # 调用第 2 次
| 陷阱 | 解决方案 |
|---|---|
元数据丢失:装饰后的函数 __name__ 和 docstring 会变成 wrapper。 |
必须使用 @functools.wraps(func) 修复。 |
作用域问题:内层 wrapper 中修改外层变量(如 count)会报错。 |
使用 nonlocal 声明(Python 3 专属)。 |
类装饰器与 self:装饰类方法时,wrapper 必须接收 self 参数,否则会报错。 |
在 wrapper(*args, **kwargs) 中透传,self 会自动传递。 |
场景一:重试机制(处理网络抖动)
def retry(max_attempts=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"重试第 {attempt} 次...")
return wrapper
return decorator
场景二:参数校验(类型检查)
def type_check(**types):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 绑定参数名和值
bound = func.__annotations__ # 或使用 inspect.signature
for name, value in {**kwargs}.items():
if name in types and not isinstance(value, types[name]):
raise TypeError(f"{name} 应为 {types[name]}")
return func(*args, **kwargs)
return wrapper
return decorator
@type_check(name=str, age=int)
def greet(name, age):
print(f"{name} is {age}")
场景三:缓存(LRU,Python 3.2+ 内置)
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_fib(n):
if n < 2: return n
return expensive_fib(n-1) + expensive_fib(n-2)
为了让装饰器既可以 @deco 用,也可以 @deco(arg) 用,需要做一个“智能适配”,不过日常开发中分开写更清晰,不建议过度设计。
nonlocal:让嵌套函数修改外层变量成为可能(装饰器计数器必备)。functools.wraps:修复元数据的标准方案(Python 3.2+ 完善)。async def,wrapper 也必须定义为 async def,并用 await func() 调用。def async_timer(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
start = time.perf_counter()
result = await func(*args, **kwargs)
print(f"耗时 {time.perf_counter() - start}")
return result
return wrapper
写装饰器 = 写“返回函数的函数”;用装饰器 = 用 @ 语法糖增强函数。
记住 @functools.wraps 是保命符,理解“闭包”是核心,掌握“带参装饰器”是分水岭。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。