Python 开发技巧 · 高级装饰器 —— 从基础到工业级实战

📌 知识点简介

装饰器是 Python 最优雅的特性之一,但很多开发者停留在 @timer 这种基础用法。今天深入两层:带参数的装饰器类装饰器,封装几个生产环境可直接复用的工具函数。


🔧 核心代码

1. 带参数的装饰器(三层嵌套)

python 复制代码
from functools import wraps
import time
import logging

logger = logging.getLogger(__name__)

def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
    """可配置重试装饰器"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        raise
                    logger.warning(
                        f"{func.__name__} 第 {attempt} 次失败: {e},"
                        f"{delay}s 后重试..."
                    )
                    time.sleep(delay)
            return None  # 不会执行到这里,仅供类型检查
        return wrapper
    return decorator


def timed(unit="s", precision=4):
    """可配置单位的计时装饰器"""
    scale = {"s": 1, "ms": 1000, "μs": 1_000_000}
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            start = time.perf_counter()
            result = func(*args, **kwargs)
            elapsed = (time.perf_counter() - start) * scale.get(unit, 1)
            print(f"[⏱] {func.__name__} 耗时: {elapsed:.{precision}f} {unit}")
            return result
        return wrapper
    return decorator

2. 类装饰器(带状态)

python 复制代码
class CountCalls:
    """统计函数调用次数的类装饰器"""

    def __init__(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)


class Singleton:
    """将任意类变为单例的装饰器"""

    def __init__(self, cls):
        self.cls = cls
        self._instance = None

    def __call__(self, *args, **kwargs):
        if self._instance is None:
            self._instance = self.cls(*args, **kwargs)
        return self._instance

3. 装饰器堆叠 & functools.wraps 的重要性

python 复制代码
def debug_info(func):
    """打印传入/传出参数的调试装饰器"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"[🔍] 调用: {func.__name__}({signature})")
        result = func(*args, **kwargs)
        print(f"[🔍] 返回: {result!r}")
        return result
    return wrapper


# 堆叠使用(执行顺序从下往上,从里到外)
@timed(unit="ms")
@debug_info
def fetch_user(user_id: int) -> dict:
    return {"id": user_id, "name": "Alice"}

🧪 使用示例

python 复制代码
# ---- 重试装饰器 ----
@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url: str) -> str:
    # 模拟不稳定网络请求
    import random
    if random.random() < 0.7:
        raise ConnectionError("网络波动")
    return f"data from {url}"

res = fetch_data("https://api.example.com/data")
print(res)


# ---- 计时装饰器 ----
@timed(unit="ms")
def heavy_compute(n: int) -> int:
    return sum(i * i for i in range(n))

heavy_compute(10_000_000)


# ---- 计数装饰器 ----
@CountCalls
def greet(name: str) -> str:
    return f"Hello, {name}!"

print(greet("Alice"))  # [📊] greet 已调用 1 次
print(greet("Bob"))    # [📊] greet 已调用 2 次


# ---- 单例装饰器 ----
@Singleton
class Database:
    def __init__(self, host: str):
        self.host = host

db1 = Database("localhost")
db2 = Database("remote")  # 不会重新初始化
print(db1 is db2)         # True
print(db1.host)           # "localhost"(首次创建的值)


# ---- 函数元数据保留 ----
@debug_info
def simple_add(a: int, b: int) -> int:
    """将两个数相加"""
    return a + b

# 没有 @wraps 的话,这里会显示 wrapper 的信息
print(f"函数名: {simple_add.__name__}")       # simple_add
print(f"文档: {simple_add.__doc__}")          # 将两个数相加

⚠️ 注意事项 / 避坑指南

坑点 说明 解决
元数据丢失 不加 @wraps(func) 会导致 __name____doc____annotations__ 全被覆盖 每个 wrapper 上务必加 @wraps(func)
多层装饰器顺序 @A @B 等价于 A(B(func)),A 在最外层 把耗时/日志放外面,重试/缓存放里面
类装饰器的 __init__ 签名 class-based 的 __init__ 接收的是函数,容易和类本身的 __init__ 混淆 用函数装饰器做复杂场景,类装饰器做简单计数/单例
装饰器参数类型检查 三层嵌套的装饰器容易把参数写混淆 善用 functools.partial 简化,或是使用 @dataclass 包装配置
性能开销 每个装饰器调用都增加函数调用栈 生产环境适量使用,高频路径避免多层装饰器堆叠

简化带参装饰器的技巧(减少嵌套)

python 复制代码
from functools import partial

def retry(func=None, *, max_attempts=3, delay=1):
    """同时支持 @retry 和 @retry(max_attempts=5) 两种用法"""
    if func is None:
        return partial(retry, max_attempts=max_attempts, delay=delay)

    @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
                time.sleep(delay)
        return None
    return wrapper


# 无括号用法
@retry
def api_call_1(): ...

# 带参数用法
@retry(max_attempts=5, delay=2)
def api_call_2(): ...

📝 总结

装饰器是 Python 的"语法糖核弹"------用得好,代码复用率和可读性暴涨;用不好,层层嵌套让调试想哭。掌握带参装饰器、类装饰器、wraps 保护元数据、以及 partial 简化嵌套这四个要点,日常开发中无论是埋点、缓存、重试还是权限校验,都能信手拈来。

相关推荐
宠友信息1 小时前
Redis 内容社区源码架构优化与即时通讯数据一致性处理
spring boot·后端·websocket·mysql·uni-app
码云之上1 小时前
项目团队从 5 人扩到 15 人,我写了个 CLI 让 IDE 共享 AI 规则
前端·人工智能·后端
掘金者阿豪1 小时前
因为一条LEFT JOIN,差点把客户的数据搞丢了
后端
查不理1 小时前
给 WorkBuddy 接入AI 音乐 MCP,让 Agent 真正生成音乐 demo
后端
山水洛行2 小时前
Loop Engineering:4 种 AI Agent 循环,写好一个能顶你熬夜盯屏一整晚
后端
小爷毛毛_卓寿杰2 小时前
我和 TRAE AI聊了聊,它帮我为五迷老师们做了个城市漫游小程序!
前端·人工智能·后端
SimonKing3 小时前
FreeLLMAPI:24 家厂商、128 个模型、1 个 Key智能切换所有可用的模型
java·后端·程序员
IT_陈寒3 小时前
React useEffect里面写异步,我的状态怎么老丢?
前端·人工智能·后端