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

Python的装饰器详解

时间:2023-03-12 14:20:30 科技观察

Python中的装饰器是你进入Python大门的一道坎,跨不跨,它就在那里。为什么需要装饰器假设您的程序实现了两个函数say_hello()和say_goodbye()。defsay_hello():打印“你好!”你好调试后发现say_goodbye()是错误的。老板要求在调用每一个方法之前记录下入口函数的名称,例如:[DEBUG]:Entersay_hello()Hello![DEBUG]:Entersay_goodbye()Goodbye!好吧,小A是个毕业生,他是这样认识的。defsay_hello():print[DEBUG]:entersay_hello()"print"hello!"defsay_goodbye():print"[DEBUG]:entersay_goodbye()"print"hello!"if__name__=='__main__':say_hello()say_goodbye()很低吧?嗯,是。小B工作了一段时间,他告诉小A他可以这么写。defdebug():importinspectcaller_name=inspect.stack()[1][3]print"[DEBUG]:enter{}()".format(caller_name)defsay_hello():debug()print"你好!"defsay_goodbye():debug()打印“再见!”if__name__=='__main__':say_hello()say_goodbye()是不是比较好?当然可以,但是每个业务函数都需要调用debug()函数。不舒服吗?有大佬说say相关的函数不需要debug,do相关的函数呢?那么这个时候装饰器应该出现了。装饰器本质上是一个Python函数,它允许其他函数在不更改任何代码的情况下添加额外的功能。装饰器的返回值也是一个函数对象。常用于有横切需求的场景,如:插入日志、性能测试、事务处理、缓存、权限验证等场景。装饰器是解决此类问题的最佳设计。有了装饰器,我们就可以提取出很多与函数本身无关的雷同代码,继续复用。简而言之,装饰器的作用是为现有函数或对象添加额外的功能。如何编写装饰器早期(Python版本<2.4,2004年之前),给函数添加额外功能的方式是这样的。defdebug(func):defwrapper():print"[DEBUG]:enter{}()".format(func.__name__)returnfunc()returnwrapperdefsay_hello():print"hello!"say_hello=debug(say_hello)#添加函数和保持原函数名不变。上面的debug函数其实就是一个装饰器。它包装了原始函数并返回另一个函数,添加了一些额外的函数。因为这种写法不是很优雅,所以Python后期版本支持@grammaticalsugar,下面的代码等同于早期的写法。defdebug(func):defwrapper():print"[DEBUG]:enter{}()".format(func.__name__)returnfunc()returnwrapper@debugdefsay_hello():print"你好!"这是最简单的装饰器,但是有一个问题,如果被装饰的函数需要传入参数,那么这个装饰器就坏了。因为返回的函数不能接受参数,所以可以指定装饰器函数wrapper接受与原函数相同的参数,例如:defdebug(func):defwrapper(something):#Specifythesameparametersprint[DEBUG]:enter{}()".format(func.__name__)returnfunc(something)returnwrapper#返回包装好的函数@debugdefsay(something):print"hello{}!".format(something)所以你解决了一个问题,但是还有更多N个问题。因为函数有几万个,你只关心自己的函数,别人的函数参数长什么样子,鬼知道吗?幸运的是,Python提供了可变参数*args和关键字参数**kwargs。有了这两个参数,装饰器就可以用于任何目标函数。defdebug(func):defwrapper(*args,**kwargs):#指定宇宙无敌参数print"[DEBUG]:enter{}()".format(func.__name__)print'Prepareandsay...',returnfunc(*args,**kwargs)returnwrapper#Return@debugdefsay(something):print"hello{}!".format(something)至此,你已经完全掌握了装饰器的基本写法。更高级的装饰器。参数装饰器和类装饰器是进阶内容。在了解这些装饰器之前,***对函数的闭包和装饰器的接口约定有了一定的了解。(见http://betacat.online/posts/p...带参数的装饰器。假设我们之前的装饰器需要完成的功能不仅是进入某个功能后能够打印日志信息,还要指定日志级别,那么装饰器将如下所示。(level=level,func=func.__name__)returnfunc(*args,**kwargs)returninner_wrapperreturnwrapper@logging(level='INFO')defsay(something):print"say{}!".format(something)#Ifnot使用@grammar,相当于#say=logging(level='INFO')(say)@logging(level='DEBUG')defdo(something):print"do{}...".format(something)if__name__=='__main__':say('hello')do("mywork")是不是有点晕?你可以这样理解,在函数上打上带参数的装饰器时,比如@logging(level='DEBUG'),其实就是一个函数,会立即执行,只要它返回的结果是decorator,那就没有问题了。细看。基于类实现的装饰器函数其实就是这样一个Interface约束,它必须接受一个可调用对象作为参数,然后返回一个可调用对象。在Python中,可调用对象一般是函数,但也有例外。只要一个对象重载了__call__()方法,那么这个对象就是可调用的。-in方法在Python中,有时这些被称为魔法方法。覆盖这些魔法方法通常会改变对象的内部行为。上面的示例使类对象具有被调用的行为。回到装饰器的概念,要求装饰器接受一个可调用对象,并返回一个可调用对象(不是很严格,详见下文)。那么用类来实现也是可以的。我们可以让类构造函数__init__()接受一个函数,然后重载__call__()返回一个函数,这样也可以达到装饰器函数的效果。classlogging(object):def__init__(self,func):self.func=funcdef__call__(self,*args,**kwargs):print"[DEBUG]:enterfunction{func}()".格式(func=self.func.__name__)returnsself.func(*args,**kwargs)@loggingdefsay(something):print"say{}!".format(something)带参数的类装饰器如果你需要实现一个带有参数形式的装饰器class,那么它会比前面的例子复杂一点。那么构造函数中接受的不是函数,而是传入的参数。通过类保存这些参数。那么在重载__call__方法的时候,需要接受一个函数,返回一个函数。classlogging(object):def__init__(self,level='INFO'):self.level=leveldef__call__(self,func):#acceptfunctiondefwrapper(*args,**kwargs):print"[{level}]:enterfunction{func}()".format(level=self.level,func=func.__name__)func(*args,**kwargs)returnwrapper#Returnfunction@logging(level='INFO')defsay(something):print"say{}!".format(something)内置装饰器内置装饰器的原理和普通装饰器一样,只是返回的不是函数,而是类对象,所以是更难理解。@property在理解这个装饰器之前,你需要知道如何在不使用装饰器的情况下编写一个属性。defgetx(self):returnsself._xdefsetx(self,value):self._x=valuedefdelx(self):delself._x#createapropertyx=property(getx,setx,delx,"Iamdocforxproperty")以上是标准的写法Python的属性,其实和Java挺像的,就是太啰嗦了。使用@grammaticalsugar,可以达到同样的效果,但看起来更简单。@propertydefx(self):...#等价于defx(self):...x=property(x)property有三个装饰器:setter、getter、deleter,都是基于property()封装的,因为setter和deleter是property()的第二个和第三个参数,不能直接套用@语法。getter装饰器与不带getter的属性装饰器效果相同。估计只是凑个数,本身没有任何意义。用@property修饰的函数返回的不再是一个函数,而是一个属性对象。>>>property()@staticmethod,@classmethod有@property装饰器的理解,这两个装饰器的原理是相似的。@staticmethod返回一个staticmethod类对象,@classmethod返回一个classmethod类对象。他们都调用各自的__init__()构造函数。classclassmethod(object):"""classmethod(function)->method"""def__init__(self,function):#for@classmethoddecoratorpass#...classstaticmethod(object):"""staticmethod(function)->method"""def__init__(self,function):#for@staticmethoddecoratorpass#...装饰器的@语法相当于调用了这两个类的构造函数。classFoo(object):@staticmethoddefbar():pass#相当于bar=staticmethod(bar)至此,我们上面提到的装饰器接口的定义就可以更加清晰了。装饰器必须接受一个可调用对象,但它并不真正关心你返回的可以是另一个可调用对象(在大多数情况下),或者其他类对象,比如属性。装饰器中的坑装饰器可以让你的代码更优雅,减少重复,但并不是所有的优点,也会带来一些问题。放错地方的代码让我们直接看示例代码。defhtml_tags(tag_name):print'beginouterfunction.'defwrapper_(func):print"beginofinnerwrapperfunction."defwrapper(*args,**kwargs):content=func(*args,**kwargs)print"<{tag}>{content}".format(tag=tag_name,content=content)print'endofinnerwrapperfunction.'returnwrapperprint'endofouterfunction'returnwrapper_@html_tags('b')defhello(name='Toby'):return'Hello{}!'.format(name)hello()hello()在装饰器中,我在每一个可能的位置都添加了打印语句来记录被调用的情况。你知道它们打印出来的顺序吗?如果你没有想法,那么最好不要在装饰器函数之外添加逻辑函数,否则装饰器将不受你的控制。下面是输出:beginouterfunction.endofouterfunctionbeginofinnerwrapperfunction.endofinnerwrapperfunction.HelloToby!HelloToby!错误的函数签名和文件装饰器装饰函数看似名字没变,实则变了改变了。deflogging(func):defwrapper(*args,**kwargs):"""printlogbeforefunction."""print"[DEBUG]{}:enter{}()".format(datetime.now(),func.__name__)returnfunc(*args,**kwargs)returnwrapper@loggingdefsay(something):"""saysomething"""print"say{}!".format(something)printsay.__name__#wrapper为什么会这样?想想装饰@的语法糖替换一下就明白了。@相当于这种写法。say=logging(say)logging其实返回的是函数名作为wrapper,所以上面的语句只是把结果赋值给say,而say的__name__自然是wrapper,不仅是名字,还有其他属性。它来自包装器,例如doc、source等。使用标准库中的functools.wraps基本可以解决这个问题。fromfunctoolsimportwrapsdeflogging(func):@wraps(func)defwrapper(*args,**kwargs):"""printlogbeforefunction.""print"[DEBUG]{}:enter{}()".format(datetime.now(),func.__name__)returnfunc(*args,**kwargs)returnwrapper@loggingdefsay(something):"""saysomething"""print"say{}!".format(something)printsay.__name__#sayprintsay.__doc__#saysomethingSee看起来不错!主要问题解决了,但还不是很完美。因为函数的签名和源代码还没有。importinspectprintinspect.getargspec(say)#failedprintinspect.getsource(say)#failed如果想彻底解决这个问题,可以借用第三方包,比如wrapt。后面有介绍。不能装饰@staticmethod或@classmethod当你想在静态方法或类方法上使用装饰器时,抱歉,报错。classCar(object):def__init__(self,model):self.model=model@logging#修饰实例方法,OKdefrun(self):print"{}isrunning!".format(self.model)@logging#修饰static方法,失败@staticmethoddefcheck_model_for(obj):ifisinstance(obj,Car):print"Themodelofyourcaris{}".format(obj.model)else:print"{}isnotacar!".format(obj)"""Traceback(mostrecentcallast):...文件“example_4.py”,line10,inlogging@wraps(func)文件“C:\Python27\lib\functools.py”,line33,inupdate_wrappersetattr(wrapper,attr,getattr(wrapped,attr))AttributeError:'staticmethod'objecthasnoattribute'__module__'"""装饰器@staticmethod前面已经解释过了,但是它返回的不是一个可调用对象,而是一个staticmethod对象,所以不符合装饰器的要求(比如传入一个可调用对象),你自然不能在它上面添加其他装饰器。解决这个问题很简单,只要把你的装饰器放在@staticmethod之前,因为你的装饰器返回的是一个普通的函数,然后再加一个@staticmethod就不会出问题了。classCar(object):def__init__(self,model):self.model=model@staticmethod@logging#Decoratebefore@staticmethod,OKdefcheck_model_for(obj):pass如何优化你的decorator嵌套装饰功能不直观,我们这种情况可以通过使用第三方包类进行改进,使装饰器函数更具可读性。decorator.pydecorator.py是一个非常简单的装饰器增强包。可以直观的先定义包装器函数wrapper(),再使用decorate(func,wrapper)方法完成一个装饰器。fromdecoratorimportdecoratedefwrapper(func,*args,**kwargs):"""printlogbeforefunction."""print"[DEBUG]{}:enter{}()".format(datetime.now(),func.__name__)returnfunc(*args,**kwargs)deflogging(func):returndecorate(func,wrapper)#Decoratefuncwithwrapper你也可以使用它自己的@decorator装饰器来完成你的装饰器。fromdecoratorimportdecorator@decoratordeflogging(func,*args,**kwargs):打印“[DEBUG]{}:enter{}()”.format(datetime.now(),func.__name__)returnfunc(*args,**kwargs)decorator.py实现的装饰器可以完全保留原函数的name、doc和args。唯一的问题是inspect.getsource(func)返回装饰器的源代码。您需要将其更改为inspect.getsource(func.__wrapped__)。wraptwrapt是一个非常完整的包,用于实现各种你想到或没想到的装饰器。有了wrapt实现的装饰器,你就不用担心之前inspect遇到的所有问题了,因为它帮你处理好了,连inspect.getsource(func)也是准确的。importwrapt#withoutargumentindecorator@wrapt.decoratordelogging(wrapped,instance,args,kwargs):#instanceismmustprint“[DEBUG]:enter{}()”.format(wrapped.__name__)returnwrapped(*args,**kwargs)@loggingdefsay(something)):pass要使用wrapt,只需要定义一个装饰器函数,但是函数签名是固定的,必须是(wrapped,instance,args,kwargs)。请注意,第二个参数实例是必需的,即使您不使用它也是如此。当装饰器被装饰在不同的位置时,会得到不同的值。例如,在类实例方法中修饰时,可以获得类实例。你可以根据实例的值更灵活地调整你的装饰器。另外args和kwargs也是固定的,注意前面没有星号。星号仅在装饰器内部调用原始函数时使用。如果需要用wrapt写带参数的装饰器,可以这样写。deflogging(level):@wrapt.decoratordefwrapper(wrapped,instance,args,kwargs):print"[{}]:enter{}()".format(level,wrapped.__name__)返回包装(*args,**kwargs)returnwrapper@logging(level="INFO")defdo(work):pass关于wrapt的使用,建议查阅官方文档,这里不再赘述。http://wrapt.readthedocs.io/e...总结Python的装饰器与Java的注解(Annotation)不是一回事,也不同于C#的属性(Attribute),完全是两个概念。装饰器的思想是加强原有的功能和对象,相当于重新包装,所以一般的装饰器函数取名为wrapper(),也就是包装的意思。函数只有在被调用时才会工作。比如@logging装饰器可以在函数执行的时候额外输出日志,@cache装饰的函数可以缓存计算结果等等。注解和属性就是给目标函数或对象加上一些属性,相当于对其进行了分类。这些属性可以通过反射获得,在程序运行时介入不同的特征函数或对象。例如,带有Setup的函数作为准备步骤被执行,或者带有TestMethod的所有函数被找到并按顺序执行等等。至此我已经讲完了我所知道的装饰器,但还有一些东西没有提到过,比如装饰类的装饰器。有机会再补充。感谢收看。