0 Comments

Python 装饰器:从基础到高级

什么是装饰器?

装饰器(Decorator)是 Python 中最优雅的特性之一。简单来说,装饰器是一个接受函数作为参数并返回新函数的可调用对象——它让你在不修改原函数代码的情况下,为函数添加额外的功能。

def my_decorator(func):
    def wrapper():
        print("函数执行前...")
        func()
        print("函数执行后...")
    return wrapper

@my_decorator
def say_hello():
    print("Hello, World!")

say_hello()
# 输出:
# 函数执行前...
# Hello, World!
# 函数执行后...

@my_decorator 本质上等价于 say_hello = my_decorator(say_hello)。装饰器在 Python 中是一等语法糖,但它的强大远超于此。

为什么要用装饰器?

装饰器解决了一个核心问题:横切关注点(Cross-Cutting Concerns)的分离。以下场景都是装饰器的典型应用:

  • 日志记录:自动记录函数调用、参数和返回值
  • 性能计时:测量函数执行时间
  • 权限校验:检查用户是否有权限执行某操作
  • 缓存:缓存函数返回值,避免重复计算
  • 重试机制:自动重试失败的操作
  • 输入验证:检查函数参数是否符合预期

如果没有装饰器,这些逻辑就会散布在业务代码中,导致难以维护。

基础装饰器模式

带参数的函数

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"调用 {func.__name__},参数: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"返回: {result}")
        return result
    return wrapper

@log_call
def add(a, b):
    return a + b

add(3, 5)
# 调用 add,参数: (3, 5), {}
# 返回: 8

关键点:使用 *args**kwargs 让包装函数能接受任意参数,然后原封不动地传递给原函数。同时记得 return result——这是新手最容易犯的错误。

使用 functools.wraps 保留元信息

直接使用装饰器会导致原函数的元信息丢失:

def naive_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@naive_decorator
def greet():
    """返回问候语"""
    return "Hi"

print(greet.__name__)  # wrapper  (不是 greet!)
print(greet.__doc__)   # None     (文档丢了!)

解决方案:

from functools import wraps

def better_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@better_decorator
def greet():
    """返回问候语"""
    return "Hi"

print(greet.__name__)  # greet ✓
print(greet.__doc__)   # 返回问候语 ✓

每条黄金法则:写装饰器时始终使用 @wraps(func)

进阶:带参数的装饰器

有时我们需要装饰器本身接受参数。例如,一个重试装饰器需要知道重试次数:

from functools import wraps
import time

def retry(max_attempts=3, delay=1):
    """重试装饰器工厂"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    print(f"第 {attempt + 1} 次失败: {e}")
                    if attempt < max_attempts - 1:
                        time.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def fetch_data(url):
    # 模拟可能失败的网络请求
    import random
    if random.random() < 0.7:
        raise ConnectionError("网络超时")
    return {"data": "success"}

这里有三层嵌套retry() 返回 decoratordecorator 返回 wrapper。理解这个「俄罗斯套娃」结构是掌握装饰器的关键。

类装饰器

装饰器不限于函数,类也可以作为装饰器。使用 __call__ 方法:

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)

@CountCalls
def process():
    pass

process()  # process 已被调用 1 次
process()  # process 已被调用 2 次

类装饰器特别适合需要维护状态的场景,因为实例属性能自然地存储状态而不用闭包。

实战示例:构建实用的装饰器

1. 缓存装饰器(Memoization)

from functools import wraps

def memoize(func):
    cache = {}
    @wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(100))  # 354224848179261915075 — 瞬间算出

没有缓存时,计算 fibonacci(100) 需要天文数字级别的递归调用。有了 @memoize,每个值只算一次,时间复杂度从 O(2^n) 降到 O(n)。

2. 权限校验装饰器

def require_role(role):
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if user.get("role") != role:
                raise PermissionError(f"需要 {role} 权限")
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

@require_role("admin")
def delete_user(admin_user, target_id):
    return f"用户 {target_id} 已删除"

# 使用示例
admin = {"name": "Alice", "role": "admin"}
guest = {"name": "Bob", "role": "guest"}

print(delete_user(admin, 42))  # 用户 42 已删除
# delete_user(guest, 42)       # PermissionError: 需要 admin 权限

3. 计时与日志装饰器

import time
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def log_execution(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            result = func(*args, **kwargs)
            elapsed = time.perf_counter() - start
            logger.info(
                f"{func.__name__} 完成,耗时 {elapsed:.4f}s"
            )
            return result
        except Exception as e:
            elapsed = time.perf_counter() - start
            logger.error(
                f"{func.__name__} 失败,耗时 {elapsed:.4f}s,错误: {e}"
            )
            raise
    return wrapper

@log_execution
def heavy_computation(n):
    total = sum(i * i for i in range(n))
    return total

heavy_computation(10_000_000)
# INFO:__main__:heavy_computation 完成,耗时 0.8234s

多个装饰器的叠加顺序

装饰器可以叠加,执行顺序是从下到上(离函数越近越先执行)

@decorator_a
@decorator_b
def my_func():
    pass

等价于 my_func = decorator_a(decorator_b(my_func))decorator_b 先包装,decorator_a 后包装。执行时则相反——就像洋葱,外层先进,内层后出。

总结

层级 结构 使用场景
基础装饰器 func → wrapper 简单日志、计时
带参数装饰器 params → decorator → wrapper 可配置的重试、权限
类装饰器 __init__ + __call__ 需要维护状态的场景

装饰器的精髓在于 AOP(面向切面编程) 思想:将横切关注点从业务逻辑中抽离,让代码更干净、更可测试、更可维护。

掌握了装饰器,你的 Python 代码将从「能跑就行」进化到「优雅且健壮」。动手试一下吧——把项目中重复的日志、计时、权限检查逻辑提炼成装饰器,你会立刻感受到代码质量的飞跃。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注