python装饰器深入学习

yipeiwu_com5年前Python基础

什么是装饰器

在我们的软件产品升级时,常常需要给各个函数新增功能,而在我们的软件产品中,相同的函数可能会被调用上百次,这种情况是很常见的,如果我们一个个的修改,那我们的码农岂不要挂掉了(有人就说了 ,你笨呀,修改函数定义不就行了!同学,你醒醒吧,如果要新加的功能会修改参数,或者返回值呢?)。这个时候,就是我们装饰器大显神通的时候了。装饰器就可以实现,在不改变原函数的调用形式下(即函数的透明化处理),给函数新增功能的作用。如何实现,以及实现原理,下文会详解。

装饰器遵循的原则

装饰器,顾名思义就是起装饰的作用,既然是装饰,那么被装饰的对象是啥样就是啥样,不能有丝毫改变。在这里,我们写装饰器就是必须把握不能修改被修饰函数的源代码这条铁律。如何遵循这条铁律,我们还需还需做一些铺垫,必须先要了解三个概念,如下:

函数名即“变量”

在python中,函数名其实就像是c语言的函数指针,代表的是我们的函数地址,只有解释器获取到这个地址,它才会去执行这块内存的代码。因此,本质上,函数名就和不同变量没什么区别,只不过函数名和普通变量所指代的那块内存的使用方式不同罢了,这些都是底层解释器的机制所决定的,对于程序猿来说,都是透明的,所以,我们可以认为两者是没有区别的。

高阶函数

什么是高阶函数其实很简单,把握两个原则就好:

  • 形式参数有函数名
  • 返回值有函数名

只要满足这两个原则之一,就可以称之为是高阶函数。翻回头来看,这里出现了我们上面说的函数名,仔细体会一下,我们在这里不就是把其当成实参看待的吗?

嵌套函

什么是嵌套函数其实也非常简单,把握一个原则就好:

  • 在一个函数的函数体中去定义另一个函数

在这里需要强调的是,函数定义时是不会执行函数体的,就和定义变量是不会去读取变量里的内容一样。这一点至关重要,对于我们理解装饰器实现原理非常有帮助。

如何写装饰器

有了上文的铺垫,在现在来详解一下如何写装饰器,就好理解多了。

装饰器本质

  其实装饰器本质上就是一个函数,它也具有函数名,参数和返回值。但在python中,我们用“@auth”来表示。

@auth # 其等价于:func = auth(func)
def func():
 print("func called")

 这个示例就是python中如何修饰func函数的格式,当然我们还没有实现我们的装饰器函数。我们要注意的是注释里写的内容,我们可以看出:

  • 装饰器函数其实是一个高阶函数(参数和返回值都为函数名)。
  • “auth(func)”是在调用我们的装饰器函数,即装饰器函数的函数体会被执行,一定要记好这一点。

设计思路

装饰器即然是个函数,又有上述介绍的等价关系,那我们就可以这样设计我们的装饰器:

  • 在我们装饰器的函数体内去定义一个新的函数,在这个新定义的函数内去调用被修饰的函数,与此同时,在被修饰的函数的上下文去添加新功能。最后,利用装饰器函数的返回值返回我们新定义函数的函数名。
  • 由此可以知道,“func = auth(func)”中的返回值func表示的就是在装饰器中新定义的函数的函数名。

前面做了大量的铺垫,就是想在这里揭示装饰器的实现机制,其实没什么什么的,很简单:

  • 装饰器机制改变了被修饰函数的函数名表示的地址数据。说白了就是,被修饰前,函数名代表的是A内存块;被修饰后,函数名代表的是B内存块;只不过,在执行B内存块时,会调用A内存块罢了。B内存块中的代码就是我们新加的功能。而这种机制的实现,使用了“高阶函数”和“嵌套函数”的机制。
  • 最终的效果就是,但在调用被修饰过的函数时,其实调用的不是原来的内存块,而是修饰器新申请的内存块。

第一步:设计装饰器函数

装饰器函数定义跟普通函数定义没什么区别,关键是函数体怎么写的问题。这里,为了便于理解,先用无参数的装饰器函数说明。

#装饰器函数定义格式
def deco(func):
 '''函数体...'''
return func

这里说的无参数,指的是没有除了“func”之外的参数
难点是函数体的编写,下面的示例先告诉你为什么要有第二步:

#使用语法糖@来装饰函数,相当于“myfunc = deco(myfunc)”
def deco(func):
 print("before myfunc() called.")
 func()
 print("after myfunc() called.")
 return func
 
@deco
def myfunc():
 print("myfunc() called.")
 
myfunc()
myfunc()
 
#output:
before myfunc() called.
myfunc() called.
after myfunc() called.
myfunc() called.
myfunc() called. 

由输出结果可以看出,我们的装饰器并没有生效。别跟我说装饰器只生效了一次,那是大家忽略了“@deco”的等效机制。解释到“@deco”时,会解释成“myfunc = deco(myfunc)”。注意了,前面我提到了,这里其实在调用deco函数的,因此,deco的函数体会被执行。所以output的前三行并不是调用myfunc函数时产生的效果,那有怎能说装饰器生效了一次呢?第二步就是解决装饰器没生效的问题的。

第二步:包装被修饰函数

#基本格式
def deco(func):
 def _deco()
 #新增功能
 #...
 #...
 func() #别修饰函数调用
 return_deco

 下面给出个示例:

#使用内嵌包装函数来确保每次新函数都被调用,
#内嵌包装函数的形参和返回值与原函数相同,装饰函数返回内嵌包装函数对象
 
def deco(func):
 def _deco():
 print("before myfunc() called.")
 func()
 print("after myfunc() called.")
 # 不需要返回func,实际上应返回原函数的返回值
 return _deco
 
@deco
def myfunc():
 print("myfunc() called.")
 return 'ok'
 
myfunc()
 
#output:
before myfunc() called.
myfunc() called.
after myfunc() called.

  第三步:被修饰函数参数和返回值透明化处理

当完成了第二步时,其实装饰器已经完成了主要部分,下面就是对被修饰函数的参数和返回值的处理。这样才能真正实现装饰器的铁律。话不多说,直接上代码:

#基本格式
def deco(func):
 def _deco(*args, **kwargs) #参数透明化
 #新增功能
 #...
 #...
 res = func(*args, **kwargs) #别修饰函数调用
 return res #返回值透明化
 return_deco

通过上面的分析知:

参数透明化:当我们在调用被装饰后的函数时,其实调用的时这里的_deco函数。那么,我们就给_deco函数加上可变参数,并把得到的可变参数传递给func函数不就可以了。
返回值透明化:和参数透明化同理,给_deco函数定义返回值,并返回func的返回值就可以了。

透明化处理就是这么简单!至此,我们的装饰器编写完成。给个示例吧:

#对带参数的函数进行装饰,
#内嵌包装函数的形参和返回值与原函数相同,装饰函数返回内嵌包装函数对象
 
def deco(func):
 def _deco(*agrs, **kwagrs):
 print("before myfunc() called.")
 ret = func(*agrs, **kwagrs)
 print(" after myfunc() called. result: %s" % ret)
 return ret
 return _deco
 
@deco
def myfunc(a, b):
 print(" myfunc(%s,%s) called." % (a, b))
 return a + b
 
print("sum=",myfunc(1, 2))
print("sum=",myfunc(3, 4))
 
#output:
before myfunc() called.
 myfunc(1,2) called.
 after myfunc() called. result: 3
sum= 3
before myfunc() called.
 myfunc(3,4) called.
 after myfunc() called. result: 7
sum= 7

装饰器进阶

带参数装饰器

装饰器即然也是函数,那么我们也可以给其传递参数。我这里说的是:“@auth(auth_type = 'type1')”这中形式哟。先上个代码吧:

#基本格式
def deco(deco_type)
 def _deco(func):
 def __deco(*args, **kwargs) #参数透明化
  #新增功能
  #...
  #...
  print("deco_type:",deco_type) #使用装饰器参数
  res = func(*args, **kwargs) #别修饰函数调用
  return res #返回值透明化
 return __deco
 return_deco 

 说白了,就是在原来的装饰器的基础上再在最外层套一个deco函数,并用其来接收装饰器参数。由于是在最外层套了一个函数,那么这个函数的形参的作用范围就是函数体内部,所以里面的函数定义中随便用啦,就这么任性。
那怎么理解解释器的解析过程呢?在这里,只要我们明白一点就好,那就是: “@auth(auth_type = 'type1')”等价于“func = auth(auth_type = 'type1')(func)” 解释器会先翻译“auth(auth_type = 'type1')”,再将其返回值(假设给了_func这个不存在的函数名)当作函数指针,这里的_func函数名代表的是_deco,然后再去执行“func = _func(func)”,而这个func函数名代表的其实就是__deco。

至此,就达到了通过装饰器来传参的目的。给个示例吧:

#示例7: 在示例4的基础上,让装饰器带参数,
#和上一示例相比在外层多了一层包装。
#装饰函数名实际上应更有意义些
 
def deco(deco_type):
 def _deco(func):
 def __deco(*args, **kwagrs):
  print("before %s called [%s]." % (func.__name__, deco_type))
  func(*args, **kwagrs)
  print(" after %s called [%s]." % (func.__name__, deco_type))
 return __deco
 return _deco
 
@deco("mymodule")
def myfunc():
 print(" myfunc() called.")
 
@deco("module2")
def myfunc2():
 print(" myfunc2() called.")
 
myfunc()
myfunc2()
 
#output:
before myfunc called [mymodule].
 myfunc() called.
 after myfunc called [mymodule].
before myfunc2 called [module2].
 myfunc2() called.
 after myfunc2 called [module2].

多重装饰器修饰函数

如果说,我上面说的内容都理解了,那么这个东东,就太简单不过了。不就是把我们的是装饰器当中被修饰的函数,对它进行装饰吗?但我在这里还想说的是,我们换个角度看问题。我们的关注点放在原来的被修饰的函数上,就会发现,NB呀,我可以给它添加若干个功能撒。给个示例吧:

def deco(deco_type):
 def _deco(func):
 def __deco(*args, **kwagrs):
  print("before %s called [%s]." % (func.__name__, deco_type))
  func(*args, **kwagrs)
  print(" after %s called [%s]." % (func.__name__, deco_type))
 return __deco
 return _deco
 
@deco("module1")
@deco("mymodule")
def myfunc():
 print(" myfunc() called.")
 
@deco("module2")
def myfunc2():
 print(" myfunc2() called.")
 
myfunc()
 
#output:
before __deco called [module1].
before myfunc called [mymodule].
 myfunc() called.
 after myfunc called [mymodule].
 after __deco called [module1].

 注意结果哟,@deco("module1"),来修饰的deco("mymdule")的,和我们想的是一样的,完美!

领取干货:零基础入门学习python视频教程

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持【听图阁-专注于Python设计】。

相关文章

python实时分析日志的一个小脚本分享

前言 大家都知道Web运维总要关注相关域名的实时2xx/s、4xx/s、5xx/s、响应时间、带宽等这些指标,之前的日志是五分钟一分割,简单的用awk就可以了,现在由于要推送日志到ELK...

一个基于flask的web应用诞生 flask和mysql相连(4)

一个基于flask的web应用诞生 flask和mysql相连(4)

上一章实现了登录的部分功能,之所以说是部分功能,是因为用户名和密码写成固定值肯定是不可以的,一个整体的功能,至少需要注册,登录,密码修改等,这就需要提供一个把这些值存储到数据库的能力。...

使用python实现BLAST

使用python实现BLAST

最近在自学python,又用python实现了一下BLAST。 这次更新了打分函数如下,空位罚分改为-5,但不区分gap open 和 gap extend。 ''''' @au...

Python中文竖排显示的方法

本文实例讲述了Python中文竖排显示的方法。分享给大家供大家参考。具体如下: 这里将中文竖排显示 比如 衣食者人之生利也,然且犹尚有节,葬埋者人之死利也,夫何独无节於此乎 输出为: 衣...

SVM基本概念及Python实现代码

SVM基本概念及Python实现代码

SVM(support vector machine)支持向量机: 注意:本文不准备提到数学证明的过程,一是因为有一篇非常好的文章解释的非常好:支持向量机通俗导论(理解SVM的三层境界)...