跳转至

198_Python装饰器高级用法

一句话概述

Python装饰器是一种在不修改原函数代码的前提下动态增强函数功能的语法糖,通过@语法将一个函数包裹在另一个函数内,实现日志记录、权限检查、缓存、重试等横切关注点的优雅复用。

核心知识点表格

知识点说明
装饰器本质接受函数作为参数并返回新函数的高阶函数
@语法糖@decorator 等价于 func = decorator(func)
functools.wraps保留原函数的元信息(名称、文档字符串)
带参数装饰器三层嵌套:外层接参数,中层接函数,内层执行
类装饰器用类的__call__方法实现装饰器
装饰器叠加多个装饰器从下到上依次应用
常用场景缓存、日志、计时、重试、权限、类型检查
内置装饰器@property, @staticmethod, @classmethod, @functools.lru_cache

步骤详解

第一步:基本装饰器

白话解释:装饰器就像给函数穿一件"外套"。外套可以在函数执行前后做一些额外的事情,但函数本身的代码不需要改动。

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(n):
    """模拟耗时操作"""
    time.sleep(n)
    return n

slow_function(1)  # 输出: slow_function 耗时: 1.0012s

# 验证元信息保留
print(slow_function.__name__)   # slow_function (不是 wrapper)
print(slow_function.__doc__)    # 模拟耗时操作

第二步:带参数的装饰器

白话解释:有时候装饰器本身也需要配置参数。比如重试装饰器需要指定重试次数。这需要再包一层函数。

def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
    """可配置的重试装饰器"""
    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 exceptions as e:
                    last_exception = e
                    print(f"Attempt {attempt}/{max_attempts} failed: {e}")
                    if attempt < max_attempts:
                        time.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url):
    """从网络获取数据"""
    import random
    if random.random() < 0.7:
        raise ConnectionError("连接失败")
    return f"Data from {url}"

# 调用
try:
    result = fetch_data("https://api.example.com")
    print(result)
except ConnectionError:
    print("所有重试均失败")

第三步:类装饰器

白话解释:除了用函数,也可以用类来实现装饰器。类装饰器通过__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(name):
    return f"Hello, {name}!"

say_hello("Alice")   # say_hello 被调用了 1 次
say_hello("Bob")     # say_hello 被调用了 2 次
print(f"总调用次数: {say_hello.count}")  # 2

# 带参数的类装饰器
class RateLimit:
    """速率限制装饰器"""

    def __init__(self, max_calls=5, period=60):
        self.max_calls = max_calls
        self.period = period
        self.calls = []

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            self.calls = [t for t in self.calls if now - t < self.period]
            if len(self.calls) >= self.max_calls:
                raise RuntimeError(f"Rate limit exceeded: {self.max_calls}/{self.period}s")
            self.calls.append(now)
            return func(*args, **kwargs)
        return wrapper

@RateLimit(max_calls=3, period=10)
def api_call():
    return "API response"

第四步:装饰器叠加与执行顺序

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper

@bold
@italic
def greet(name):
    return f"Hello, {name}"

# 等价于: greet = bold(italic(greet))
# 执行顺序:先italic包装,再bold包装
# 结果: <b><i>Hello, Alice</i></b>
print(greet("Alice"))

第五步:高级装饰器模式

# 1. 缓存装饰器(支持过期时间)
def timed_cache(seconds=300):
    """带过期时间的缓存装饰器"""
    def decorator(func):
        cache = {}

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            key = (args, tuple(sorted(kwargs.items())))
            now = time.time()
            if key in cache:
                result, timestamp = cache[key]
                if now - timestamp < seconds:
                    return result
            result = func(*args, **kwargs)
            cache[key] = (result, now)
            return result

        wrapper.cache_clear = lambda: cache.clear()
        return wrapper
    return decorator

@timed_cache(seconds=60)
def expensive_query(query):
    time.sleep(2)
    return f"Result for {query}"

# 2. 类型检查装饰器
def type_check(**type_hints):
    """参数类型检查装饰器"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            import inspect
            sig = inspect.signature(func)
            bound = sig.bind(*args, **kwargs)
            bound.apply_defaults()

            for param, value in bound.arguments.items():
                if param in type_hints:
                    expected = type_hints[param]
                    if not isinstance(value, expected):
                        raise TypeError(
                            f"参数 {param} 期望 {expected.__name__},"
                            f"实际得到 {type(value).__name__}"
                        )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@type_check(name=str, age=int)
def create_user(name, age):
    return {"name": name, "age": age}

# 3. 单例装饰器
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 DatabaseConnection:
    def __init__(self, host="localhost"):
        self.host = host
        print(f"Creating connection to {host}")

# 4. 装饰器同时支持有参数和无参数调用
def smart_decorator(func=None, *, prefix="LOG"):
    """同时支持 @smart_decorator 和 @smart_decorator(prefix="DEBUG")"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{prefix}] Calling {func.__name__}")
            return func(*args, **kwargs)
        return wrapper

    if func is not None:
        return decorator(func)
    return decorator

@smart_decorator           # 无参数调用
def func1(): pass

@smart_decorator(prefix="DEBUG")  # 有参数调用
def func2(): pass

实战命令速查

# 内置实用装饰器
from functools import lru_cache, cached_property, wraps

@lru_cache(maxsize=128)           # LRU缓存
def fibonacci(n): return n if n < 2 else fibonacci(n-1) + fibonacci(n-2)

class MyClass:
    @cached_property              # 惰性计算属性(只计算一次)
    def heavy_data(self): return expensive_computation()

    @staticmethod                 # 静态方法
    def utility(): pass

    @classmethod                  # 类方法
    def from_config(cls, config): return cls(**config)

# 异步装饰器
def async_timer(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = await func(*args, **kwargs)
        print(f"{func.__name__}: {time.perf_counter()-start:.4f}s")
        return result
    return wrapper

面试常问点

Q1: 装饰器的本质是什么? A: 装饰器本质是一个高阶函数,接受一个函数作为参数,返回一个新函数。@decorator语法糖等价于func = decorator(func)。它利用了Python中函数是一等公民(可以作为参数传递和返回)的特性。

Q2: functools.wraps的作用是什么?不用会怎样? A: wraps将原函数的__name____doc____module__等元信息复制到wrapper函数上。不用的话,装饰后的函数名变成wrapper,文档字符串丢失,调试和introspection困难,pickle也会失败。

Q3: 带参数和不带参数的装饰器在结构上有什么区别? A: 不带参数的装饰器是二层嵌套:decorator(func) -> wrapper。带参数的是三层嵌套:decorator_factory(params) -> decorator(func) -> wrapper。外层接收装饰器参数,中层接收被装饰函数,内层是实际执行逻辑。

Q4: 多个装饰器叠加时的执行顺序是什么? A: 装饰从下到上应用(最靠近函数的先应用),但执行时从上到下进入。例如@A @B def f: 等价于f = A(B(f))。调用时先进入A的wrapper,A内部调用B的wrapper,B内部调用原始f。

Q5: 装饰器能装饰类吗?类能作为装饰器吗? A: 都可以。装饰类的装饰器接受一个类作为参数并返回修改后的类(如添加方法、注册类等)。类作为装饰器需要实现__init__(接受函数)和__call__(包装执行逻辑),还可以利用类的状态管理能力。

易错点

  1. 忘记functools.wraps:导致原函数元信息丢失
  2. 装饰器中不返回函数结果:wrapper内忘记return func(*args, **kwargs)
  3. 带参数装饰器少一层嵌套:三层结构缺一层导致类型错误
  4. 装饰器修改可变默认参数:如列表缓存共享问题
  5. 装饰异步函数忘记await:异步装饰器内需要await func(*args, **kwargs)

补充知识

Python标准库常用装饰器

装饰器模块用途
@lru_cachefunctoolsLRU缓存
@cached_propertyfunctools惰性属性
@total_orderingfunctools自动补全比较方法
@abstractmethodabc抽象方法
@contextmanagercontextlib上下文管理器
@dataclassdataclasses数据类
@propertybuiltins属性访问器

装饰器设计原则

  1. 单一职责:每个装饰器只做一件事
  2. 保留元信息:始终使用functools.wraps
  3. 不改变函数签名:使用args, *kwargs透传
  4. 可组合:支持与其他装饰器叠加使用
  5. 提供escape hatch:允许访问原始函数(如wrapper.__wrapped__