装饰器 (中): 进阶篇,解锁框架级玩法
基础看完了?那我们来玩点进阶的。
你有没有好奇过,FastAPI 的@depends()、Flask 的@app.route()都是怎么写的?为啥人家的装饰器能传参数,还能这么灵活?
今天我就把这些进阶玩法教给你,学会了,你也能写出灵活的装饰器,看懂各种框架的源码。
1. 带参数的装饰器,了解一下?
有时候你可能会想,能不能给装饰器自己也传点参数?比如我想要一个重复执行 N 次的装饰器,N 我自己说了算。
这也简单,就是再包一层嘛!
三层函数的秘密
普通的装饰器是两层:外部函数接收原函数,内部函数是包装后的函数。
那带参数的装饰器,就是再加一层:最外层接收装饰器的参数,然后返回真正的装饰器。
我们来看个最经典的例子:
defrepeat(times):# 这一层:接收装饰器的参数,比如times=3defdecorator(func):# 这一层:接收原函数,就是普通的装饰器了@functools.wraps(func)defwrapper(*args,**kwargs):# 这一层:真正的包装函数,执行原函数result=Nonefor_inrange(times):result=func(*args,**kwargs)returnresultreturnwrapperreturndecorator# 用一下!@repeat(times=3)defsay_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:
importloggingdeflog(level="info"):defdecorator(func):@functools.wraps(func)defwrapper(*args,**kwargs):# 根据你指定的级别来打日志msg=f"调用函数:{func.__name__}"iflevel=="debug":logging.debug(msg)else:logging.info(msg)returnfunc(*args,**kwargs)returnwrapperreturndecorator# 用一下!@log(level="debug")defdebug_function():# 这个函数的日志会打debug级别pass@log()defnormal_function():# 这个函数的日志会打默认的info级别pass🌟 这就是框架的秘密:FastAPI 的
@depends()、Flask 的@app.route(),本质上都是这个模式!学会了这个,你看很多框架的源码就都能看懂了。
2. 多个装饰器一起用,注意顺序!
你可以给一个函数加多个装饰器,但是这里有个超级容易踩的坑:装饰器的执行顺序,很多人搞反了!
我们来看个实际的例子,你就懂了:
defdecorator1(func):@functools.wraps(func)defwrapper():print("进入 decorator1")func()print("退出 decorator1")returnwrapperdefdecorator2(func):@functools.wraps(func)defwrapper():print("进入 decorator2")func()print("退出 decorator2")returnwrapper@decorator1@decorator2defhello():print("Hello!")hello()你猜输出是什么?
进入 decorator1 进入 decorator2 Hello! 退出 decorator2 退出 decorator1哦!原来装饰器是从下到上装饰,从上到下执行!
也就是说,@decorator1在最上面,它先执行,然后才是@decorator2,最后才是原函数。
等价于:hello = decorator1(decorator2(hello)),所以调用的时候,先调用 decorator1 的 wrapper,然后它调用 decorator2 的 wrapper,然后它调用原函数。
⚠️踩坑提醒:顺序很重要!比如你写 Web 接口,
@login_required和@permission_required,一定要把@login_required放在上面,不然未登录的用户会先去检查权限,直接报错!
比如:
# 正确的顺序@login_required@permission_required("delete_user")defdelete_user(user_id):# ...这样,用户先检查登录,没登录直接跳登录页,不会去检查权限了。
3. 类装饰器,给类也来个包装
除了函数,装饰器还能用来装饰类!这在写 ORM、写框架的时候特别有用。
类装饰器的原理很简单:它接收一个类,然后返回一个新的类(或者修改后的类)。
我们来写一个最实用的:自动给类加一个好看的__repr__方法,这样你打印对象的时候,就能自动看到所有的属性了:
defadd_repr(cls):# 这就是类装饰器,接收类cls@functools.wraps(cls)def__repr__(self):# 自动生成__repr__,把所有的属性都列出来attrs=", ".join(f"{k}={v!r}"fork,vinself.__dict__.items())returnf"{cls.__name__}({attrs})"# 给类加上这个方法cls.__repr__=__repr__returncls# 用一下!@add_reprclassPerson: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__方法,这样实例就能当函数用了。
我们来写一个计数器装饰器,统计函数被调用了多少次:
classCounter:def__init__(self,func):# 初始化的时候,接收原函数self.func=func self.count=0# 状态存在这里# 别忘了把原函数的信息复制过来functools.update_wrapper(self,func)def__call__(self,*args,**kwargs):# 当你调用装饰后的函数的时候,就会调用这个方法self.count+=1returnself.func(*args,**kwargs)# 用一下!@Counterdefadd(a,b):returna+bprint(add(1,2))# 输出: 3print(add(3,4))# 输出: 7print(add.count)# 输出: 2,调用了2次!看到了吗?用类实现的话,状态的维护就非常直观,self.count一眼就能看到,比闭包的 nonlocal 好理解多了。
而且如果你的装饰器还需要带参数,也很简单,把参数放到__init__里就行:
classRepeat:def__init__(self,times):self.times=timesdef__call__(self,func):@functools.wraps(func)defwrapper(*args,**kwargs):result=Nonefor_inrange(self.times):result=func(*args,**kwargs)returnresultreturnwrapper@Repeat(times=3)defsay_hello(name):print(f"Hello,{name}")是不是很灵活?
好了,进阶的内容差不多就这些了。现在你已经能写各种灵活的装饰器了,不管是带参数的,还是装饰类的,还是用类实现的,都没问题。
不过这些还不够,下一篇我们来点实战的,看看在实际工作中,装饰器都能用来干啥,有哪些你马上就能用到的工具。
