跳到主要内容

装饰器

问题

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

没有闭包就没有装饰器,但闭包不一定是装饰器。

相关链接