Python的装饰器使用详解
Python有大量强大又贴心的特性,如果要列个最受欢迎排行榜,那么装饰器绝对会在其中。
初识装饰器,会感觉到优雅且神奇,想亲手实现时却总有距离感,就像深闺的冰美人一般。这往往是因为理解装饰器时把其他的一些概念混杂在一起了。待我抚去层层面纱,你会看到纯粹的装饰器其实蛮简单直率的。
装饰器的原理
在解释器下跑个装饰器的例子,直观地感受一下。
#make_bold就是装饰器,实现方式这里略去
>>>@make_bold ...defget_content(): ...return'helloworld' ... >>>get_content() 'helloworld'
被make_bold装饰的get_content,调用后返回结果会自动被b标签包住。怎么做到的呢,简单4步就能明白了。
1.函数是对象
我们定义个get_content函数。这时get_content也是个对象,它能做所有对象的操作。
defget_content(): return'helloworld'
它有id,有type,有值。
>>>id(get_content) 140090200473112 >>>type(get_content)>>>get_content
跟其他对象一样可以被赋值给其它变量。
>>>func_name=get_content >>>func_name() 'helloworld'
它可以当参数传递,也可以当返回值
>>>deffoo(bar): ...print(bar()) ...returnbar ... >>>func=foo(get_content) helloworld >>>func() 'helloworld'
2.自定义函数对象
我们可以用class来构造函数对象。有成员函数__call__的就是函数对象了,函数对象被调用时正是调用的__call__。
classFuncObj(object): def__init__(self,name): print('Initialize') self.name=name def__call__(self): print('Hi',self.name)
我们来调用看看。可以看到,函数对象的使用分两步:构造和调用(同学们注意了,这是考点)。
>>>fo=FuncObj('python') Initialize >>>fo() Hipython
3.@是个语法糖
装饰器的@没有做什么特别的事,不用它也可以实现一样的功能,只不过需要更多的代码。
@make_bold defget_content(): return'helloworld' #上面的代码等价于下面的 defget_content(): return'helloworld' get_content=make_bold(get_content)
make_bold是个函数,要求入参是函数对象,返回值是函数对象。@的语法糖其实是省去了上面最后一行代码,使可读性更好。用了装饰器后,每次调用get_content,真正调用的是make_bold返回的函数对象。
4.用类实现装饰器
入参是函数对象,返回是函数对象,如果第2步里的类的构造函数改成入参是个函数对象,不就正好符合要求吗?我们来试试实现make_bold。
classmake_bold(object): def__init__(self,func): print('Initialize') self.func=func def__call__(self): print('Call') return'{}'.format(self.func())
大功告成,看看能不能用。
>>>@make_bold ...defget_content(): ...return'helloworld' ... Initialize >>>get_content() Call 'helloworld'
成功实现装饰器!是不是很简单?
这里分析一下之前强调的构造和调用两个过程。我们去掉@语法糖好理解一些。
#构造,使用装饰器时构造函数对象,调用了__init__
>>>get_content=make_bold(get_content) Initialize #调用,实际上直接调用的是make_bold构造出来的函数对象 >>>get_content() Call 'helloworld'
到这里就彻底清楚了,完结撒花,可以关掉网页了~~~(如果只是想知道装饰器原理的话)
函数版装饰器
阅读源码时,经常见到用嵌套函数实现的装饰器,怎么理解?同样仅需4步。
1.def的函数对象初始化
用class实现的函数对象很容易看到什么时候构造的,那def定义的函数对象什么时候构造的呢?
#这里的全局变量删去了无关的内容
>>>globals() {} >>>deffunc(): ...pass ... >>>globals() {'func':}
不像一些编译型语言,程序在启动时函数已经构造那好了。上面的例子可以看到,执行到def会才构造出一个函数对象,并赋值给变量make_bold。
这段代码和下面的代码效果是很像的。
classNoName(object): def__call__(self): pass func=NoName()
2.嵌套函数
Python的函数可以嵌套定义。
defouter(): print('Beforedef:',locals()) definner(): pass print('Afterdef:',locals()) returninner
inner是在outer内定义的,所以算outer的局部变量。执行到definner时函数对象才创建,因此每次调用outer都会创建一个新的inner。下面可以看出,每次返回的inner是不同的。
>>>outer() Beforedef:{} Afterdef:{'inner':.innerat0x7f0b18fa0048>} .innerat0x7f0b18fa0048> >>>outer() Beforedef:{} Afterdef:{'inner': .innerat0x7f0b18fa00d0>} .innerat0x7f0b18fa00d0>
3.闭包
嵌套函数有什么特别之处?因为有闭包。
defouter(): msg='helloworld' definner(): print(msg) returninner
下面的试验表明,inner可以访问到outer的局部变量msg。
>>>func=outer() >>>func() helloworld
闭包有2个特点
1.inner能访问outer及其祖先函数的命名空间内的变量(局部变量,函数参数)。
2.调用outer已经返回了,但是它的命名空间被返回的inner对象引用,所以还不会被回收。
这部分想深入可以去了解Python的LEGB规则。
4.用函数实现装饰器
装饰器要求入参是函数对象,返回值是函数对象,嵌套函数完全能胜任。
defmake_bold(func): print('Initialize') defwrapper(): print('Call') return'{}'.format(func()) returnwrapper
用法跟类实现的装饰器一样。可以去掉@语法糖分析下构造和调用的时机。
>>>@make_bold ...defget_content(): ...return'helloworld' ... Initialize >>>get_content() Call 'helloworld'
因为返回的wrapper还在引用着,所以存在于make_bold命名空间的func不会消失。make_bold可以装饰多个函数,wrapper不会调用混淆,因为每次调用make_bold,都会有创建新的命名空间和新的wrapper。
到此函数实现装饰器也理清楚了,完结撒花,可以关掉网页了~~~(后面是使用装饰的常见问题)
常见问题
1.怎么实现带参数的装饰器?
带参数的装饰器,有时会异常的好用。我们看个例子。
>>>@make_header(2) ...defget_content(): ...return'helloworld' ... >>>get_content() 'helloworld
'
怎么做到的呢?其实这跟装饰器语法没什么关系。去掉@语法糖会变得很容易理解。
@make_header(2) defget_content(): return'helloworld' #等价于 defget_content(): return'helloworld' unnamed_decorator=make_header(2) get_content=unnamed_decorator(get_content)
上面代码中的unnamed_decorator才是真正的装饰器,make_header是个普通的函数,它的返回值是装饰器。
来看一下实现的代码。
defmake_header(level): print('Createdecorator') #这部分跟通常的装饰器一样,只是wrapper通过闭包访问了变量level defdecorator(func): print('Initialize') defwrapper(): print('Call') return'{1} '.format(level,func()) returnwrapper #make_header返回装饰器 returndecorator
看了实现代码,装饰器的构造和调用的时序已经很清楚了。
>>>@make_header(2) ...defget_content(): ...return'helloworld' ... Createdecorator Initialize >>>get_content() Call 'helloworld
'
2.如何装饰有参数的函数?
为了有条理地理解装饰器,之前例子里的被装饰函数有意设计成无参的。我们来看个例子。
@make_bold defget_login_tip(name): return'Welcomeback,{}'.format(name)
最直接的想法是把get_login_tip的参数透传下去。
classmake_bold(object): def__init__(self,func): self.func=func def__call__(self,name): return'{}'.format(self.func(name))
如果被装饰的函数参数是明确固定的,这么写是没有问题的。但是make_bold明显不是这种场景。它既需要装饰没有参数的get_content,又需要装饰有参数的get_login_tip。这时候就需要可变参数了。
classmake_bold(object): def__init__(self,func): self.func=func def__call__(self,*args,**kwargs): return'{}'.format(self.func(*args,**kwargs))
当装饰器不关心被装饰函数的参数,或是被装饰函数的参数多种多样的时候,可变参数非常合适。可变参数不属于装饰器的语法内容,这里就不深入探讨了。
3.一个函数能否被多个装饰器装饰?
下面这么写合法吗?
@make_italic @make_bold defget_content(): return'helloworld'
合法。上面的的代码和下面等价,留意一下装饰的顺序。
defget_content(): return'helloworld' get_content=make_bold(get_content)#先装饰离函数定义近的 get_content=make_italic(get_content)
4.functools.wraps有什么用?
Python的装饰器倍感贴心的地方是对调用方透明。调用方完全不知道也不需要知道调用的函数被装饰了。这样我们就能在调用方的代码完全不改动的前提下,给函数patch功能。
为了对调用方透明,装饰器返回的对象要伪装成被装饰的函数。伪装得越像,对调用方来说差异越小。有时光伪装函数名和参数是不够的,因为Python的函数对象有一些元信息调用方可能读取了。为了连这些元信息也伪装上,functools.wraps出场了。它能用于把被调用函数的__module__,__name__,__qualname__,__doc__,__annotations__赋值给装饰器返回的函数对象。
importfunctools defmake_bold(func): @functools.wraps(func) defwrapper(*args,**kwargs): return'{}'.format(func(*args,**kwargs)) returnwrapper
对比一下效果。
>>>@make_bold ...defget_content(): ...'''Returnpagecontent''' ...return'helloworld' #不用functools.wraps的结果 >>>get_content.__name__ 'wrapper' >>>get_content.__doc__ >>> #用functools.wraps的结果 >>>get_content.__name__ 'get_content' >>>get_content.__doc__ 'Returnpagecontent'
实现装饰器时往往不知道调用方会怎么用,所以养成好习惯加上functools.wraps吧。
这次是真·完结了,撒花吧~~~