装饰器
问题
Python 装饰器的原理是什么?如何写函数装饰器、类装饰器、带参数的装饰器?
答案
装饰器本质是一个接收函数(或类)作为参数,返回新函数(或类)的可调用对象。@decorator 语法是语法糖。
基础装饰器
import functools
import time
def timer(func):
"""测量函数执行时间"""
@functools.wraps(func) # 保留原函数的元信息
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} 耗时 {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
# @timer 等价于:slow_function = timer(slow_function)
slow_function() # slow_function 耗时 1.0012s
functools.wraps 的重要性
不加 @functools.wraps(func) 时,被装饰函数的 __name__、__doc__ 等元信息会丢失,变成 wrapper 的信息。这会影响调试、日志和文档生成。
带参数的装饰器
需要嵌套三层函数:
import functools
def retry(max_attempts: int = 3, delay: float = 1.0):
"""带参数的重试装饰器"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"第 {attempt} 次失败: {e}")
if attempt < max_attempts:
time.sleep(delay)
raise last_exception
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def unreliable_api():
import random
if random.random() < 0.7:
raise ConnectionError("连接失败")
return "成功"
类装饰器
用类实现装饰器(利用 __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.func.__name__} 被调用了 {self.count} 次")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello() # say_hello 被调用了 1 次 \n Hello!
say_hello() # say_hello 被调用了 2 次 \n Hello!
装饰类的装饰器:
def singleton(cls):
"""单例装饰器"""
instances = {}
@functools.wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Database:
def __init__(self):
print("初始化数据库连接")
db1 = Database() # 初始化数据库连接
db2 = Database() # 不会再次初始化
print(db1 is db2) # True
装饰器叠加
装饰器叠加时,从下往上应用(靠近函数的先执行),从上往下调用:
@decorator_a
@decorator_b
def func():
pass
# 等价于:func = decorator_a(decorator_b(func))
# 调用时:decorator_a 的逻辑先执行,然后 decorator_b,最后 func
常用内置装饰器
| 装饰器 | 用途 |
|---|---|
@property | 将方法变为属性访问 |
@classmethod | 类方法 |
@staticmethod | 静态方法 |
@functools.wraps | 保留被装饰函数元信息 |
@functools.lru_cache | 函数结果缓存(LRU) |
@functools.singledispatch | 单分派泛函数 |
@dataclasses.dataclass | 数据类 |
@abc.abstractmethod | 抽象方法 |
常见面试问题
Q1: 装饰器的执行顺序?
答案:
装饰器在模块导入时立即执行(不需要调用函数):
def register(func):
print(f"注册 {func.__name__}") # 导入模块时就会执行
return func
@register
def my_func():
pass
# 导入模块时输出:注册 my_func
Q2: 如何写一个既可以带参数也可以不带参数的装饰器?
答案:
import functools
def log(func=None, *, level="INFO"):
"""既可以 @log 也可以 @log(level="DEBUG") 使用"""
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
print(f"[{level}] 调用 {fn.__name__}")
return fn(*args, **kwargs)
return wrapper
if func is not None:
# @log 不带参数的情况
return decorator(func)
# @log(level="DEBUG") 带参数的情况
return decorator
@log
def func_a(): pass # ✅
@log(level="DEBUG")
def func_b(): pass # ✅
Q3: 装饰器如何访问被装饰函数的参数?
答案:
import functools
def validate_positive(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 通过 args 和 kwargs 访问参数
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError(f"参数不能为负数: {arg}")
return func(*args, **kwargs)
return wrapper
# 更精确地使用 inspect 获取参数名
import inspect
def log_args(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
print(f"{func.__name__} 参数: {dict(bound.arguments)}")
return func(*args, **kwargs)
return wrapper
@log_args
def add(a: int, b: int = 0) -> int:
return a + b
add(1, b=2) # add 参数: {'a': 1, 'b': 2}
Q4: @property 的原理是什么?
答案:
@property 是一个描述符,将方法调用伪装成属性访问:
class Temperature:
def __init__(self, celsius: float):
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float):
if value < -273.15:
raise ValueError("温度不能低于绝对零度")
self._celsius = value
@property
def fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
t = Temperature(100)
print(t.celsius) # 100 — 像属性一样读
t.celsius = 0 # 像属性一样写(触发 setter)
print(t.fahrenheit) # 32.0 — 只读属性
t.fahrenheit = 100 # ❌ AttributeError: can't set
Q5: 装饰器和闭包的关系?
答案:
装饰器是闭包的典型应用:
- 闭包:内层函数引用外层函数的变量
- 装饰器:闭包的变量恰好是被装饰的函数
def decorator(func): # 外层函数接收 func
def wrapper(): # 内层函数引用 func → 闭包
return func()
return wrapper
没有闭包就没有装饰器,但闭包不一定是装饰器。