Python 装饰器详解:从原理到实战的 5 个经典用法

站长 2026-09-29 0 约 2 分钟 437 字
#Python#装饰器#闭包#进阶

装饰器解决什么问题

先看一个真实痛点。项目里有一堆函数,现在要给每一个都加上「记录执行时间」的功能:

def get_user_list():
    # ... 业务逻辑
    pass

def create_order():
    # ... 业务逻辑
    pass

笨办法:每个函数内部都加两行计时代码。20 个函数改 20 处,以后要加「记录日志」再改 20 处——烦且容易漏。

装饰器的思路:写一个「包装机」,把原函数包一层,在不改原函数代码的前提下增加新功能。

@timer   # 这一行就是装饰器,自动给函数加上计时功能
def get_user_list():
    pass

原理:装饰器就是「函数套函数」

理解装饰器只需要知道两件事:

  1. 函数可以当参数传递,也可以当返回值返回
  2. 闭包:内部函数可以记住外部函数的变量

最简装饰器长这样:

import functools
import time

def timer(func):
    """装饰器:统计函数执行时间"""
    @functools.wraps(func)  # 保留原函数的名字和文档,好习惯
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)   # 执行原函数
        print(f"{func.__name__} 耗时 {time.time() - start:.3f} 秒")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)

slow_function()  # 输出:slow_function 耗时 1.002 秒

@timer 这行代码等价于 slow_function = timer(slow_function)——把原函数传进 timer,用返回的 wrapper 替换掉原来的名字。之后每次调用 slow_function(),实际执行的是包装后的 wrapper。

实战 1:日志记录

import functools
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")

def log_calls(func):
    """记录函数调用的参数和返回值"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"调用 {func.__name__},args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        logging.info(f"{func.__name__} 返回 {result}")
        return result
    return wrapper

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

add(3, 5)
# 输出两行日志:调用 add,args=(3, 5), kwargs={} / add 返回 8

实战 2:缓存(记忆化)

递归算斐波那契数列,不加缓存时 fib(35) 要算好几秒,因为重复计算了大量子问题。加个缓存装饰器:

import functools

def cache(func):
    """把算过的结果存起来,相同参数直接返回"""
    saved = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in saved:
            saved[args] = func(*args)
        return saved[args]
    return wrapper

@cache
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(100))  # 秒出结果

其实不用自己写:标准库 functools.lru_cache 就是现成的缓存装饰器,还带容量淘汰策略:

from functools import lru_cache

@lru_cache(maxsize=128)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

实战 3:权限校验(Web 开发天天用)

import functools

# 模拟当前登录用户
current_user = {"name": "张三", "role": "admin"}

def require_role(role):
    """带参数的装饰器:要求特定角色才能访问"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if current_user.get("role") != role:
                raise PermissionError(f"需要 {role} 权限")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_role("admin")
def delete_all_data():
    print("数据已删除")

delete_all_data()  # 正常执行
# 把 current_user["role"] 改成 "guest" 再调用 → PermissionError: 需要 admin 权限

注意这里变成了三层函数嵌套:require_role("admin") 先拿到参数返回装饰器,装饰器再接收函数。规律:装饰器带参数,就多套一层。

Flask 里的 @app.route("/api")、登录校验 @login_required,全都是这个模式。

实战 4:重试机制

调用不稳定的网络接口时特别好用:

import functools
import time

def retry(times=3, delay=1):
    """失败自动重试"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"第 {attempt} 次失败:{e}")
                    if attempt == times:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=3, delay=2)
def call_unstable_api():
    import random
    if random.random() < 0.7:   # 70% 概率失败,模拟不稳定接口
        raise ConnectionError("网络超时")
    return "成功"

实战 5:函数注册表(进阶但极其有用)

插件系统、命令分发器的标准写法:

COMMANDS = {}  # 命令注册表

def register(name):
    """把函数注册到命令表"""
    def decorator(func):
        COMMANDS[name] = func
        return func   # 不改函数行为,只登记
    return decorator

@register("hello")
def cmd_hello():
    print("你好!")

@register("bye")
def cmd_bye():
    print("再见!")

# 根据用户输入动态调用
user_input = "hello"
COMMANDS[user_input]()   # 输出:你好!

新增命令只需要加新函数并装饰,调用处的分发逻辑一行不用改——这就是「开放封闭原则」。

避坑指南

1. 别忘了 @functools.wraps

不加的话,被装饰函数的 __name__、__doc__ 都会变成 wrapper 的,调试和文档生成时会错乱。

2. 装饰顺序很重要

@timer
@log_calls
def f(): ...
# 等价于 f = timer(log_calls(f))
# 执行顺序:先 timer 的包装,进去后先记日志再执行,出来后计时

3. 装饰器在导入时就执行

@register 这类装饰器在模块导入时就跑完了,不是调用时才跑。利用这个特性做自动注册,也要注意别在装饰器里写耗时操作拖慢启动。

写在最后

装饰器的学习路径:先会用 @lru_cache、@app.route 这些现成的 → 再读懂别人的装饰器 → 最后才会写自己的。别反过来。先把本文的 5 个例子各敲一遍,用到的场景出现时,你自然会长出「这里该用装饰器」的直觉。

评论 (0)

我的头像

还没有评论,快来抢沙发吧~