函数写好了,想给它前后加点通用逻辑——记日志、算耗时、做校验——总不能每个函数都复制一遍。装饰器就是干这个的:给函数套一层"外壳",功能加上,原函数一行不改。
先看效果
import time
def timer(func):
def wrapper():
start = time.time()
func() # 调用原来的函数
print(f"耗时 {time.time() - start:.3f} 秒")
return wrapper
@timer
def download():
time.sleep(1)
print("下载完成")
download() # 输出:下载完成
# 输出:耗时 1.001 秒
@timer 这一行就是装饰器,它等价于 download = timer(download)。之后再调用 download,实际跑的是 wrapper,它在原函数前后各加了一件事:掐表和报时。
原理:函数是可以传来传去的值
Python 里函数跟数字、字符串一样是个对象,可以赋值、传参、当返回值。装饰器的全部原理就一句话:接收一个函数,返回一个新函数。
def shout(fn):
def wrapper():
print(">>> 开嗓")
fn()
print("<<< 收工")
return wrapper
def sing():
print("唱歌~")
sing = shout(sing) # 手动装饰,不用 @ 也能写
sing()
# 输出:>>> 开嗓
# 唱歌~
# <<< 收工
看懂这个手动版本,@ 就只是少打一行的语法糖,没有任何魔法。
原函数有参数怎么办
被装饰的函数往往带参数,wrapper 得能接住任意参数,用 *args 和 **kwargs 通吃:
def log_call(func):
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__},参数 {args}")
result = func(*args, **kwargs) # 原样透传
return result # 别把返回值弄丢
return wrapper
@log_call
def add(a, b):
return a + b
print(add(3, 5))
# 输出:调用 add,参数 (3, 5)
# 输出:8
两个细节最容易踩坑:wrapper 内部要 return func(...),否则原函数的返回值会变成 None;args 原样传回,装饰器不需要知道参数具体是啥。
保留函数的"身份证"
装饰后 add.name 会变成 wrapper,调试器、文档工具都会认错人。标准库 functools 一行修好:
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
print(add.__name__) # 输出:add,而不是 wrapper
写自定义装饰器时建议养成肌肉记忆:@wraps(func) 永远带上,代价为零。
一个装饰器,到处复用
攒下几个常用装饰器,新函数加一行就能白得功能:
@log_call
def greet(name):
print(f"你好,{name}")
greet("雪梨")
# 输出:调用 greet,参数 ('雪梨',)
# 输出:你好,雪梨
日志、计时、重试、缓存,这类横切的通用逻辑都适合做成装饰器,写一次到处用。
小结
- 装饰器本质是"接收函数、返回新函数",@ 只是语法糖
- wrapper 用 *args、**kwargs 透传参数,记得 return 保住返回值
- functools.wraps 让装饰后的函数保留原名和文档
- 通用逻辑(日志/计时/校验)抽成装饰器,业务函数保持干净
理解了"函数可以当值传来传去",装饰器就不再神秘。