← 返回首页

装饰器:让代码像乐高一样拼装

第一次看见别人代码里写着 @login_required@staticmethod@app.route("/") 的时候,我整个人是懵的。这是啥?宏?注释?某种咒语?

后来才知道,这叫装饰器(Decorator)。它不是魔法,只是把"包装函数"这件事写得特别优雅。今天我们就把它从里到外拆开看一遍。

一、最朴素的场景:我想给函数加点东西

假设你写了一个函数 say_hello,现在想看看它每次跑多久。最直接的做法是改函数本身:

naive.py Python
import time

def say_hello(name):
    start = time.time()
    print(f"Hello, {name}!")
    time.sleep(0.5)  # 假装在干活
    end = time.time()
    print(f"{name} 耗时 {end - start:.2f}s")

say_hello("chatoken")

能跑,但问题也明显:我想测时间的函数不止一个,难道每个都写一遍 start/end?这就开始重复了。

二、第一步:把"包装"抽出来

我们可以写一个 timer 函数,接受一个函数,返回一个"加了一层壳"的新函数:

decorator_step1.py Python
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 耗时 {end - start:.2f}s")
        return result
    return wrapper

def say_hello(name):
    print(f"Hello, {name}!")
    time.sleep(0.5)

# 手动"装饰"
say_hello = timer(say_hello)
say_hello("chatoken")

输出:

Hello, chatoken!
say_hello 耗时 0.50s

看出来了吗?say_hello 本身没动,只是被"包了一层"。这就是装饰器的本质——接受一个函数,返回一个新函数。

三、第二步:用 @ 语法糖,Python 帮你做这件事

既然大家都这么写,Python 就给了个语法糖 @:

decorator_step2.py Python
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 耗时 {end - start:.2f}s")
        return result
    return wrapper

@timer
def say_hello(name):
    print(f"Hello, {name}!")
    time.sleep(0.5)

say_hello("chatoken")

这段代码和第二步那段完全等价,只是看起来更"声明式"了:哦,say_hello 这个函数,是带计时的。

记住这句话: @decorator 就是 func = decorator(func) 的语法糖。没有别的魔法。

四、带参数的装饰器

有时候你想让装饰器"可配置",比如重复执行 N 次,或者设置超时时间。这时候再套一层:

decorator_args.py Python
import time
from functools import wraps

def repeat(times):
    # 这层接配置参数
    def decorator(func):
        @wraps(func)   # 关键!保留原函数元信息
        def wrapper(*args, **kwargs):
            for i in range(times):
                print(f"--- 第 {i+1} 次 ---")
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"Hi, {name}!")

greet("chatoken")

注意 @wraps(func)——它把原函数的 __name____doc__ 等元信息复制过来。否则你的函数名会变成 wrapper,日志、调试都会很尴尬。

五、实战:一个真正的"鉴权"装饰器

Web 开发里常见的 @login_required,其实就是这个套路:

login_required.py Python
from functools import wraps

def login_required(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        user = get_current_user()   # 假设从 session/token 取
        if not user:
            raise PermissionError("请先登录")
        return func(*args, **kwargs)
    return wrapper

@login_required
def delete_user(user_id):
    # 删用户的危险操作
    ...

@login_required
def view_dashboard():
    ...

每个需要登录的函数,加一行 @login_required 就好,不用每个函数里都写一遍 if 判断。整洁、复用、好读。

六、几个容易踩的坑

坑 1:忘记 *args, **kwargs

被装饰的函数签名是多样的,你的 wrapper 一定要接 *args, **kwargs,否则会丢参数。

坑 2:忘记 return result

被装饰的函数如果原本 return 了值,你的 wrapper 不返回,调用方就拿不到结果。

坑 3:装饰类/静态方法时顺序错乱

在类里装饰方法时,记得 @functools.wraps 一定要放在最靠近函数的那一层,否则元信息会错。

七、写在最后

装饰器这个东西,第一眼"我懂",第二眼"我不懂",第三眼"原来这么简单"。

它本质就是"函数是一等公民"这一特性的优雅应用:函数可以当参数传,可以当返回值,可以被替换。理解了这件事,装饰器、偏函数、柯里化、pipeline,全都通了。

所以下次再看到 @xxx,别怕——它就是给你函数"加 buff"。

One-liner takeaway

装饰器 = 接受函数 + 返回新函数。@decorator 只是 func = decorator(func) 的语法糖。写装饰器别忘 *args, **kwargs、返回值、和 functools.wraps