当前位置: 首页 > 科技观察

读懂Python装饰器这篇文章就够了

时间:2023-03-20 15:03:40 科技观察

在讲Python装饰器之前,我先举个例子。虽然有点脏,但是和装饰器的话题很相关。每个人都有的内衣的主要作用就是遮羞,但是到了冬天却不能为我们挡风挡雨,怎么办呢?我们想到的一种方法是修改内衣,使其更厚、更长。这样一来,既有遮羞的作用,又有保暖的作用,但是有个问题。我们把内裤改造成裤子后,虽然它还有遮羞的作用,但本质上已经不是真正的内裤了。于是聪明人发明了裤子。在不影响内裤的情况下,他们直接把裤子套在内裤外面,这样内裤还是内裤,宝宝有了裤子就不会再冷了。装饰器就像我们这里说的裤子。它在不影响内衣功能的情况下为我们的身体提供温暖。在谈论装饰器之前,您需要了解一件事。Python中的函数不同于Java和C++。Python中的函数可以像普通变量一样作为参数传递给另一个函数,例如:deffoo():print("foo")defbar(func):func()bar(foo)正式回到我们的主题。装饰器本质上是一个Python函数或类,它允许其他函数或类在不修改任何代码的情况下添加额外的功能。装饰器的返回值也是一个函数/类对象。常用于有横切需求的场景,如:插入日志、性能测试、事务处理、缓存、权限验证等场景。装饰器是解决此类问题的绝佳设计。有了装饰器,我们就可以将大量与函数本身无关的雷同代码提取到装饰器中,继续复用。简而言之,装饰器的作用是为现有对象添加额外的功能。先看一个简单的例子,虽然实际代码可能比这个复杂很多:deffoo():print('iamfoo')现在有一个新需求,希望记录函数的执行日志,所以在代码中加入Logging代码:deffoo():print('iamfoo')logging.info("fooisrunning")如果函数bar()和bar2()有类似的需求,怎么办?在bar函数中再写一个日志记录?这导致了很多类似的代码。为了减少代码重复,我们可以重新定义一个新的函数:专门处理日志,处理完日志后再执行真正的业务代码defuse_logging(func):logging.warn("%sisrunning"%func.__name__)func()deffoo():print('iamfoo')use_logging(foo)这样做逻辑上没问题,功能实现了,但是当我们调用的时候并没有调用真正的业务逻辑foo函数,取而代之的是use_logging函数,破坏了原有的代码结构。现在我们每次都必须将原始的foo函数作为参数传递给use_logging函数。那么有没有更好的办法呢?答案当然是装饰器。简单装饰器defuse_logging(func):defwrapper():logging.warn("%sisrunning"%func.__name__)returnfunc()#当foo作为参数传入时,执行func()相当于执行foo()returnwrapperdeffoo():print('iamfoo')foo=use_logging(foo)#因为装饰器use_logging(foo)返回的是函数对象wrapper,所以这条语句相当于foo=wrapperfoo()#执行foo()相当于执行wrapper()。use_logging是一个装饰器。它是一个普通函数,包装了执行真正业务逻辑的函数func。貌似foo被use_logging修饰了,use_logging返回了一个函数,这个函数的名字叫wrapper。在这个例子中,函数进入和退出的时候,叫做横截面,这种编程风格叫做面向切面编程。@语法糖如果你接触过一段时间的Python,你一定对@符号不陌生。是的,@符号是装饰器的语法糖。它放在函数定义的开头,这样最后一步就可以省略了。赋值操作。defuse_logging(func):defwrapper():logging.warn("%sisrunning"%func.__name__)returnfunc()returnwrapper@use_loggingdeffoo():print("iamfoo")foo()如上所示,有了@,我们就可以省去foo=use_logging(foo)这句话了,直接调用foo()就可以得到想要的结果了。有没有看到foo()函数不需要修改,只需要在定义的地方加一个装饰器,调用还是和之前一样。如果我们还有其他类似的功能,我们可以继续调用装饰器来装饰功能,而不需要重复修改功能或添加新的包。这样,我们提高了程序的复用性,增加了程序的可读性。Python中装饰器的方便是因为Python函数可以像普通对象一样作为参数传递给其他函数,可以赋值给其他变量,可以作为返回值,可以在另一个函数中定义。*args,**kwargs有人会问,如果我的业务逻辑函数foo需要参数怎么办?例如:deffoo(name):print("iam%s"%name)我们可以在定义包装函数时指定参数:defwrapper(name):logging.warn("%sisrunning"%func.__name__)returnfunc(name)returnwrapper这样就可以在wrapper函数中定义foo函数定义的参数。这时候又有人要问了,如果foo函数接收两个参数怎么办?三个参数呢?更重要的是,我可能会通过很多。当装饰器不知道foo有多少个参数时,我们可以使用*args代替:defwrapper(*args):logging.warn("%sisrunning"%func.__name__)returnfunc(*args)return在这way,不管foo定义了多少个参数,我都可以完全传给func。这不会影响foo的业务逻辑。这时候可能有读者会问,如果foo函数也定义了一些关键字参数呢?例如:deffoo(name,age=None,height=None):print("Iam%s,age%s,height%s"%(name,age,height))此时,你可以把wrapperfunctionSpecifykeywordfunction:defwrapper(*args,**kwargs):#args是一个数组,kwargs是一个字典logging.warn("%sisrunning"%func.__name__)returnfunc(*args,**kwargs)带returnwrapper的装饰器更加灵活,比如带参数的装饰器。在上面的装饰器调用中,装饰器接收的唯一参数是执行业务的函数foo。装饰器语法允许我们在调用时提供额外的参数,例如@decorator(a)。这样就为装饰器的编写和使用提供了更大的灵活性。比如我们可以在装饰器中指定日志级别,因为不同的业务功能可能需要不同的日志级别。defuse_logging(level):defdecorator(func):defwrapper(*args,**kwargs):iflevel=="warn":logging.warn("%sisrunning"%func.__name__)eliflevel==“信息”:记录。info("%sisrunning"%func.__name__)returnfunc(*args)returnwrapperreturndecorator@use_logging(level="warn")deffoo(name='foo'):print("我是%s"%name)foo()上面的use_logging是一个允许参数的装饰器。它实际上是对原始装饰器的函数包装,并返回一个装饰器。我们可以理解为带参数的闭包。当我们使用@use_logging(level="warn")调用时,Python可以发现这种级别的封装并将参数传递给装饰器的环境。@use_logging(level=”warn”)相当于@decorator类装饰器是的,装饰器不仅可以是函数,也可以是类。与函数装饰器相比,类装饰器具有更大的灵活性、高内聚性、封装性等优点。类装饰器的使用主要依赖于类的__call__方法,当装饰器使用@形式附加到函数时调用。classFoo(object):def__init__(self,func):self._func=funcdef__call__(self):print('classdecoratorrunning')self._func()print('classdecoratorending')@Foodefbar():print('bar')bar()functools.wraps使用装饰器极大地重用了代码,但缺点是缺少了原函数的元信息,比如函数的docstring、__name__、参数列表.我们先看例子。:#装饰器deflogged(func):defwith_logging(*args,**kwargs):printfunc.__name__#output'with_logging'printfunc.__doc__#outputNonereturnfunc(*args,**kwargs)returnwith_logging#function@loggeddeff(x):"""doessomemath"""returnx+x*xlogged(f)不难发现函数f被with_logging替换了,当然它的docstring,__name__就变成了with_logging函数信息.好在我们有functools.wraps,wraps本身也是一个装饰器,它可以将原函数的元信息复制到装饰器中的func函数中,使得装饰器中的func函数也具有与原始功能fooup。fromfunctoolsimportwrapsdeflogged(func):@wraps(func)defwith_logging(*args,**kwargs):printfunc.__name__#output'f'printfunc.__doc__#output'doessomemath'returnfunc(*args),**kwargs)returnwith_logging@loggeddeff(x):"""doessomemath"""returnx+x*xorderofdecorators一个函数也可以同时定义多个装饰器,例如:@a@b@cdeff():pass它的执行顺序是从里到外,先调用最里面的装饰器,最后调用最外层的装饰器,相当于f=a(b(c(f)))