Python装饰器,Python闭包

可参考:https://www.cnblogs.com/lianyingteng/p/7743876.html

suqare(5)等价于power(2)(5);cube(5)等价于power(3)(5);

power相当于是一个工厂,由于参数不同,得到了两个不同的生产线,一个是square一个是cube,前者是返回参数的平方,后者返回的是参数的立方;这就是展开讨论的意义。

由于嵌套函数的外层作用域会被保存下来,所以在执行square=power(2)时,square变量指向的这个exp_of函数就记住了外层函数power(exp)作用域的这个exp参数的值是2;而在执行cube=power(3)时,cube变量指向的这个exp_of()函数,他记住的exp参数的值是3,(网友:这里明白外层函数作用域会被保留的意思了;),所以,调用square()函数总是计算平方,而调用cube()函数则总是计算立方。

此外,我们知道使用nonlocal函数可以让嵌套函数的内层函数去修改到外层函数的这个变量,那么我们可以如下图所示在外部定义一个叫做outer的函数名,并在内部给两个变量一个是x,y=0,0,具体如下所示。

然后把外部函数outer()赋值给move变量,然后再用move间接的调用内部的函数inner(),而inner()使用参数的,现在给它(1,2)。得到的如下图所示。(有网友说报错:报错的检查下return内部函数时有没有加());

 

所以现在我们就利用了内层函数可以记住外层函数的特性,并且使用nonlocal语句,让它可以修改到外层函数作用域的里面的这个变量,我们就可以实现一个带记忆功能的一个函数。

在实际的开发中,闭包的应用:在游戏开发中需要将游戏中的角色移动位置给保护起来,不希望被其他含税轻易地就能够修改到它,就可以利用闭包来实现,如下图示例所示。用于实现角色移动的代码片段。

最开始设置origin也就是原点为(0,0);step参数设置的是移动的距离,为了修改外部作用域的,我们这里使用了nonlocal pos_x和pos_y,这样就可以在内层的moving函数里面,去修改到外层create的这个pos_x和pos_y的值,new_x,new_y等于什么什么,

pos_x加上一个方向乘以步进值,就等于移动的最终位置。然后下面对X轴和Y轴,进行一个检测是否超出边界的一个操作;如果超出编辑那么就制造一个撞墙反弹的效果,moving函数是反水移动好的最新的位置。然后外层函数是返回内层函数moving的一个引用。

       

既然函数可以作为返回值给返回,那么,能否把一个函数作为参数传递给林各一个函数呢?如果想把程序变得彬彬有礼,就让他在干啥事之前先打个招呼。

上图不是最优的方案,因为每次你想知道一个函数运行多长时间,都要显式的调用time_master()函数才行;更好的方案是再调用myfunc()函数时,它能自觉地执行time_master()函数;

如上图示,程序被强制time.sleep(2)休息了2秒钟后,打印了几行字符串,然后结束程序的运行,最后统计一共用时2.04秒。但是在代码中我们并没有显示的去调用time_master()函数,这就和上上图中演示的不一样了,但是确实做了和它类似的工作;这就是装饰器的作用。

使用了装饰器并不用去修改原来的代码,只需要在函数的上方加一个@time_master即可,然后正常的去调用myfun()就能够实现统计运行时间的功能了。这在真正的开发中是非常常见的。比如你代码写的好好的,突然然你去做插入日志,或者添加性能测试或者要求先进行权限验证这些,那么此时就可以通过添加装饰器,在不修改原来代码的前提下,来实现这些功能下。 

其实你仔细看下,装饰器就是我们之前介绍的闭包和那函数当参数的结合。而@+函数名其实是个语法糖;装饰器原来的样子原始的样子应该是如下图所示。把@+函数名删掉,然后在myfunc处调用time_master然后把myfunc作为参数传进去,具体为

myfunc=time_master(myfunc),你现在回头看一下,其实time_master()是个闭包;当我们去调用time_master这个函数的时候,它其实并不会执行内部的call_func,而是将它返回;那我们这里调用了time_master并且把myfunc给传递进去,所以这时候它并不会调用,而是返回它内部的call_func,把内部返回的call_func赋值给了myfunc;相当于我们内部做了一件狸猫换太子的事情;那么下面我们调用myfunc()的时候,就相当于调用了call_func(),然后这个call_func()就开始运行程序,再获取时间戳,再真的运行程序,在获取时间戳,然后把后续的一系列执行完。(网友:两层()()相当于调用内部函数;这波是直接hook了myfunc;)。可看到实现的效果是一样的。

以上就是装饰器原来的样子。刚刚谈到语法糖,之前说过语法糖是某种特殊的语法,对于语言的功能本身是没有影响的,也即用不用都可以,但是对于程序员来说,有了它程序就有更好的易用性、简洁性、可读性和便捷性。就像上述截图代码块中的 f" " 字符串就是一个语法糖,它是format的一个语法糖。而我们装饰器的语法糖的样子是在需要作为传入函数的那个函数定义前面用@符号+被传入的函数名字。也即是@+装饰器的名字。在这里是@+time_master。这样在后面调用myfunc的时候,我门并不是直接调用myfunc,而是把myfunc这个函数作为一个参数传递到上面的装饰器里面,然后再执行/调用这个装饰器。下面看一下多个装饰器同时用到同一个函数上面的例子:

下面直接看一段代码的例子。

如上图所示,可以看到test()函数上面有三个装饰器,带了三顶帽子;运行后结果是65,可以推算出(实际也是)其调用顺序为从离其最近的以此开始向上调用,也即调用顺序square,cube,add反过来也得不到65这个值。

以上就是同时调用多个装饰器的方法和调用的顺序的例子。那么下面再看一下如何给装饰器传递参数

可看到,下图左边的装饰器是带有参数的,通过观察其定义的部分,可以看到传递参数的实现形式是多加了一层嵌套。

现在我们把上图所示能传递参数的语法糖给拆掉,还原为它原来的样子。

反正最后肯定要覆盖回原本的函数名的;然后我们这里是logger是装饰器最外层,最外层需要你传递一个msg,他得到的是time_master函数的一个引用,也就三层函数里面中间那一层time_master的引用,但其实我们要的是call_func的引用;那么我们应该再调用一次,因为调用time_master得到的就是call_func,call_func这里传入的应该是函数的名字,这就得到了它原来的样子:funA=logger(msg="A")(funcA);同理funB=logger(msg="A")(funcB)。也就是通过这种嵌套的方式实现参数的传递,第一次把调用把参数给扔进去,第二次调用把函数给扔进去;这样在调用funA和funB时,就和上面本质一样了。打印的当然也一样。如下图所示。

下面再对比一下(如下图所示),有参数和无参数的装饰器:左边是带参数的装饰器,右边是不带参数的装饰器 ;可以看到只是添加了一次调用,然后通过这次调用将参数给传递进去,看着挺复杂,其实很简单。

如果把上上图中的代码改为如下的代码形式,直接运行也可得到和上图一样的结果。

但是如果把下面的代码最后两行同样加上:

print(funcA())

print(funcB())

这两句的话,则会返回TypeError: 'NoneType'对象不可调用的(TypeError: 'NoneType' object is not callable)报错。

 

import time

def time_master(msg):
    def call_func(func):
        start=time.time()
        func()
        stop=time.time()
        print(f"[{msg}一共耗费了{(stop-start):.2f}]")
    return call_func

@time_master(msg="A")
def funcA():
    time.sleep(1)
    print('正在调用funcA...')

@time_master(msg="B")
def funcB():
    time.sleep(1)
    print("正在调用funcB...")

 

但是如果把上面的代码的最后两行同样加上:

print(funcA())

print(funcB())

这两句的话,则会返回TypeError: 'NoneType'对象不可调用的(TypeError: 'NoneType' object is not callable)报错。

 

        

 

 

顺序:思维导图(设计)-->流程图(实现)-->开发

以前你有没有这样一段经历:很久之前你写过一个函数,现在你突然有了个想法就是你想看看,以前那个函数在你数据集上的运行时间是多少,这时候你可以修改之前代码为它加上计时的功能,但是这样的话是不是还要大体读读你之前的这个的代码,稍微搞清楚一点它的逻辑,才敢给它添加新的东西。这样是不是很繁琐,要是你之前写的代码足够乱足够长,再去读它是不是很抓狂...。实际工作中,我们常常会遇到这样的场景,可能你的需求还不只是这么简单。那么有没有一种可以不对源码做任何修改,并且可以很好的实现你所有需求的手段呢?答案当然是有,这就是今天我们要介绍的python装饰器。有了装饰器,你除了不用担心前面提到的问题,并且还可以很好的处理接下来要做的事:那就是现在你又有了一个新的需求,比如为另一个函数添加计时功能,这时就非常简单了,把要装饰的函数丢给装饰器就好了,它会自动给你添加完功能并返回给你。是不是很神奇?下面我们将一层层剥开它的神秘面纱。

 

 

 

1. 闭包函数

  在看装饰器之前,我们先来搞清楚什么是闭包函数。python是一种面向对象的编程语言,在python中一切皆对象,这样就使得变量所拥有的属性,函数也同样拥有。这样我们就可以理解在函数内创建一个函数的行为是完全合法的。这种函数被叫做内嵌函数,这种函数只可以在外部函数的作用域内被正常调用,在外部函数的作用域之外调用会报错,例如:

Python装饰器 

 

 而如果内部函数里引用了外部函数里定义的对象(甚至是外层之外,但不是全局变量),那么此时内部函数就被称为闭包函数。闭包函数所引用的外部定义的变量被叫做自由变量。闭包从语法上看非常简单,但是却有强大的作用。闭包可以将其自己的代码和作用域以及外部函数的作用结合在一起。 

 总结:什么函数可以被称为闭包函数呢?主要是满足两点:函数内部定义的函数;引用了外部变量但非全局变量。 

2. python装饰器

  有了闭包函数的概念,我们再去理解装饰器会相对容易一些。python装饰器本质上就是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象(函数的指针)。装饰器函数的外部函数传入我要装饰的函数名字,返回经过修饰后函数的名字;内层函数(闭包)负责修饰被修饰函数。从上面这段描述中我们需要记住装饰器的几点属性,以便后面能更好的理解:

    实质: 是一个函数

    参数:是你要装饰的函数名(并非函数调用

    返回:是装饰完的函数名(也非函数调用

    作用:为已经存在的对象添加额外的功能

    特点:不需要对对象做任何的代码上的变动

python装饰器有很多经典的应用场景,比如:插入日志、性能测试、事务处理、权限校验等。装饰器是解决这类问题的绝佳设计。并且从引入中的列子中我们也可以归纳出:装饰器最大的作用就是对于我们已经写好的程序,我们可以抽离出一些雷同的代码组建多个特定功能的装饰器,这样我们就可以针对不同的需求去使用特定的装饰器,这时因为源码去除了大量泛化的内容而使得源码具有更加清晰的逻辑

显然,被“@函数”修饰的函数不再是原来的函数,而是被替换成一个新的东西(取决于装饰器的返回值),即如果装饰器函数的返回值为普通变量,那么被修饰的函数名就变成了变量名;同样,如果装饰器返回的是一个函数的名称,那么被修饰的函数名依然表示一个函数。实际上,所谓函数装饰器,就是通过装饰器函数,在不修改原函数的前提下,来对函数的功能进行合理的扩充。

 

2.1  函数装饰器

函数的函数装饰器

我们还是以为函数添加计时功能为例,讲述函数装饰器。

import time

def decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        func()
        end_time = time.time()
        print(end_time - start_time)

    return wrapper

@decorator 
def func():
    time.sleep(0.8)

func() # 函数调用# 输出:0.8097302913665771

在上面代码中 func是我要装饰器的函数,我想用装饰器显示func函数运行的时间。@decorator这个语法相当于 执行 func = decorator(func),为func函数装饰并返回。

再来看一下我们的装饰器函数 - decorator,该函数的传入参数是func (被装饰函数),返回参数是内层函数。这里的内层函数-wrapper,其实就相当于闭包函数,它起到装饰给定函数的作用,wrapper参数为*args, **kwargs。*args表示的参数以列表的形式传入;**kwargs表示的参数以字典的形式传入:  

def wrapper1(*args,**kwargs):
    print('args: ',args)
    print("kwargs: ",kwargs)
wrapper1(['hello',1,99,'nihao'],1000,'notebook',a=89,b="TheKing")
#输出为:
args:  (['hello', 1, 99, 'nihao'], 1000, 'notebook')
kwargs:  {'a': 89, 'b': 'TheKing'}

 

从以上我们可以看到:凡是以key=value形式的参数均存在kwargs中,剩下的所有参数都以列表的形式存于args中。

这里要注意的是:为了不破坏原函数的逻辑,我们要保证内层函数wrapper和被装饰函数func的传入参数和返回值类型必须保持一致。

类方法的函数装饰器

  类方法的函数装饰器和函数的函数装饰器类似。

import time

def decorator(func):
    def wrapper(me_instance):
        start_time = time.time()
        func(me_instance)
        end_time = time.time()
        print(end_time - start_time)
    return wrapper

class Method(object):

    @decorator
    def func(self):
        time.sleep(0.8)

p1 = Method()
p1.func() # 函数调用

输出为:
0.8149094581604004

对于类方法来说,都会有一个默认的参数self,它实际表示的是类的一个实例,所以在装饰器的内部函数wrapper也要传入一个参数 - me_instance就表示将类的实例p1传给wrapper,其他的用法都和函数装饰器相同。

2.2 类装饰器

  前面我们提到的都是让 函数作为装饰器去装饰其他的函数或者方法,那么可不可以让 一个类发挥装饰器的作用呢?答案肯定是可以的,一切皆对象嚒,函数和类本质没有什么不一样。类的装饰器是什么样子的呢?

class Decorator(object):
    def __init__(self, f):
        self.f = f
    def __call__(self):
        print("decorator start")
        self.f()
        print("decorator end")

@Decorator
def func():
    print("func")

func()
输出为:
decorator start
func
decorator end

这里有注意的是:__call__()是一个特殊方法,它可将一个类实例变成一个可调用对象:

class Decorator(object):
    def __init__(self, f):
        self.f = f
    def __call__(self):
        print("decorator start")
        self.f()
        print("decorator end")

@Decorator
def func():
    print("func")

# func()

p = Decorator(func) # p是类Decorator的一个实例
#到p=p = Decorator(func)这一步还没输出任何东西
p() # 实现了__call__()方法后,p可以被调用;此部开始输出
#输出结果为:
decorator start
decorator start
func
decorator end
decorator end

要使用类装饰器必须实现类中的__call__()方法,就相当于将实例变成了一个方法。

 

 

 

2.3  装饰器链

一个python函数也可以被多个装饰器修饰,要是有多个装饰器时,这些装饰器的执行顺序是怎么样的呢?

def makebold(f): return lambda: "<b>"+f()+"</b>"
def makeitalic(f): return lambda: "<i>" + f() + "</i>"
@makebold
@makeitalic
def say(): return "hello"
print(say())

#输出为:
<b><i>hello</i></b>

可见,多个装饰器的执行顺序:是从近到远依次执行。

2.4  python装饰器库 - functools

def decorator(func):
    def inner_function():
        print("I'm inner_function()")
    return inner_function

@decorator
def func():
    print("I'm func")

print(func.__name__)
#输出:
inner_function

 上述代码最后执行的结果不是 func,而是 inner_function!这表示被装饰函数自身的信息丢失了!怎么才能避免这种问题的发生呢?

可以借助functools.wraps()函数:

from functools import wraps
def decorator(func):
    @wraps(func)
    def inner_function():
        pass
    return inner_function

@decorator
def func():
    pass

print(func.__name__)
#输出为:
func

 

posted on 2021-08-26 21:37  lmqljt  阅读(45)  评论(0编辑  收藏  举报

导航