当前位置:首页>python>我被Python装饰器坑了三个月,直到搞懂它的"套路"

我被Python装饰器坑了三个月,直到搞懂它的"套路"

  • 2026-09-09 19:12:53
我被Python装饰器坑了三个月,直到搞懂它的"套路"

先说个真实故事。

去年我接了一个接口日志项目,需要记录每个API的调用时间、请求参数和返回状态。团队里有个"老司机"说用装饰器简单,写个@log就搞定了。我照着文档抄了一遍,运行——直接报错:TypeError: wrapper() missing 1 required positional argument。

我对着屏幕发了半小时呆。文档明明就是这样写的啊?哪里出问题了?

后来排查半天才发现,问题出在我抄的博客太老了,用的是Python 2的写法。到了Python 3,连个像样的报错提示都不给,就扔给你一句"缺少参数"。那个项目差点因此延期。

那次经历让我下定决心把装饰器彻底搞清楚——不是半懂不懂地照抄,而是从原理到实战全部拿下。

今天把我踩过的坑、总结的经验、提炼的套路一次性讲清楚。不堆概念,不念教材,直接上你在代码里会遇到的问题。


装饰器到底是什么?

网上说"装饰器是一种闭包",对,但看完你还是不会写。

换个理解方式。装饰器就是一个函数的"包装膜"。

你去水果店买水果,水果本身没变,但套上包装盒之后:可以保鲜,可以印logo,可以加个防伪标签。装饰器干的就是这个事——在不改原函数代码的前提下,给函数加点额外的功能。

最常见的场景:计时、登录校验、日志记录、性能监控。这些需求如果每个函数都手写一遍,代码会变得又臭又长。装饰器让你只写一次,到处贴标签。

import timedef timer(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@timerdef slow_function():    time.sleep(1)    return "Done"slow_function()

slow_function 本身没有任何改动,贴上@timer之后,自动拥有了计时能力。这就是装饰器的核心价值——增强函数,但不污染函数本身。


它为什么能工作?

很多人会用装饰器,但不知道为什么能这样用。看懂下面这段,你就能自己创造装饰器了:

def slow_function():    return "Done"def timer(func):    def wrapper():        start = time.time()        result = func()        end = time.time()        print(f"耗时 {end - start:.4f} 秒")        return result    return wrapperslow_function = timer(slow_function)  # 这一行就是 @timer 的本质

装饰器的语法糖@timer就是上面那行手动包装的简写。

所以装饰器的本质就是:一个高阶函数,输入是函数,输出还是函数。


踩坑实录:那些让新手崩溃的瞬间

坑一:*args, **kwargs 写错一个符号

def my_decorator(func):    def wrapper(*args, **kwargs):  # 星号必须带        print("开始执行")        return func(*args, **kwargs)  # 这里也是    return wrapper

把*args写成args,或者把**kwargs写成kwargs,Python会报一个很隐晦的错误。是解包操作,func(*args)是把元组里的每个元素拆开传给函数。写func(args)是把整个元组当一个参数传进去,函数签名就对不上了。

自检方法:把装饰器在复杂签名的函数上先测一遍:

@my_decoratordef complex_func(a, b=10, *args, **kwargs):    print(f"a={a}, b={b}, args={args}, kwargs={kwargs}")complex_func(1, 2, 3, 4, name="test")

坑二:装饰器带参数,写成了套娃

def log(level="INFO"):    def decorator(func):        def wrapper(*args, **kwargs):            print(f"[{level}] 调用 {func.__name__}")            return func(*args, **kwargs)        return wrapper    return decorator@log("ERROR")def run():    pass

记忆法:装饰器带参数,就是"工厂的工厂"。

拆解三层各自负责什么:

  • 第一层log("ERROR")负责接收参数,返回一个装饰器
  • 第二层decorator负责接收被装饰的函数,返回包装函数
  • 第三层wrapper才是真正包裹原函数的逻辑

@log("ERROR")等价于run = log("ERROR")(run)。

坑三:装饰器改变了原函数的"身份"

@timerdef add(a, b):    """两数相加,返回结果"""    return a + bprint(add.__name__)   # 输出什么?wrapper  ← 函数名丢了print(add.__doc__)    # 输出什么?None    ← 文档丢了

很多框架会根据函数名和文档字符串来生成路由或校验规则。元数据丢了,轻则路由名变成wrapper,重则整个应用行为异常。

解法只有一行:

import functoolsdef timer(func):    @functools.wraps(func)  # 把原函数的元数据复制过来    def wrapper(*args, **kwargs):        start = time.time()        result = func(*args, **kwargs)        end = time.time()        print(f"耗时 {end - start:.4f} 秒")        return result    return wrapper

加上@functools.wraps(func)这一行,原函数的__name__、__doc__、__annotations__全部保留。这行代码没有理由不写,养成习惯。

坑四:装饰器返回了None

def bad_decorator(func):    def wrapper(*args, **kwargs):        print("做一些操作")        func(*args, **kwargs)  # 没有return!    return wrapper@bad_decoratordef get_data():    return [1, 2, 3]result = get_data()print(result)  # 输出:None ← 返回值丢了

记住:装饰器的wrapper永远要return原函数的执行结果。


实战:写一个带缓存的装饰器

import functools, timedef memoize(ttl=300):    """    带过期时间的内存缓存装饰器    ttl: 缓存有效期(秒),默认5分钟    """    cache = {}    def decorator(func):        @functools.wraps(func)        def wrapper(*args, **kwargs):            key = str(args) + str(sorted(kwargs.items()))            now = time.time()            if key in cache:                val, t, expire = cache[key]                if now - t < expire:                    print(f"命中缓存 (剩余 {expire - (now - t):.0f}s)")                    return val            result = func(*args, **kwargs)            cache[key] = (result, now, ttl)            print("缓存未命中,执行函数")            return result        return wrapper    return decorator@memoize(ttl=10)def fetch_api(user_id, include_history=True):    time.sleep(2)  # 假设每次请求耗时2秒    return {"user_id": user_id, "data": [1, 2, 3]}data1 = fetch_api(1001)  # 耗时2秒data2 = fetch_api(1001)  # 0秒返回,命中缓存data3 = fetch_api(1002)  # 耗时2秒,不同参数

同样参数10秒内重复调用,2秒请求变成0秒。配合Redis,可以做成分布式缓存。


类装饰器:不是函数也可以被装饰

单例模式装饰器

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@singletonclass DatabaseConnection:    def __init__(self):        print("创建数据库连接...")        self.connected = Truedb1 = DatabaseConnection()db2 = DatabaseConnection()db3 = DatabaseConnection()print(db1 is db2 is db3)  # True,只初始化一次

调用三次DatabaseConnection(),但__init__只执行了一次。适合场景:数据库连接池、配置管理器、线程池、日志实例。

路由注册装饰器

class Router:    routes = {}    @classmethod    def route(cls, path):        def decorator(func):            cls.routes[path] = func            print(f"注册路由: {path} -> {func.__name__}")            return func        return decoratorclass MyApp(Router):    @Router.route("/home")    def home(self): return "Welcome home"    @Router.route("/about")    def about(self): return "About us"print(MyApp.routes)

多个装饰器的叠加顺序

@log_decorator@timer_decoratordef process():    time.sleep(0.1)    return "Done"

从下往上依次执行。 可以想象从里到外穿衣服:先穿内衣@timer_decorator,再套外套@log_decorator。

反过来看调用:process()先经过外套log_decorator,再进入内衣timer_decorator,最后到达process本体。这就是为什么内层装饰器加@functools.wraps那么重要——它确保log_decorator看到的是真实的函数名。


真实项目的两个实用场景

场景一:重试机制

import functools, time, logginglogger = logging.getLogger(__name__)def retry(max_attempts=3, delay=1):    def decorator(func):        @functools.wraps(func)        def wrapper(*args, **kwargs):            for attempt in range(max_attempts):                try:                    return func(*args, **kwargs)                except Exception as e:                    if attempt == max_attempts - 1:                        raise                    logger.warning(f"第{attempt+1}次失败,{delay}s后重试: {e}")                    time.sleep(delay)        return wrapper    return decorator@retry(max_attempts=3, delay=2)def call_api(url):    import random    if random.random() < 0.7:        raise ConnectionError("网络超时")    return "成功"

场景二:权限校验

def requires_auth(permission):    def decorator(func):        @functools.wraps(func)        def wrapper(*args, **kwargs):            user = get_current_user()            if permission not in user.permissions:                raise PermissionError(f"需要权限: {permission}")            return func(*args, **kwargs)        return wrapper    return decorator@requires_auth("admin")def delete_user(user_id): pass@requires_auth("finance")def export_report(): pass

权限逻辑和业务逻辑完全分离,改一处不影响另一处。


行动框架

第一步,理解闭包的原理。 装饰器背后就是闭包——函数可以记住定义时的变量。理解了闭包,装饰器就是顺水推舟。

第二步,动手写。 从@timer开始,重点跑通@functools.wraps这一步。然后试带参数的装饰器,再试类装饰器。

第三步,读优质源码。 Flask的@app.route、Django的@login_required、FastAPI的路径装饰器——都是非常高质量的参考。

第四步,警惕过度封装。 同一个逻辑需要在多处使用时,才值得抽取成装饰器。只在某个函数里加一行日志,手动写就行。


别再把装饰器当成高深莫测的高级语法来回避了。它的本质就是一个函数包裹函数的技巧,搞清楚三件事就够了:谁包裹谁、参数怎么传递、元数据怎么保留。

下次再遇到@开头的代码,别跳过。花五分钟读一下,看看它给函数包了什么功能。你会发现,看懂别人的装饰器,比你想象的要简单得多。

最新文章

随机文章