Python 装饰器:给函数穿上"外衣"
我是陈默,一个正拼命上岸的码农。
你出门前会穿外套吗?
人还是那个人,但加了外套,就能挡风、防雨、保暖。
装饰器就是函数的外套。函数还是那个函数,但加了装饰器,就能计时、检查权限、记录日志——不用改函数本身的一行代码。
听起来有点意思吧?
1. 先搞懂:函数能当变量用
在理解装饰器之前,你得先接受一件事:Python 里,函数是一等公民。
defsay_hello():return"你好"# 函数可以赋给变量greet = say_helloprint(greet()) # 输出: 你好# 函数可以当参数传递defcall_func(func):return func()print(call_func(say_hello)) # 输出: 你好# 函数可以当返回值defget_func():return say_hellof = get_func()print(f()) # 输出: 你好
函数能当变量、当参数、当返回值。这就是装饰器的基础。
2. 手写一个装饰器
装饰器本质上就是:接收一个函数,返回一个新函数。
最简单的装饰器
defmy_decorator(func):defwrapper(): print("函数执行前") func() print("函数执行后")return wrapperdefsay_hello(): print("你好")# 用装饰器"包装"这个函数say_hello = my_decorator(say_hello)say_hello()# 输出:# 函数执行前# 你好# 函数执行后
函数还是叫 say_hello,但它的行为被"包装"了一层。
用 @ 语法糖
上面那种写法太啰嗦。Python 提供了 @ 语法糖:
defmy_decorator(func):defwrapper(): print("函数执行前") func() print("函数执行后")return wrapper@my_decorator # 等同于 say_hello = my_decorator(say_hello)defsay_hello(): print("你好")say_hello()# 输出:# 函数执行前# 你好# 函数执行后
@my_decorator 就是一个简写。它把下面的函数自动传给装饰器,再把结果赋回给函数名。
3. 装饰带参数的函数
上面的例子中 say_hello() 没有参数。但实际函数通常都有参数。
用 *args 和 **kwargs
defmy_decorator(func):defwrapper(*args, **kwargs): print("函数执行前") result = func(*args, **kwargs) print("函数执行后")return resultreturn wrapper@my_decoratordefadd(a, b):return a + bprint(add(3, 5))# 输出:# 函数执行前# 函数执行后# 8
*args 和 **kwargs 表示接收任意参数。这样不管原函数有几个参数,装饰器都能正常工作。
4. 实用装饰器:计时器
这是最实用的装饰器之一——测量函数执行时间。
import timedeftimer(func):defwrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} 耗时:{end - start:.4f}秒")return resultreturn wrapper@timerdefslow_function(): total = sum(range(1000000))return totalresult = slow_function()# 输出: slow_function 耗时:0.0xxx秒
不用改函数本身,加一行 @timer 就能计时。这就是装饰器的魅力。
5. 实用装饰器:缓存
避免重复计算,把结果存起来。
defmemoize(func): cache = {}defwrapper(*args):if args in cache: print(f"命中缓存:{args}")return cache[args] result = func(*args) cache[args] = resultreturn resultreturn wrapper@memoizedeffibonacci(n):if n < 2:return nreturn fibonacci(n - 1) + fibonacci(n - 2)print(fibonacci(10))# 第一次计算:逐步递归# 后续调用相同参数:命中缓存print(fibonacci(10))# 输出: 命中缓存:(10,)
(其实 Python 自带了 @functools.lru_cache,比这个更强。但自己写一遍能帮你理解原理。)
6. 带参数的装饰器
装饰器本身也能接收参数。
defrepeat(n):"""让函数重复执行 n 次"""defdecorator(func):defwrapper(*args, **kwargs):for _ in range(n): result = func(*args, **kwargs)return resultreturn wrapperreturn decorator@repeat(3)defsay_hello(name): print(f"你好,{name}!")say_hello("陈默")# 输出:# 你好,陈默!# 你好,陈默!# 你好,陈默!
多了一层嵌套:repeat(n) 返回 decorator,decorator 再返回 wrapper。
# 等同于say_hello = repeat(3)(say_hello)
7. functools.wraps:保留函数元信息
装饰器有个小问题:被装饰的函数的 __name__、__doc__ 会丢失。
defmy_decorator(func):defwrapper(*args, **kwargs):"""这是 wrapper 的文档"""return func(*args, **kwargs)return wrapper@my_decoratordefsay_hello():"""打招呼函数""" print("你好")print(say_hello.__name__) # 输出: wrapper(不是 say_hello!)print(say_hello.__doc__) # 输出: 这是 wrapper 的文档
用 @functools.wraps 解决:
from functools import wrapsdefmy_decorator(func): @wraps(func)defwrapper(*args, **kwargs):return func(*args, **kwargs)return wrapper@my_decoratordefsay_hello():"""打招呼函数""" print("你好")print(say_hello.__name__) # 输出: say_hello ✓print(say_hello.__doc__) # 输出: 打招呼函数 ✓
写装饰器时,永远加上 @wraps(func)。这是一个好习惯。
8. 类装饰器
除了函数,类也能当装饰器。
classCountCalls:"""统计函数被调用的次数"""def__init__(self, func): self.func = func self.count = 0def__call__(self, *args, **kwargs): self.count += 1 print(f"{self.func.__name__} 第{self.count}次调用")return self.func(*args, **kwargs)@CountCallsdefsay_hello(name): print(f"你好,{name}!")say_hello("陈默") # 输出: say_hello 第1次调用 \n 你好,陈默!say_hello("小明") # 输出: say_hello 第2次调用 \n 你好,小明!say_hello("小红") # 输出: say_hello 第3次调用 \n 你好,小红!
类装饰器通过 __init__ 接收函数,通过 __call__ 实现调用。
好处是可以保存状态——比如调用次数。
9. 多个装饰器:叠加使用
装饰器可以叠加,从下往上执行:
defbold(func):defwrapper():returnf"<b>{func()}</b>"return wrapperdefitalic(func):defwrapper():returnf"<i>{func()}</i>"return wrapper@bold@italicdefgreet():return"你好"print(greet()) # 输出: <b><i>你好</i></b>
执行顺序:greet 先被 italic 包装,再被 bold 包装。
等同于 greet = bold(italic(greet))。
10. 实战:写一个权限检查装饰器
from functools import wrapsdefrequire_role(role):"""检查用户是否有指定角色"""defdecorator(func): @wraps(func)defwrapper(user, *args, **kwargs):if user.get("role") != role: print(f"权限不足!需要 {role},当前是 {user.get('role', '游客')}")returnNonereturn func(user, *args, **kwargs)return wrapperreturn decorator@require_role("admin")defdelete_user(user, target): print(f"{user['name']} 删除了用户 {target}")# 测试admin = {"name": "陈默", "role": "admin"}guest = {"name": "游客", "role": "guest"}delete_user(admin, "测试账号")# 输出: 陈默 删除了用户 测试账号delete_user(guest, "测试账号")# 输出: 权限不足!需要 admin,当前是 guest
你看,权限检查的逻辑和业务逻辑完全分离。以后要改权限规则,只改装饰器就行。
最后
装饰器是 Python 进阶的标志。理解了它,你才算真正开始用 Python 的方式思考。
记住三件事:
- 永远用
@functools.wraps 保留原函数的元信息 - 装饰器适合做"横切关注点"——日志、权限、缓存、计时
我的建议:
选一个你写过的函数,给它加个 @timer 装饰器,看看它到底跑了多久。然后再试试写个 @retry 装饰器——函数失败时自动重试。
装饰器的精髓:不改老代码,就能加新功能。
今天就到这里。
我是陈默,我们下期再见。
如果你觉得这篇文章有帮助,欢迎关注我。我会持续分享 Python 学习的干货。