Python 装饰器超详细讲解:从“看不懂”到“会使用”,一篇吃透

先把结论放在前面:装饰器其实就是一个"能接收函数、还能返回函数"的闭包。它像给函数套上一层"滤镜",不改原函数一行代码,却能悄悄加功能。你把这句话吃透了,装饰器的雾就散了一半。

这篇文章我不背书、不掉书袋,按"能用、好用、少踩坑"的顺序来。你会看到清晰的心智模型、贴合实战的范式、以及一堆能直接拷走的代码模板。认真读完,你能做到两件事: 1)看懂别人写的装饰器 ;2)按需自己写一个


一、装饰器到底在解决什么问题?

写代码最怕两件事:重复侵入

  • 重复:同样的日志/鉴权/计时逻辑,N 个函数都要写一遍;
  • 侵入:为加这点逻辑,你得把每个函数的源码都打开改一遍,风险大、回滚难。

装饰器的使命就是:把"通用增强"与"业务逻辑"分离。 它像给函数穿外套:今天穿"日志外套",明天换"缓存外套",业务本体不动分毫。这就是"非侵入式扩展"的威力。


二、两把钥匙:作用域 & 闭包(超短复习版)

1)作用域:变量各有地盘

函数里的变量默认只在函数里生效(局部),文件级别的是全局。局部对全局有"隔离墙"。你可以用 global 硬撞墙,但易出事故,不推荐。

2)闭包:把"局部变量"搬出函数的正规方法

闭包是函数里再定义函数 ,内层函数"记住"了外层函数的变量。外层把内层函数返回出去,你在外面调用内层函数,就能"间接操作"那块原本局部的变量。这就是装饰器的底层基建。

心里装个模型:装饰器 = 以函数为参数的闭包 + @ 语法糖


三、从 0 到 1:把闭包"拧成"装饰器

先分享一个完全可运行的"三步走"心法:

Step1:写一个"外层函数"接收被增强的函数 Step2:在外层里写一个 wrapper,负责加料(日志/校验/计时...) Step3:返回 wrapper,并用 @ 套在目标函数上

语法糖记忆法:@decorator 等价于 func = decorator(func)


四、5 个高频实战场景(可直接落地)

1)日志记录:谁在什么时候、带着什么参数,调用了哪个函数?

python 复制代码
import functools
import time

def log_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        ts = time.strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{ts}] CALL {func.__name__} args={args} kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"[{ts}] RET  {func.__name__} -> {result}")
        return result
    return wrapper

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

add(3, 5)

要点:functools.wraps 必须用,保留原函数的名字、文档、注解,不然调试、堆栈、工具链都不友好(后面详讲)。

2)性能测试:测函数执行时间(原文代码,原样保留

python 复制代码
import time
import functools
def timing_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()  # 开始时间
        result = func(*args, **kwargs)
        end = time.time()    # 结束时间
        print(f"{func.__name__}执行时间:{end - start:.4f}秒")
        return result
    return wrapper
# 测试一个耗时函数
@timing_decorator
def slow_func():
    time.sleep(2)  # 模拟耗时操作
slow_func()  # 输出:slow_func执行时间:2.0005秒

3)权限验证:先验权再执行函数(原文代码,原样保留

python 复制代码
import functools
# 带参数的装饰器:指定需要的权限
def permission_required(permission):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 假设user_has_permission是实际的权限检查函数
            if not user_has_permission(permission):
                raise Exception("没有权限执行此操作!")
            return func(*args, **kwargs)
        return wrapper
    return decorator
# 假设的权限检查逻辑
def user_has_permission(permission):
    return permission == "admin"  # 模拟当前用户是管理员
# 只有管理员能删除用户
@permission_required("admin")
def delete_user(user_id):
    print(f"用户{user_id}已删除")
delete_user(123)  # 正常执行;如果权限不是admin,会抛异常

4)参数校验:入口就把坏参数挡在门外(原文代码,原样保留

python 复制代码
import functools
def positive_check(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 校验位置参数
        for arg in args:
            if arg <= 0:
                raise ValueError(f"参数{arg}必须是正数")
        # 校验关键字参数
        for val in kwargs.values():
            if val <= 0:
                raise ValueError(f"参数{val}必须是正数")
        return func(*args, **kwargs)
    return wrapper
@positive_check
def multiply(a, b):
    return a * b
multiply(2, 3)  # 正常返回6
multiply(-1, 3)  # 抛异常:参数-1必须是正数

5)结果缓存:重复调用不重复计算(原文代码,原样保留

python 复制代码
import functools
def cache_decorator(func):
    cache = {}  # 缓存字典,存参数和结果的对应关系
    @functools.wraps(func)
    def wrapper(*args):
        if args in cache:
            print(f"使用缓存:{args}")
            return cache[args]
        result = func(*args)
        cache[args] = result  # 存入缓存
        print(f"缓存新增:{args}")
        return result
    return wrapper
# 模拟复杂计算(比如斐波那契数列)
@cache_decorator
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)
fib(5)  # 第一次计算,缓存新增
fib(5)  # 第二次调用,直接用缓存

生产里可直接用官方的 functools.lru_cache(maxsize=...),一行搞定,还更稳。


五、带参数的装饰器:再加一层"工厂"

你可能注意到"权限验证"示例,外面还包了一层 permission_required(permission)。这就是装饰器工厂:先接配置,再返回真正装饰器。

通俗点:第一层接参数,第二层接函数,第三层是 wrapper 执行增强 。 记忆口诀:三层套娃


六、多个装饰器叠加:执行顺序怎么记?

python 复制代码
def A(func):
    def w(*a, **k):
        print("A-前")
        r = func(*a, **k)
        print("A-后")
        return r
    return w

def B(func):
    def w(*a, **k):
        print("B-前")
        r = func(*a, **k)
        print("B-后")
        return r
    return w

@A
@B
def work():
    print("干活")

work()

记忆法 :离函数最近的装饰器(这里是 B)先包住函数,最外层(A)最后包。调用时"外到内",日志打印是:A-前 → B-前 → 干活 → B-后 → A-后

另一个角度:装饰器在定义阶段 自上而下应用,但执行阶段由外到内。


七、必须会的 8 个技巧(真·避坑指南)

1)永远用 @functools.wraps(func) 不然 __name____doc____annotations____wrapped__ 都丢失,调试困难,配合工具(如单测、依赖注入)会出妖。

2)参数兜底:*args, **kwargs 别偷懒 装饰器要像"透明胶带",不改变被装饰函数的签名行为。除非你明确要改。

3)闭包变量的"晚绑定"陷阱 在循环里生成多个装饰器或 wrapper 时,注意把循环变量"固化"到默认参数里,否则全都引用最后一个值。

python 复制代码
funcs = []
for i in range(3):
    def deco(func, i=i):   # 用默认参数绑定当前 i
        def w(*a, **k):
            print(i)
            return func(*a, **k)
        return w
    @deco
    def f(): pass
    funcs.append(f)
# 正确打印 0、1、2

4)装饰 async def 时要用异步 wrapper 否则你会得到一个"没被 await 的协程"。示例:

python 复制代码
import functools
import asyncio

def async_timing(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):  # 注意 async
        start = asyncio.get_event_loop().time()
        result = await func(*args, **kwargs)
        end = asyncio.get_event_loop().time()
        print(f"{func.__name__}耗时:{end - start:.4f}s")
        return result
    return wrapper

5)类方法的 self/cls 别弄丢 给实例方法加装饰器时,签名里要接住 self;给类方法(@classmethod)加装饰器要接住 cls

6)缓存要考虑"可哈希性"和"过期策略" 用自制 dict 缓存时,只能缓存可哈希的参数(如 tupleint)。生产里优先 lru_cache,或引入 TTL/清理策略。

7)异常要么上抛、要么统一处理 别在装饰器里偷偷吞异常。吞了等于"把锅焊死",查不出问题。

8)别把装饰器写成黑魔法 目标是读得懂、测得了、改得起。能用普通函数抽取就别强行上装饰器;复杂逻辑建议用类装饰器中间件模式


八、类装饰器:当你需要"带状态"的增强

当装饰器需要维护状态(比如统计调用次数、限流),用类更直观:

python 复制代码
import functools
class CountCalls:
    def __init__(self, func):
        functools.wraps(func)(self)        # 把元数据贴到实例上
        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 greet(name):
    return f"Hi, {name}"

greet("Alice")
greet("Bob")

优点:语义清晰,状态天然落在实例属性里,测试也更容易。


九、标准库里"现成能用"的装饰器

  • functools.lru_cache(maxsize=128):函数级缓存,线程安全、性能好;
  • functools.cached_property:把耗时计算变成一次性属性(类上用);
  • functools.singledispatch:函数"泛型",按参数类型分发;
  • dataclasses.dataclass:类转数据类,自动生成 __init____repr__ 等;
  • @property / @staticmethod / @classmethod:面向对象三件套。

结论:能用现成的别自己造轮子,稳定、可读、踩过坑。


十、类型提示进阶(可选):优雅保留签名

想让装饰器对类型检查更友好,可以用 typing.ParamSpecTypeVar

python 复制代码
from typing import TypeVar, ParamSpec, Callable
import functools

P = ParamSpec("P")
R = TypeVar("R")

def logged(func: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print("call", func.__name__)
        return func(*args, **kwargs)
    return wrapper

这段的收益是:被装饰后的函数,在 IDE/静态检查里仍保留原始签名与返回值类型,开发体验更丝滑。


十一、生产可复用模板(复制即用)

通用装饰器模板(同步)

python 复制代码
import functools
def decorator_name(*dargs, **dkwargs):
    """装饰器工厂,可带配置;不带配置时也兼容"""
    def _decorator(func):
        @functools.wraps(func)
        def _wrapper(*args, **kwargs):
            # 前置逻辑(可用 dargs/dkwargs 配置)
            result = func(*args, **kwargs)
            # 后置逻辑
            return result
        return _wrapper
    # 兼容无参写法:@decorator_name
    if callable(dargs[0]) if dargs else False:
        func = dargs[0]
        return _decorator(func)
    return _decorator

通用装饰器模板(异步)

python 复制代码
import functools
import asyncio

def async_decorator(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        # 前置
        res = await func(*args, **kwargs)
        # 后置
        return res
    return wrapper

十二、装饰器 vs 继承 vs 组合:怎么选?

  • 装饰器:不改业务函数源码,只想加"切面"能力(日志、鉴权、计时、缓存、限流、重试)→ 选它;
  • 继承/组合:对象级别的大改造、复杂状态机、跨多方法的协作 → 选类。
  • 中间件:有明确的"请求-响应"链路(Web/爬虫/数据流),统一处理更自然。

一句话:函数增强用装饰器,复杂行为用类或中间件


十三、10 个常见坑(配对照答案)

  1. 忘记 return wrapper → 装饰后函数变成 None
  2. 忘记 *args, **kwargs → 一调用非空参数就炸。
  3. 不加 @wraps → 名字/文档丢失,inspect/IDE 体验差。
  4. 在循环里闭包捕获同一个变量 → 用默认参数固化。
  5. 装饰器里偷偷吞异常 → 线上排障地狱。
  6. 同时装同步/异步函数 → 分开写两个版本,或检测 iscoroutinefunction
  7. 缓存没考虑"不可哈希参数" → 用 lru_cache 或自定义 key。
  8. 叠加装饰器没想清顺序 → 把"最外层切面"写在最上面。
  9. 在装饰器里修改全局可变对象 → 引发难以定位的副作用。
  10. 在类方法上装饰器忘记接 self/cls → 位置参数错位。

十四、给新手的实话

  • 不用急着精通:先会看懂,会使用几个模板;真正的理解来自"用过"。
  • 日常多复用:标准库的装饰器已经非常强大,优先用。
  • 复杂度上升就"上岸" :当装饰器逻辑越来越厚重,果断切到类或中间件,别强撑。

十五、速查清单(收藏版)

  • 记忆模型:装饰器 = 闭包 + 接受函数 + 返回函数 + @ 语法糖
  • 三层套娃:配置层 → 装饰器层 → 包装层(wrapper)
  • 必备工具:functools.wrapslru_cachecached_propertysingledispatch
  • 顺序规则:定义时自上而下应用,执行时由外到内
  • 同步/异步:不要混;分别写,或自动分流
  • 最佳实践:无侵入、可读性优先、异常透明、状态最小化

结尾彩蛋:把"闭包→装饰器"的桥,搭得更直白

还记得我们前面说"装饰器其实就是闭包吗"?看这两句你就彻底通了:

  • 闭包版调用:enhanced = outer(add); enhanced(1, 2)
  • 语法糖版:在 add 上方写 @outer,然后直接 add(1, 2)

差别只有"写法",本质完全一致

相关推荐
我要成为前端高手几秒前
给不支持摇树的三方库(phaser) tree-shake?
前端·javascript
秋难降10 分钟前
优雅的代码是什么样的?🫣
java·python·代码规范
Noxi_lumors16 分钟前
VITE BALABALA require balabla not supported
前端·vite
周胜218 分钟前
node-sass
前端
aloha_24 分钟前
Windows 系统中,杀死占用某个端口(如 8080)的进程
前端
牧野星辰25 分钟前
让el-table长个小脑袋,记住我的滚动位置
前端·javascript·element
code_YuJun27 分钟前
React 常见问题
前端
_Congratulate29 分钟前
vue3高德地图api整合封装(自定义撒点、轨迹等)
前端·javascript·vue.js
二闹40 分钟前
聊天怕被老板发现?摩斯密码来帮你
后端·python
mit6.8241 小时前
[RestGPT] OpenAPI规范(OAS)
人工智能·python