基础看完了?那我们来玩点进阶的。
你有没有好奇过,FastAPI 的@depends()、Flask 的@app.route()都是怎么写的?为啥人家的装饰器能传参数,还能这么灵活?
今天我就把这些进阶玩法教给你,学会了,你也能写出灵活的装饰器,看懂各种框架的源码。
1. 带参数的装饰器,了解一下?
有时候你可能会想,能不能给装饰器自己也传点参数?比如我想要一个重复执行 N 次的装饰器,N 我自己说了算。
这也简单,就是再包一层嘛!
三层函数的秘密
普通的装饰器是两层:外部函数接收原函数,内部函数是包装后的函数。
那带参数的装饰器,就是再加一层:最外层接收装饰器的参数,然后返回真正的装饰器。
我们来看个最经典的例子:
python
def repeat(times):
# 这一层:接收装饰器的参数,比如times=3
def decorator(func):
# 这一层:接收原函数,就是普通的装饰器了
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 这一层:真正的包装函数,执行原函数
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
# 用一下!
@repeat(times=3)
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
# 输出:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
看到了吗?@repeat(times=3)其实就是先调用repeat(3),它返回了一个装饰器,然后用这个装饰器去装饰say_hello函数。
等价于:say_hello = repeat(times=3)(say_hello),就这么简单!
更实用的例子:带级别的日志装饰器
我们来写一个更实用的,你可以指定日志的级别,是 info 还是 debug:
python
import logging
def log(level="info"):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 根据你指定的级别来打日志
msg = f"调用函数: {func.__name__}"
if level == "debug":
logging.debug(msg)
else:
logging.info(msg)
return func(*args, **kwargs)
return wrapper
return decorator
# 用一下!
@log(level="debug")
def debug_function():
# 这个函数的日志会打debug级别
pass
@log()
def normal_function():
# 这个函数的日志会打默认的info级别
pass
🌟 这就是框架的秘密:FastAPI 的
@depends()、Flask 的@app.route(),本质上都是这个模式!学会了这个,你看很多框架的源码就都能看懂了。
2. 多个装饰器一起用,注意顺序!
你可以给一个函数加多个装饰器,但是这里有个超级容易踩的坑:装饰器的执行顺序,很多人搞反了!
我们来看个实际的例子,你就懂了:
python
def decorator1(func):
@functools.wraps(func)
def wrapper():
print("进入 decorator1")
func()
print("退出 decorator1")
return wrapper
def decorator2(func):
@functools.wraps(func)
def wrapper():
print("进入 decorator2")
func()
print("退出 decorator2")
return wrapper
@decorator1
@decorator2
def hello():
print("Hello!")
hello()
你猜输出是什么?
Plain
进入 decorator1
进入 decorator2
Hello!
退出 decorator2
退出 decorator1
哦!原来装饰器是从下到上装饰,从上到下执行!
也就是说,@decorator1在最上面,它先执行,然后才是@decorator2,最后才是原函数。
等价于:hello = decorator1(decorator2(hello)),所以调用的时候,先调用 decorator1 的 wrapper,然后它调用 decorator2 的 wrapper,然后它调用原函数。
⚠️ 踩坑提醒 :顺序很重要!比如你写 Web 接口,
@login_required和@permission_required,一定要把@login_required放在上面,不然未登录的用户会先去检查权限,直接报错!
比如:
python
# 正确的顺序
@login_required
@permission_required("delete_user")
def delete_user(user_id):
# ...
这样,用户先检查登录,没登录直接跳登录页,不会去检查权限了。
3. 类装饰器,给类也来个包装
除了函数,装饰器还能用来装饰类!这在写 ORM、写框架的时候特别有用。
类装饰器的原理很简单:它接收一个类,然后返回一个新的类(或者修改后的类)。
我们来写一个最实用的:自动给类加一个好看的__repr__方法,这样你打印对象的时候,就能自动看到所有的属性了:
python
def add_repr(cls):
# 这就是类装饰器,接收类cls
@functools.wraps(cls)
def __repr__(self):
# 自动生成__repr__,把所有的属性都列出来
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{cls.__name__}({attrs})"
# 给类加上这个方法
cls.__repr__ = __repr__
return cls
# 用一下!
@add_repr
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p = Person("Alice", 25)
print(p)
# 输出: Person(name='Alice', age=25)
是不是超方便?不用你自己写__repr__了,加个装饰器就搞定了!
这个在写 ORM 框架的时候特别有用,比如 SQLAlchemy 的模型类,很多都是用类装饰器来自动注册的。
4. 用类来实现装饰器,保存状态更方便
如果你的装饰器需要维护复杂的状态,用类比用闭包更好,代码更清晰,也更容易测试。
因为类可以很自然的保存状态,用self.xxx就可以了,不用像闭包那样搞 nonlocal。
原理也很简单:装饰器接收原函数,存到self.func里,然后实现__call__方法,这样实例就能当函数用了。
我们来写一个计数器装饰器,统计函数被调用了多少次:
python
class Counter:
def __init__(self, func):
# 初始化的时候,接收原函数
self.func = func
self.count = 0 # 状态存在这里
# 别忘了把原函数的信息复制过来
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
# 当你调用装饰后的函数的时候,就会调用这个方法
self.count += 1
return self.func(*args, **kwargs)
# 用一下!
@Counter
def add(a, b):
return a + b
print(add(1, 2)) # 输出: 3
print(add(3, 4)) # 输出: 7
print(add.count) # 输出: 2,调用了2次!
看到了吗?用类实现的话,状态的维护就非常直观,self.count一眼就能看到,比闭包的 nonlocal 好理解多了。
而且如果你的装饰器还需要带参数,也很简单,把参数放到__init__里就行:
python
class Repeat:
def __init__(self, times):
self.times = times
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(self.times):
result = func(*args, **kwargs)
return result
return wrapper
@Repeat(times=3)
def say_hello(name):
print(f"Hello, {name}")
是不是很灵活?
好了,进阶的内容差不多就这些了。现在你已经能写各种灵活的装饰器了,不管是带参数的,还是装饰类的,还是用类实现的,都没问题。
不过这些还不够,下一篇我们来点实战的,看看在实际工作中,装饰器都能用来干啥,有哪些你马上就能用到的工具。