装饰器
装饰器(decorator)是 Python 中一种优雅的语法机制,它能在不修改函数源代码的前提下,给函数增加额外的功能。装饰器本质上是一个接收函数作为参数并返回新函数的高阶函数。它是闭包最经典的应用之一,广泛用于日志、性能监控、权限校验、缓存、重试等场景。
函数是对象
要理解装饰器,首先要彻底理解"函数是一等对象"。函数可以:
- 赋值给变量
- 作为参数传递
- 作为返回值
- 拥有属性
def shout(text):
return text.upper()
# 1. 赋值给变量
yell = shout
print(yell("hello")) # 输出:HELLO
# 2. 函数有属性
print(shout.__name__) # 输出:shout
print(shout.__doc__) # None(未定义时)
# 3. 动态修改函数属性
shout.author = "Alice"
print(shout.author) # 输出:Alice
装饰器本质
装饰器本质是一个函数:它接收一个函数作为参数,返回一个新的函数(通常是包装过的版本)。
def my_decorator(func):
def wrapper():
print("函数执行前")
func()
print("函数执行后")
return wrapper
def say_hello():
print("Hello!")
# 手动装饰
say_hello = my_decorator(say_hello)
say_hello()
输出:
函数执行前
Hello!
函数执行后
@ 语法糖
@decorator 是装饰器的语法糖,等价于 func = decorator(func):
def my_decorator(func):
def wrapper():
print("函数执行前")
func()
print("函数执行后")
return wrapper
@my_decorator # 等价于 say_hello = my_decorator(say_hello)
def say_hello():
print("Hello!")
say_hello()
处理参数和返回值
实用的装饰器需要能处理任意参数和返回值。使用 *args, **kwargs 配合返回原函数结果:
def log_call(func):
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"{func.__name__} 返回 {result!r}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
@log_call
def greet(name, greeting="你好"):
return f"{greeting},{name}"
print(add(3, 5))
# 输出:
# 调用 add((3, 5), {})
# add 返回 8
# 8
print(greet("Alice", greeting="Hi"))
# 输出:
# 调用 greet(('Alice',), {'greeting': 'Hi'})
# greet 返回 'Hi,Alice'
# Hi,Alice
:::tip 通用 wrapper 模板 通用的装饰器 wrapper 模板:
def decorator(func):
def wrapper(*args, **kwargs):
# 装饰前的逻辑
result = func(*args, **kwargs)
# 装饰后的逻辑
return result
return wrapper
:::
带参数装饰器
如果装饰器本身需要接收参数,需要再嵌套一层:外层函数接收参数,返回真正的装饰器。
def repeat(times):
"""让被装饰函数重复执行 times 次"""
def decorator(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hi(name):
print(f"Hi, {name}!")
return name
say_hi("Alice")
# 输出:
# Hi, Alice!
# Hi, Alice!
# Hi, Alice!
执行流程:
repeat(3)返回decoratordecorator(say_hi)返回wrapper- 调用
say_hi("Alice")实际是调用wrapper("Alice")
带参数装饰器模板
def decorator_with_args(arg1, arg2):
def decorator(func):
def wrapper(*args, **kwargs):
# 这里可以使用 arg1, arg2
result = func(*args, **kwargs)
return result
return wrapper
return decorator
functools.wraps
装饰器会替换原函数,导致原函数的元信息(__name__、__doc__ 等)丢失:
def log_call(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log_call
def add(a, b):
"""两个数相加"""
return a + b
print(add.__name__) # 输出:wrapper ← 不是 add!
print(add.__doc__) # 输出:None
使用 functools.wraps 保留原函数元信息:
from functools import wraps
def log_call(func):
@wraps(func) # 复制原函数的元信息到 wrapper
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def add(a, b):
"""两个数相加"""
return a + b
print(add.__name__) # 输出:add ← 正确
print(add.__doc__) # 输出:两个数相加 ← 正确
print(add.__wrapped__) # Python 3.2+ 可访问原函数
:::warning 永远使用 @wraps
编写装饰器时永远记得在 wrapper 上加 @wraps(func),否则:
- 调试时堆栈跟踪中显示的是
wrapper,难以定位 - 文档工具(如 Sphinx、help)失效
- 一些依赖函数元信息的库会出错 :::
类装饰器
装饰器也可以是类,利用 __call__ 方法让实例可调用:
class CountCalls:
"""统计函数调用次数"""
def __init__(self, func):
self.func = func
self.count = 0
wraps(func)(self) # 复制元信息
def __call__(self, *args, **kwargs):
self.count += 1
print(f"第 {self.count} 次调用 {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hi(name):
print(f"Hi, {name}!")
say_hi("Alice") # 第 1 次调用 say_hi / Hi, Alice!
say_hi("Bob") # 第 2 次调用 say_hi / Hi, Bob!
say_hi("Charlie") # 第 3 次调用 say_hi / Hi, Charlie!
print(f"总调用次数:{say_hi.count}") # 输出:总调用次数:3
带参数的类装饰器
class Retry:
"""失败时自动重试"""
def __init__(self, max_retries=3, delay=0):
self.max_retries = max_retries
self.delay = delay
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
last_error = None
for attempt in range(1, self.max_retries + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e
print(f"第 {attempt} 次失败:{e}")
if attempt < self.max_retries and self.delay:
time.sleep(self.delay)
raise last_error
return wrapper
@Retry(max_retries=3, delay=0.1)
def fetch_data(url):
import random
if random.random() < 0.7:
raise ConnectionError(f"无法连接 {url}")
return f"来自 {url} 的数据"
# 可能多次重试后成功
# print(fetch_data("http://example.com"))
内置装饰器
Python 内置了几个常用装饰器:
@staticmethod
将方法标记为静态方法,不需要 self 或 cls 参数:
class MathUtils:
@staticmethod
def square(x):
return x ** 2
@staticmethod
def is_even(n):
return n % 2 == 0
# 既可以通过类调用,也可以通过实例调用
print(MathUtils.square(5)) # 输出:25
print(MathUtils.is_even(4)) # 输出:True
m = MathUtils()
print(m.square(3)) # 输出:9
@classmethod
将方法标记为类方法,第一个参数是类本身(约定为 cls):
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_birth_year(cls, name, birth_year):
"""工厂方法:通过出生年份创建实例"""
import datetime
current_year = datetime.date.today().year
age = current_year - birth_year
return cls(name, age) # 等价于 Person(name, age)
@classmethod
def from_dict(cls, data):
"""工厂方法:从字典创建实例"""
return cls(data["name"], data["age"])
p1 = Person("Alice", 30)
p2 = Person.from_birth_year("Bob", 1995)
p3 = Person.from_dict({"name": "Charlie", "age": 25})
print(p1.name, p1.age) # 输出:Alice 30
print(p2.name, p2.age) # 输出:Bob 31(取决于当前年份)
print(p3.name, p3.age) # 输出:Charlie 25
:::tip 工厂方法的用途
@classmethod 经常用于定义多个构造器。Python 类只能有一个 __init__,需要不同的初始化方式时,用类方法封装为 from_xxx 形式。
:::
@property
将方法转换为只读属性,访问时无需加括号:
class Circle:
def __init__(self, radius):
self._radius = radius # 用下划线表示"受保护"
@property
def radius(self):
"""获取半径"""
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("半径必须为正数")
self._radius = value
@property
def area(self):
"""面积(只读)"""
return 3.14159 * self._radius ** 2
@property
def perimeter(self):
"""周长(只读)"""
return 2 * 3.14159 * self._radius
c = Circle(5)
print(c.radius) # 输出:5 ← 像属性一样访问
print(c.area) # 输出:78.53975
print(c.perimeter) # 输出:31.4159
c.radius = 10 # 调用 setter
print(c.area) # 输出:314.159
# c.area = 100 # AttributeError: can't set attribute
# c.radius = -1 # ValueError: 半径必须为正数
常用第三方 / 标准库装饰器
@functools.cache
Python 3.9+ 引入的无界缓存装饰器,用于记忆化:
from functools import cache
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # 瞬间输出:354224848179261915075
print(fibonacci.cache_info())
# 输出示例:CacheInfo(hits=98, misses=101, maxsize=None, currsize=101)
@functools.lru_cache
带 LRU(最近最少使用)淘汰策略的缓存:
from functools import lru_cache
@lru_cache(maxsize=128) # 最多缓存 128 个结果
def expensive_computation(n):
print(f" 计算 {n}")
return n * n
print(expensive_computation(4)) # 输出: 计算 4 / 16
print(expensive_computation(4)) # 输出:16(命中缓存,不打印)
print(expensive_computation(5)) # 输出: 计算 5 / 25
print(expensive_computation.cache_info())
# 输出:CacheInfo(hits=1, misses=2, maxsize=128, currsize=2)
:::warning 缓存的参数必须可哈希
@cache/@lru_cache 使用字典存储结果,因此参数必须可哈希。列表、字典等不可哈希类型会报错:
@cache
def sum_list(lst):
return sum(lst)
# sum_list([1, 2, 3]) # TypeError: unhashable type: 'list'
解决方法:传入元组而非列表,或使用 functools.cached_property 缓存方法结果。
:::
@dataclasses.dataclass
Python 3.7+ 自动生成 __init__/__repr__/__eq__ 等:
from dataclasses import dataclass, field
@dataclass
class Student:
name: str
age: int
scores: list[float] = field(default_factory=list)
@property
def average(self) -> float:
return sum(self.scores) / len(self.scores) if self.scores else 0
s = Student("Alice", 20, [90, 85, 88])
print(s) # 输出:Student(name='Alice', age=20, scores=[90, 85, 88])
print(s.average) # 输出:87.66666666666667
@contextlib.contextmanager
把生成器函数变成上下文管理器:
from contextlib import contextmanager
@contextmanager
def timer(name: str):
import time
start = time.perf_counter()
try:
yield # with 块中的代码在此执行
finally:
elapsed = time.perf_counter() - start
print(f"[{name}] 耗时 {elapsed:.4f}s")
with timer("数据处理"):
total = sum(i * i for i in range(1_000_000))
print(f"结果:{total}")
# 输出示例:
# 结果:333332833333500000
# [数据处理] 耗时 0.1234s
装饰器叠加
多个装饰器可以叠加使用,执行顺序是从下往上装饰,从上往下执行:
def make_bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def make_italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@make_bold
@make_italic
def hello(name):
return f"Hello, {name}"
# 等价于 hello = make_bold(make_italic(hello))
print(hello("Alice"))
# 输出:<b><i>Hello, Alice</i></b>
:::info 装饰器顺序理解
- 装饰阶段:从离函数最近的装饰器开始,逐层向外包装
hello = make_bold(make_italic(hello)) - 调用阶段:从最外层装饰器开始执行
调用
hello()→ 进入make_bold.wrapper→ 调用func()→ 进入make_italic.wrapper→ 调用真实hello():::
实战:计时器与缓存装饰器
下面综合运用装饰器,实现一个可配置的计时器和缓存装饰器:
from functools import wraps, cache, lru_cache
import time
import math
def timer(label: str = "[耗时]"):
"""带参数的计时装饰器。
Args:
label: 输出标签。
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{label} {func.__name__}: {elapsed:.6f}s")
return result
return wrapper
return decorator
def validate_types(func):
"""类型校验装饰器(运行时检查注解类型)。"""
import inspect
sig = inspect.signature(func)
@wraps(func)
def wrapper(*args, **kwargs):
bound = sig.bind(*args, **kwargs)
for name, value in bound.arguments.items():
param = sig.parameters[name]
annotation = param.annotation
if annotation is inspect.Parameter.empty:
continue
# 处理 X | Y 形式的联合类型
if hasattr(annotation, "__args__"):
allowed = annotation.__args__
else:
allowed = (annotation,)
if not isinstance(value, allowed) and type(None) not in allowed:
raise TypeError(
f"参数 {name} 期望 {annotation}, 实际 {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
# 1. 计时 + 缓存组合使用
@timer("[统计]")
@cache
def sum_of_squares(n: int) -> int:
"""计算 1²+2²+...+n²"""
return sum(i * i for i in range(1, n + 1))
print("--- 计时+缓存 ---")
print(sum_of_squares(1_000_000))
# 第一次调用:实际计算并打印耗时
print(sum_of_squares(1_000_000))
# 第二次调用:缓存命中,几乎不耗时
# 2. 类型校验装饰器
@validate_types
def divide(a: int | float, b: int | float) -> float:
return a / b
print("\n--- 类型校验 ---")
print(divide(10, 4)) # 输出:2.5
try:
divide("10", 4) # 抛出 TypeError
except TypeError as e:
print(f"错误:{e}")
# 3. 缓存斐波那契(带统计)
@lru_cache(maxsize=100)
@timer("[fib]")
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print("\n--- 缓存斐波那契 ---")
print(fib(20))
# 第一次调用会打印每次进入 fib 的耗时
print(fib(20)) # 第二次全部命中缓存,几乎瞬间完成
print(fib.cache_info())
:::tip 装饰器组合的执行顺序
@A @B @C def f 等价于 f = A(B(C(f)))。
- 调用
f()时最先执行A的 wrapper - 真正的
f()在最内层被调用 - 缓存装饰器应该放在最靠近函数的位置,这样它缓存的才是真实结果 :::
小结
- 装饰器本质是接收函数、返回函数的高阶函数,是闭包的经典应用
@decorator是func = decorator(func)的语法糖- 通用 wrapper 模板:
def wrapper(*args, **kwargs): ... - 永远使用
@functools.wraps(func)保留原函数元信息 - 带参数的装饰器需要再嵌套一层:外层接收参数,返回真正的装饰器
- 类装饰器利用
__call__实现,适合需要保存状态的场景 - 内置装饰器:
@staticmethod、@classmethod、@property - 标准库装饰器:
@functools.cache、@lru_cache、@dataclass、@contextmanager - 多个装饰器叠加:从下往上装饰,从上往下执行
下一节将介绍生成器与 yield 关键字——一种用函数形式创建迭代器的优雅机制。