Python 装饰器:给函数穿件"外套",到底难在哪?

如果你学过一段时间 Python,大概率在某段代码里撞见过一个 @ 符号,孤零零地蹲在函数定义上方,像这样:

python 复制代码
@login_required
def dashboard(request):
    ...

你大概知道这叫"装饰器",也大概知道它"在函数执行前后做点什么"。但如果有人让你从零手写一个装饰器,你可能就会开始挠头了。

别慌,今天咱们就用大白话,把装饰器这件事彻底聊明白。


先忘掉装饰器,聊一个前置知识

在 Python 里,函数是"一等公民"。

这句话听着很唬人,其实意思特别朴素:函数和整数、字符串、列表一样,是个普通对象。你可以把它赋给变量,可以把它塞进列表,也可以把它当成参数传给另一个函数。

来,看个最简单的例子:

python 复制代码
def say_hello():
    print("你好呀!")

greet = say_hello  # 把函数赋给另一个名字
greet()            # 输出:你好呀!

你看,say_hello 这个函数被我"贴了个新标签"叫 greet,照样能调用。

再进一步,函数也能当参数传:

python 复制代码
def do_twice(func):
    func()
    func()

do_twice(say_hello)
# 输出:
# 你好呀!
# 你好呀!

到这里你应该觉得毫无压力。如果有,那就再看两遍------因为装饰器的全部秘密,就藏在这两件事里。


装饰器的本质:一个"吃函数、吐函数"的函数

好,现在我们可以给装饰器下个定义了------

装饰器就是一个函数,它接收一个函数作为参数,返回一个新的函数。

就这么简单。真的。

我换个接地气的说法:你有一个函数 A,装饰器"咬"进去,"吐"出来一个功能更丰富的函数 B。你还是叫它 A,但它已经不一样了。

就像你给手机贴了个膜------手机还是那个手机,但它多了层保护。装饰器就是给函数"贴膜"。

来,我们手写一个:

python 复制代码
def shout_decorator(func):
    def wrapper():
        result = func()       # 先调用原来的函数
        return result.upper() # 把结果变成大写
    return wrapper            # 返回新函数

def greet():
    return "hello"

# 手动装饰
decorated_greet = shout_decorator(greet)
print(decorated_greet())  # 输出:HELLO

发生了什么?shout_decorator 吃掉了 greet,吐出来一个 wrapper。这个 wrapper 做的事是:先调用原来的 greet 拿到结果,再把结果变大写。

原始的 greet() 返回 "hello",装饰后的版本返回 "HELLO"。函数还是那个函数名,但行为已经悄悄升级了。


@ 语法糖:让代码好看一点

上面那个 decorated_greet = shout_decorator(greet) 写法虽然清楚,但有点啰嗦。Python 的设计者说:咱们用 @ 来简化一下吧。

于是你可以这么写:

python 复制代码
@shout_decorator
def greet():
    return "hello"

print(greet())  # 输出:HELLO

@shout_decorator 放在函数定义上方,等价于 greet = shout_decorator(greet) 就这么一条规则,没有魔法。

很多人第一次看到 @ 觉得它是什么高深的语法,其实它就是个"快捷方式",帮你省掉一行赋值语句而已。


等等,如果原函数有参数呢?

上面的 greet() 没有参数,所以 wrapper() 也没有参数。但如果原函数有参数呢?

python 复制代码
@shout_decorator
def greet(name):
    return f"hello, {name}"

greet("Alice")  # 报错!wrapper() 不接受参数

炸了。因为 wrapper() 的定义是 def wrapper():,它不接受任何参数。但装饰之后,greet 其实指向的是 wrapper,你传参数就传不进去。

解决办法很直接------让 wrapper 也能接收任意参数:

python 复制代码
def shout_decorator(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

*args**kwargs 这对搭档,意思是"不管传什么参数,我全接着,原封不动转给原函数"。这是写装饰器的标准姿势,几乎所有正经的装饰器都这么干。


一个真正有用的例子:计时器

光变大写没什么意思,来个实用的。假设你想知道每个函数跑了多久:

python 复制代码
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"[{func.__name__}] 耗时 {end - start:.4f} 秒")
        return result
    return wrapper

@timer
def slow_add(a, b):
    time.sleep(1)
    return a + b

slow_add(1, 2)
# 输出:[slow_add] 耗时 1.0012 秒

漂亮。你写业务逻辑的人完全不用操心计时的事,加一行 @timer 就搞定了。想给别的函数计时?照样 @timer 一贴就行。

这就是装饰器的威力:把跟业务无关的"横切关注点"(日志、计时、鉴权、缓存......)抽出来,写成装饰器,让业务代码干干净净。


带参数的装饰器:给装饰器也"装饰"一下

有时候你想让装饰器更灵活一点。比如计时器,你希望可以选择是否打印日志:

python 复制代码
def timer(verbose=True):
    def decorator(func):
        def wrapper(*args, **kwargs):
            start = time.perf_counter()
            result = func(*args, **kwargs)
            end = time.perf_counter()
            if verbose:
                print(f"[{func.__name__}] 耗时 {end - start:.4f} 秒")
            return result
        return wrapper
    return decorator

注意看,这里有三层嵌套。别晕,一层一层拆:

  • timer(verbose=True) 返回 decorator
  • decorator(func) 返回 wrapper
  • wrapper(*args, **kwargs) 才是真正执行的那个函数

用的时候:

python 复制代码
@timer(verbose=False)
def fast_multiply(a, b):
    return a * b

记住一个规律:装饰器外面多一层括号和参数,就意味着多了一层函数嵌套。 参数是给最外面那层函数传的,不是给 decoratorwrapper 传的。


functools.wraps:一个你必须知道的"善后"工作

到这里你可能会想:装饰器已经够好了,还有什么问题吗?

有一个小坑。来看:

python 复制代码
@timer
def slow_add(a, b):
    """两数相加(带计时版)"""
    return a + b

print(slow_add.__name__)  # 输出:wrapper
print(slow_add.__doc__)   # 输出:None

什么?函数名变成了 wrapper?文档字符串也没了?

这是因为装饰之后,slow_add 指向的其实是 wrapper 函数。wrapper 的名字就是 "wrapper",文档字符串自然是空的。

这在日常开发中可能不是什么大事,但如果你用 help(slow_add) 查看帮助,或者用某些依赖函数元信息的框架,就会出问题。

Python 标准库给你准备了修复方案------functools.wraps

python 复制代码
from functools import wraps

def timer(func):
    @wraps(func)  # 加这一行就行
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"[{func.__name__}] 耗时 {end - start:.4f} 秒")
        return result
    return wrapper

加了 @wraps(func) 之后,wrapper 会自动继承原函数的 __name____doc__ 等元信息。这是写装饰器的"行业标准"------你写的每一个装饰器,都应该在 wrapper 上面加 @wraps(func) 养成习惯,不亏。


再来几个真实场景,巩固一下

场景一:登录校验

Web 开发里最常见的装饰器用法:

python 复制代码
def login_required(func):
    @wraps(func)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated:
            return redirect("/login")
        return func(request, *args, **kwargs)
    return wrapper

@login_required
def my_profile(request):
    return render("profile.html")

没登录?直接跳登录页。登录了?正常访问。业务代码完全不用关心鉴权逻辑。

场景二:缓存 / 记忆化

其实 Python 标准库已经帮你写好了:

python 复制代码
from functools import lru_cache

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

@lru_cache 自动缓存函数的返回值。第二次传同样的参数,直接返回缓存结果,不重新计算。这就是为什么 Python 社区说"装饰器是复用代码的优雅方式"。

场景三:重试机制

网络请求经常不稳定,写个重试装饰器:

python 复制代码
def retry(max_attempts=3):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"第 {attempt} 次失败:{e}")
                    if attempt == max_attempts:
                        raise
        return wrapper
    return decorator

@retry(max_attempts=5)
def fetch_data(url):
    # 请求网络...
    pass

失败了自动重试,到了最大次数才抛异常。业务代码只管调 fetch_data,完全不用写 for 循环和 try/except


装饰器叠加:一件外套不够,就穿两件

一个函数可以同时用好几个装饰器:

python 复制代码
@timer
@retry(max_attempts=3)
def fetch_data(url):
    pass

执行顺序是从下往上:先 retry,再 timer。就像穿衣服,先穿内衣再穿外套。用等价代码表示就是:

python 复制代码
fetch_data = timer(retry(max_attempts=3)(fetch_data))

记住:离函数定义最近的装饰器最先执行。


类装饰器:用类来写装饰器也行

虽然函数形式的装饰器最常见,但 Python 也允许你用类来写:

python 复制代码
class CountCalls:
    def __init__(self, func):
        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 say_hi():
    print("Hi!")

say_hi()   # say_hi 已被调用 1 次 \n Hi!
say_hi()   # say_hi 已被调用 2 次 \n Hi!

类装饰器的好处是可以方便地维护状态(比如这里的 self.count)。当你需要在多次调用之间记住某些信息时,类装饰器比函数装饰器更直观。


小结:装饰器的三个核心心法

回头看看,装饰器其实就三件事:

第一,函数是对象。 它可以被赋值、传递,也可以被"包装"。

第二,装饰器就是包装函数。 它接收一个函数,返回一个功能更丰富的新函数。@ 只是语法糖。

第三,@wraps(func) 永远别忘了。 保持原函数的元信息,是装饰器的基本礼貌。

如果你之前对装饰器一知半解,希望现在有一种"哦,原来如此"的感觉。它不是什么黑魔法,就是 Python 里一个设计得很巧妙的模式------把重复的"前处理/后处理"逻辑抽出来,让代码更干净、更好维护。

下次你写代码的时候,如果发现自己在好几个函数里复制粘贴了同样的逻辑,不妨停下来想一想:这是不是该写个装饰器了?

相关推荐
深爱水瓶 血影S狂风1 小时前
Asp.Net无刷新上传并裁剪头像
后端·asp.net
大地之灯1 小时前
从零做一个自己的 CLI
python·agent
gugucoding1 小时前
34.【Java】I/O流(下):NIO与文件操作
java·python·nio
cui_ruicheng1 小时前
Python数据分析(十三):Seaborn 统计可视化
开发语言·python·信息可视化·数据分析
如此这般英俊1 小时前
手搓Claude Code-第十章 system_prompt
数据结构·人工智能·python·语言模型·自然语言处理·prompt
高洁012 小时前
VLA:驱动具身智能迈向通用的关键引擎
人工智能·python·深度学习·transformer·知识图谱
用户8181870627462 小时前
第30章 Web 层与事务异常:参数校验、@Transactional 失效场景全集
后端
Java内核笔记2 小时前
BeanRegistrar:Spring Boot 4 最被低估的新特性,彻底改变 Bean 注册方式
java·后端