前言

你有没有发现,自己写的代码里总有一些"套路"——计时、日志、缓存、权限检查……这些逻辑到处重复,代码越来越臃肿。

今天给大家分享一个 Python 里被严重低估的神器:装饰器(Decorator)。学会它,你会发现很多重复代码都可以一行搞定。

什么是装饰器?

简单说,装饰器就是一个"函数的包装器"——它不改变函数本身的功能,但在函数执行前后加了一些额外行为。

用生活中的例子:手机壳不改变手机的功能,但给它加了防摔和美观的"装饰"。装饰器对函数做的事情,就是手机壳对手机做的事情。

1. 计时装饰器:精准测量函数耗时

性能优化第一步,是知道哪里慢。

import time
import functools

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}秒")
        return result
    return wrapper

# 使用示例
@timer
def process_data(data):
    """模拟数据处理"""
    time.sleep(1.2)
    return [x * 2 for x in data]

result = process_data([1, 2, 3, 4, 5])
# 输出: [process_data] 耗时: 1.2001秒

**小贴士**:用 `time.perf_counter()` 而不是 `time.time()`,前者精度更高,不受系统时间调整影响。

2. 重试装饰器:网络请求不再怕断连

写爬虫或调 API 的时候,网络不稳定很常见。与其每个函数里都写 try-except,不如一个装饰器搞定:

import functools
import time
import random

def retry(max_attempts=3, delay=1, backoff=2, exceptions=(Exception,)):
    """
    自动重试装饰器
    - max_attempts: 最大重试次数
    - delay: 初始等待时间(秒)
    - backoff: 每次重试的等待时间倍数
    - exceptions: 触发重试的异常类型
    """
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            current_delay = delay
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        print(f"[{func.__name__}] 重试{max_attempts}次后仍失败: {e}")
                        raise
                    wait = current_delay + random.uniform(0, 0.5)
                    print(f"[{func.__name__}] 第{attempt}次失败, {wait:.1f}秒后重试...")
                    time.sleep(wait)
                    current_delay *= backoff
        return wrapper
    return decorator

# 使用示例
@retry(max_attempts=3, delay=1, backoff=2, exceptions=(ConnectionError, TimeoutError))
def fetch_api(url):
    """模拟网络请求"""
    if random.random() < 0.5:
        raise ConnectionError("网络连接失败")
    return {"status": "ok", "data": "hello"}

# 多试几次,观察重试行为
for i in range(5):
    fetch_api("https://api.example.com/data")

这里用了指数退避策略,每次等待时间翻倍,是业界公认的最佳实践。

3. 缓存装饰器:重复计算说拜拜

有些函数计算很耗时,但同样的参数结果不会变。用缓存可以极大提升性能:

import functools

def memoize(func):
    """简易缓存装饰器(适用于不可变参数)"""
    cache = {}

    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]

    # 添加手动清除缓存的方法
    wrapper.cache_clear = lambda: cache.clear()
    wrapper.cache_info = lambda: f"缓存条目数: {len(cache)}"
    return wrapper

# 使用示例
@memoize
def fibonacci(n):
    """计算斐波那契数列(递归版,无缓存会非常慢)"""
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 第一次计算,需要递归
print(fibonacci(35))  # 输出: 9227465
print(fibonacci.cache_info())  # 输出: 缓存条目数: 36

# 第二次直接从缓存取,瞬间完成
print(fibonacci(35))  # 瞬间输出: 9227465

**进阶**:Python 标准库自带了 `@functools.lru_cache`,支持 LRU(最近最少使用)淘汰策略,生产环境推荐用这个。

4. 日志装饰器:函数调用一目了然

调试的时候,想知道哪个函数被调用了、传了什么参数、返回了什么:

import functools
import logging

# 配置日志格式
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%H:%M:%S"
)
logger = logging.getLogger(__name__)

def log_calls(func):
    """记录函数调用信息的装饰器"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args_str = ", ".join(repr(a) for a in args)
        kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
        all_args = ", ".join(filter(None, [args_str, kwargs_str]))

        logger.info(f"调用 {func.__name__}({all_args})")
        result = func(*args, **kwargs)
        logger.info(f"{func.__name__} 返回 {result!r}")
        return result
    return wrapper

# 使用示例
@log_calls
def add_user(name, age, role="普通用户"):
    return {"name": name, "age": age, "role": role}

@log_calls
def calculate_discount(price, discount):
    return round(price * discount, 2)

add_user("张三", 25)
add_user("李四", 30, role="管理员")
calculate_discount(199.00, 0.8)

输出类似这样:

08:15:30 [INFO] 调用 add_user('张三', 25)
08:15:30 [INFO] add_user 返回 {'name': '张三', 'age': 25, 'role': '普通用户'}
08:15:31 [INFO] 调用 add_user('李四', 30, role='管理员')
08:15:31 [INFO] add_user 返回 {'name': '李四', 'age': 30, 'role': '管理员'}

5. 权限检查装饰器:一行代码保护敏感操作

在 Web 开发中,权限检查是最常见的横切关注点:

import functools

# 模拟用户角色
USERS = {
    "admin": {"name": "管理员", "role": "admin"},
    "editor": {"name": "编辑", "role": "editor"},
    "guest": {"name": "访客", "role": "guest"},
}

def require_role(*allowed_roles):
    """权限检查装饰器工厂"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(user, *args, **kwargs):
            if user.get("role") not in allowed_roles:
                raise PermissionError(
                    f"权限不足: 需要 {allowed_roles},当前角色 '{user.get('role')}'"
                )
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

# 使用示例
@require_role("admin")
def delete_all_users(user):
    return f"{user['name']} 执行了清空用户操作"

@require_role("admin", "editor")
def publish_article(user, title):
    return f"{user['name']} 发布了文章: {title}"

# 正常执行
print(publish_article(USERS["editor"], "Python装饰器入门"))
# 输出: 编辑 发布了文章: Python装饰器入门

# 权限不足
try:
    delete_all_users(USERS["guest"])
except PermissionError as e:
    print(e)  # 输出: 权限不足: 需要 ('admin',),当前角色 'guest'

总结:装饰器的核心模式

以上5个装饰器其实都遵循同一个模式:

import functools

def my_decorator(func):
    @functools.wraps(func)  # 保留原函数信息(重要!)
    def wrapper(*args, **kwargs):
        # 前置逻辑
        result = func(*args, **kwargs)  # 调用原函数
        # 后置逻辑
        return result
    return wrapper

记住这几点:

1. 永远加 @functools.wraps,否则你的函数会丢失 __name____doc__

2. 装饰器本质上就是高阶函数——接受函数作为参数,返回新函数

3. 带参数的装饰器需要多一层嵌套(装饰器工厂模式)

4. Python 3.10+ 可以用 @timer@timer() 两种写法,后者的定义要多一层

装饰器是 Python 最优雅的设计之一。一旦你习惯用它来处理横切关注点,回头再看到处 copy-paste 的代码,你会庆幸自己学会了这个技能。


你在项目中用过装饰器吗?遇到了什么坑? 欢迎在评论区聊聊你的经验,点赞收藏不迷路,后续会分享更多 Python 实战技巧!

更多推荐