如果你学过一段时间 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)返回decoratordecorator(func)返回wrapperwrapper(*args, **kwargs)才是真正执行的那个函数
用的时候:
python
@timer(verbose=False)
def fast_multiply(a, b):
return a * b
记住一个规律:装饰器外面多一层括号和参数,就意味着多了一层函数嵌套。 参数是给最外面那层函数传的,不是给 decorator 或 wrapper 传的。
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 里一个设计得很巧妙的模式------把重复的"前处理/后处理"逻辑抽出来,让代码更干净、更好维护。
下次你写代码的时候,如果发现自己在好几个函数里复制粘贴了同样的逻辑,不妨停下来想一想:这是不是该写个装饰器了?