Python-高级部分-闭包

Python闭包---装饰器

闭包

Python的闭包是一个很重要的知识点,和装饰器紧密联系,明白了闭包的原理,装饰器也就不难理解了。

在讲解闭包之前,首先要明白一个概念:Python中一切皆对象,包括函数在内。

之所以强调这样一个概念,就是因为,闭包的核心就是把函数当做对象进行传递,从而做到了在一个‘“封闭空间“里修改原本的函数的功能,并且不需要源码做出改变。

闭包,字面上很好理解,就是一个封闭的包裹,那这个包裹有多强呢?先来看一个示例

def outer():
    print('outer函数被调用。。。')

    # 定义内部函数
    def inner():
        print('inner函数被调用。。。')

    return inner

fun = outer()

print('------分割线-----')

fun()

代码执行结果:


outer函数被调用。。。

------分割线-----

inner函数被调用。。。


代码分析:

上面这个代码就是一个很简单的闭包,可以发现以下几点:

1、闭包是一个嵌套函数

2、外层函数将内部函数的引用作为返回值

上述代码在执行过程中,首先是调用了外部函数,但在执行过程中,内部函数并没有被调用,外部函数将内部函数名作为了外部函数的返回值。并将内部函数的引用赋给了变量 fun 。此时,fun 就指向了内部函数 inner  。因此在执行fun函数时,实际上是调用了inner函数。简单来说一句话:通过调用函数来得到一个函数。

由此可以看出,函数可以像一个变量一样赋值给其他变量,实际上就是将函数功能的引用当做了返回值并赋值给了另一个变量。

明白了该代码的执行流程,也就明白了闭包的原理。既然闭包是一个函数,那么这个闭包比普通函数好在哪呢?单单就上面一个例子,是无法看出来的。那就继续深入研究一下吧。。。

向闭包传递参数

def outer(k, b):
    print('outer函数被调用')
    
    def inner(x):
        y = k*x + b
        print('y结果为:%s' % y)
    
    return inner

fun = outer(3, 5)

fun(2)

代码执行结果:


outer函数被调用。。。

y结果为:11


代码分析:

以上是一个求解一条线上某一点的功能代码实现。

和上一个代码不同的是,我们给外部函数、内部函数都传递了参数。并且发现,内部函数可以访问到外部函数的变量 k,b。

通过这个示例我们可以看到,调用一个函数,得到一个新的函数,而这个新函数还可以访问外部函数的局部变量!

 

上面这个示例,换做普通函数也可以办到,只需要定义一个函数,传递三个参数即可,如果闭包仅此而已,那么闭包也就没有出现的必要了。因此,还需要在深一步研究闭包和普通函数的区别。

 

如果我们更改需求:求取另一条线上的点。y = 5x +10 上 x=3 的点的 y 值

 

普通函数做法:重新调用函数,传递三个参数 k,b,x。即:5,10,3

闭包做法:调用外层函数,传递两个值 5,10,得到一个新函数,调用新函数,传递 x,3。

乍一看。闭包好像更麻烦了。。。

别急,如果我们再次更改需求,需要求之前那一条线上的点。

普通函数:重新调用函数,传递三个参数。

闭包:不需要调用外部函数来得到内部函数了。因为之前调用的时候就已经把内部函数引用赋值给了 fun ,当下次需要再次调用时,可以直接调用该函数,只需啊哟传递一个 x 的值 3。

# 接着上一个示例代码运行

fun(3)

代码执行结果:


outer函数被调用。。。

y结果为:14


这样,我们就发现了普通函数和闭包的不同之处:

普通函数调用都是一次性的,一旦调用后,无法重复利用,每次都要进行完整的传参。而闭包则可以把返回的函数引用存储起来,重复利用,简化了传参过程,并且通过闭包获得的函数之间时相互独立,互不干扰。

细心地朋友可能会发现,内部函数访问外部函数的局部变量就像是一个函数在访问全局变量一样,的确,访问过程是类似的,但是要修改外部函数的变量的话,就不太一样了。

函数要修改全局变量时,需要用global关键字进行声明,而闭包的内部函数在修改外部函数的变量时,需要使用nonlocal关键字进行声明。

代码示例:

x = 20

def outer():
    x = 10
    def inner():
        nonlocal x
        print('x = %s' % x)
        x += 1
        print('x = %s' % x)
    return inner

fun = outer()
   
fun()

上述代码使用nonlocal关键字进行声明后,我们可以发现,x 的值是外部函数里面的 x 值,而不是全局变量中的x。

另外提醒一点:对于变量的 nonlocal 、global 声明,必须放在函数的开头部分。且两种声明不能一起使用。否则会报错。

 

至此,我们就明白了闭包的原理及使用,闭包和函数很像,两者都是适合自己的发挥空间,没有谁比谁好的说法,只有适不适合。但在实际应用中,闭包还是很常用的。

 

讲解完闭包的原理后,就可以讲解另一个Python的重要概念:装饰器

装饰器

装饰器之于Python,就如同指针之于C语言。不会指针,就不能说会C语言,不会装饰器,就不要说会Python。可见装饰器的重要性。

那么什么是装饰器呢?先来一个例子:

def outer(fun):
    print('outer函数被调用。。。')
    
    # 打印传进来的参数
    print(fun)
    
    def inner():
        print('inner函数被调用。。。')
        fun()
        print('inner函数调用完毕。。。')
    return inner

def A():
    print('普通函数 A 被调用。。。')


A = outer(A)
A()

print('-----分割线-----')

@outer  # Python 语法糖 此句相当于:B = outer(B)
def B():
    print('普通函数 B 被调用。。。')

print('-----分割线2-----')

B()

 

代码运行结果:


outer函数被调用。。。
<function A at 0x00000245DC3729D8>
inner函数被调用。。。
普通函数 A 被调用。。。
inner函数调用完毕。。。
-----分割线-----
outer函数被调用。。。
<function B at 0x00000245DC372AE8>

-----分割线2------
inner函数被调用。。。
普通函数 B 被调用。。。
inner函数调用完毕。。。


代码分析:

运行结果对于初学者可能有乱,但是请冷静分析。首先以分割线为界,将其分为两部分。首先是分析上半部分。

代码由上至下执行,首先是一个 outer 函数,这是一个闭包,接收一个参数 fun,函数定义在调用之前不会被执行,因此直接跳过,然后到达定义函数 A ,这是一个很贱单的打印函数。一样在被调用之前不会被执行,继续向下走。

A = outer(A)

这句代码包含两步:

1、先执行等号右边:调用函数 outer ,同时传递参数 A 。

2、将 outer 函数的返回值赋值给 A。

在执行第一步时,打印出了前两句,我们可以看到第二句显示的是打印函数A。由此,我们可以确定,outer 函数接收到的参数是函数 A 的引用。但也要注意到另一件事:outer 函数内部的 inner 函数并没有被调用!这是因为在执行 outer 函数时,inner 函数在里面只是被定义了而已,并没有被调用,因此没有执行。outer 函数的 return 语句,返回的是 inner 函数名,并不是 inner(),这一点一定要搞清楚。函数名是一个变量,它指向一个函数。但是一旦加上了 "()",就变成了调用函数,并将函数返回值作为该语句的结果。 因此 outer 函数反悔的是一个函数,并不是调用函数后的返回值。这一点和闭包是一样的:通过调用 outer 函数,得到了一个新的函数 inner 。

执行第二步:将 outer 函数返回的内部函数的引用,赋值给了变量 A。此时 A,不再指向原来的函数,而是指向了 inner 函数。

 

然后就是调用函数 A。

A()

这一步执行后。打印了三句话,然后分割线被打印出来。前面的分析也已经说明了。A 此时指向的是 inner 函数。因此调用函数 A,实际上就是调用了 inner 函数。于是就看到了 inner 函数中打印信息。同时应该注意到,inner 函数内部调用了一个函数

fun ,而这个函数执行的结果竟然是原来的函数 A 的打印信息!没错,我们发现了一件事:调用A函数,一样可以执行原来的函数A的功能,但在执行这个函数之前、之后都有新的功能出现!

 

如果你已经明白了闭包的原理,那么这个简单装饰器,应该也就很好理解了。inner 函数里调用了 fun 。之前我们也说过,闭包里面的函数可以访问到外部函数的局部变量,因此,这个fun变量实际上就是外部函数的接收参数 fun 。而这个 fun ,指向的就是我们之前调用时传进去的 A ,因此 fun 指向了原来的函数 A 。因此在 inner 函数中调用 fun 时,实际上就是调用了 A 原来的函数。

至此,实例代码的前半段就分析完毕了,通过这段解析,我们需要总结一下这一段和闭包的不同之处。即同样是使用到了闭包,为什么我们要叫它装饰器呢?

1、其实装饰器和闭包是一样的,只不过我们传进去的参数是一个函数的引用。而不是其他的数据类型。

2、当闭包的外部函数接受了一个函数的引用时,就相当于把外面的函数的引用存储了起来。因此我们就可以就可以让外面的函数名来接收闭包函数的返回值,而不用担心原来的函数引用丢失而无法找到原来的函数。这样就实现了同样是调用原来的函数,但却增加了新功能!而这也就是装饰器的核心所在。

接下来看下半段

@outer

这个写法在Python中叫做语法糖,其实就是一种简便写法,同时提高了可读性,让人一看就能明白这是一个装饰器。

先暂时不管这个写法是怎么执行的,先看结果!

从运行结果中可以看出,通过这种写法运行结果和上面的 A 函数的运行情况完全一样。因此我们可以直接将其看做和上面的写法是一种等同效果。就如同代码中 @outer 后面的注释一样,这种写法其实是另一种写法的替代而已。这可以从打印结果中的 -----分割线2------ 中可以看出:

1、我们在调用 B 函数之前,outer 函数就已经被调用了。

2、调用 B 函数的结果其实就是调用了 inner 函数的结果。说明 B 函数已经被“装饰”了,指向了 inner 函数。而这个改变就是在语法糖那里执行了。

以上就是装饰器的解释说明,总结一下就是:

1、装饰器就是一个闭包,外部函数接收一个函数作为参数,并且返回内部定义的函数。

2、在内部函数调用外部函数的参数指向的函数。并且可以在函数调用前后进行添加额外功能。

至此,我们就明白了什么是装饰器,以及装饰器的执行过程。但以上仅仅是一个简单的装饰器。既然是对函数进行装饰,而函数又分有四种类型。有无参数、有无返回值。上面仅仅是对一个无参数、无返回值的函数进行装饰。接下来我们就来看看如何对其他类型的函数进行装饰。

对不同类型的函数进行装饰

1、对有参数、无返回值函数进行装饰

def outer(fun):
    print('outer函数被调用。。。')
    
    def inner(a):
        print('传进来的参数是 %s' % a)
        fun(a)

    return inner

@outer
def B(x):
    res = x**2
    print('%s 的平方是 %s' % (x, res) )

print('-----分割线------')

B(3)

代码运行结果:


outer函数被调用。。。
-----分割线------
传进来的参数是 3
3 的平方是 9


代码分析:

以上代码就是对一个有参数的函数 B 进行装饰。前面的分析中,我们已经明白了装饰器实际上就是改变了原来的函数的指向,从而在一个新的函数中运行原来的函数。并添加功能。而现在,多出来了一个问题,现在这个函数需要参数,那么我们如何将这个参数准确的传进去,让原来的函数正常运行。这就需要对装饰器的运行过程非常清楚才行。

程序由上至下:

1、定义一个装饰器 outer

2、定义一个函数 B 并用 outer 对其进行装饰。此时调用了 outer 函数,然后改变了变量 B 的指向,使其指向了 inner 函数。(注意,函数指向发生变化,也就影响了后续的传参)

3、当后面调用了 B 函数时,实际上是调用了 inner 函数,而 inner 函数中有一个变量 fun 指向了原来的 B 函数,而调用这个函数需要一个参数。那么我们就需要像它提供这个参数才行,那么这个参数怎么传进去呢?

刚刚我们已经说明了,调用 B 函数实际上就是调用 inner,那么此时就需要把参数传递给 inner 函数,然后再让 inner 函数把它从何外面接收来的参数传递给 fun 函数。因此需要 inner 函数定义对应的形参来接收,并在调用 fun 函数时把参数传递给 fun 函数。因为 fun 指向的是原本的 B 函数,因此就需要定义对应的参数来接收参数。

至此,整个程序运行结束。从以上分析中我们不难发现,当对一个函数进行装饰后,要对其传递参数,就需要闭包的内部函数、内部调用的函数 添加对应的参数来接收才行。

对有返回值的函数进行装饰,并获取返回值

很多函数都会带有返回值,我们希望函数完成一定功能的同时,也需要获取函数的结果,以便后续使用。这就需要我们懂得如何获取被装饰的函数的返回值

代码示例:

def outer(fun):
    print('outer函数被调用。。。')
    
    def inner(a):
        print('传进来的参数是 %s' % a)
        res = fun(a)
        # 将函数运行的结果的返回值,作为返回值返回
        return res

    return inner

@outer
def B(x):
    res = x**2
    print('%s 的平方是 %s' % res)
    return res

print('-----分割线------')

A = B(3)

print('A = %s' % A)

代码运行结果:


outer函数被调用。。。
-----分割线------
传进来的参数是 3
3 的平方是 9
A = 9


代码分析:

这个代码是在上一个示例中做出一些修改的例子。在函数中添加了返回值。

函数的执行过程应该就不需要过多赘述了,和上一个例子没有什么区别,仅仅是添加了返回值。

在这个例子中,需要注意一点就是,外部函数 B 添加了返回值后,要想得到运行之后的返回值,就需要在 inner 函数内部将该 fun 函数的返回值作为 inner 的的返回值,这样才会得到原本 B 函数的执行结果。

至此,对有参数的函数、有返回值的函数进行装饰的过程和使用我们就讲解完了。但是我们也会发现一件事,被装饰后的函数依然是一个函数,那么我们是否可以继续进行装饰呢?答案是可以的!

对函数进行多重装饰。

不说废话,上代码:

def outer1(fun):
    print('outer1 函数被调用。。。')
    
    def inner():
        print('inner1---函数被调用。。。')

        fun()

        print('inner1---函数调用完毕。。。')
    return inner

def outer2(fun):
    print('outer2 函数被调用。。。')
    
    def inner():
        print('inner2...函数被调用。。。')

        fun()

        print('inner2...函数调用完毕。。。')
    return inner


@outer1
@outer2
def A():
    print('A函数被调用。。。')

print('-----分割线------')

A()

代码运行结果:


outer2 函数被调用。。。
outer1 函数被调用。。。
-----分割线------
inner1---函数被调用。。。
inner2...函数被调用。。。
A函数被调用。。。
inner2...函数调用完毕。。。
inner1---函数调用完毕。。。


乍一看,有点乱,不慌,先从整体上看。首先肯定一点:一个函数确实可以被多次装饰!

还是以分割线为界

分割线以上是两层装饰,那么这就涉及到了一个先后顺序的问题,从结果来看,可以确定,装饰顺序是从下至上的,也可以说是“就近原则”。

分割线以下是调用 A 函数的结果,由于 A 被装饰了两次,那么理所当然会被添加“双重功能”,从结果上来看,运行顺序是由上至下,和装饰依顺序相反。其实只要明白了流程,就能够明白这是如何做到的的。打个形象的比喻,那就是寄快递了,快递员对你的物品进行层层包装,就是装饰器在对函数进行修饰,而对方收到快递拆开包装,就好比调用函数 A。寄包裹是由内向外包装,而拆包裹就是由外向内拆解。这和我们都看到结果是一致的。

在实际运用中,我们并不会纠结于这是如何实现的,而仅仅是懂得如何运用即可。就好比这个装饰器,他的作用就是用来装饰函数,我们只需要定义一个闭包,并接受函数作为参数,然后对目标函数进行装饰,即可实现对函数动态的添加功能。

带参数的装饰器

前面已经讲过,装饰器就是对一个函数就行装饰,使其添加额外的功能,但有的时候,这个额外功能具有不确定性。

例如:一个系统需要登录才能够使用,但是登录前需要验证身份,对于不同的用户,采取的验证手段会有所差别。这时候在对登录函数进行装饰的时候,就需要分情况考虑了。

代码示例:

def border(a):
    print('border函数被调用。。。')
    print('a = %s' % a)

    def outer(fun):
        print('outer函数被调用。。。')

        def inner():
            if a == 1:
                print('启动一号验证方案。。。')
                fun()
            else:
                print('启动二号验证方案。。。')
                fun()
        return inner

    return outer

@border(1)
def A():
    print('A类用户登录。。。')
            

print('-----分割线1------')

@border(2)
def B():
    print('B类用户登录。。。')


print('-----分割线2-----')

A()

print('-----分割线3-----')

B()

代码运行结果:


border函数被调用。。。
a = 1
outer函数被调用。。。
-----分割线1------
border函数被调用。。。
a = 2
outer函数被调用。。。
-----分割线2-----
启动一号验证方案。。。
A类用户登录。。。
-----分割线3-----
启动二号验证方案。。。
B类用户登录。。。


代码分析:

乍一看,有点眼花缭乱,不慌。

首先看整体,是一个双层嵌套函数,外面一层函数仅仅是接收一个参数,然后打印两句话,内部定义了一个函数,然后返回一个函数。

然后看内部函数,恩,没错,他就是前面讲过的装饰器。只不过在调用内部函数中的 fun 前,首先进行了判断,然后分情况运行了而已。。。而决定是哪种情况的决定参数,就是在进行装饰时调用的 border 函数传入的参数。

装饰器的原理明白了,这里需要注意的地方就只有一个点,就是 @ 符号后面的 border() 。

border是一个函数,加上了 “()”后,就变成了调用 border 函数,所以我们会看到border 的打印信息,但重点不在这,重点是他的返回值:outer 函数,没错,这是一个装饰器。装饰器有什么作用?--装饰函数。所以到底仍然是装饰器对函数进行装饰,但是我们这个装饰器是通过调用函数得来的。

所以一句 @border(1) ,执行了两步:

1、调用border函数获得返回值-----装饰器

2、把得到的装饰器对函数进行装饰。

因此我们才会看到,一次装饰,却打印了 border 函数、outer 函数的打印信息,因为它调用了两层函数。

另一个需要关注的终点就是,调用 border 函数时传递的参数。而这个参数就决定了将来执行 fun 函数时,需要使用哪种验证方案。

剩余的执行细节就不必多说了,明白了装饰器是如何执行的,那这里应该也就明白了。但有几点需要补充声明一下。

1、带参数的装饰器实际上就是在装饰器的外面又包裹了一层函数,这个函数同来接收选择性的参数,这个参数可以用于内部函数执行时的判断依据。同时这个最外层函数会把内部定义的一个装饰器返回,对函数进行修饰。

2、针对不同情况增加的功能不一样,这些功能可以封装在函数里面,根据不同的情况调用不同的函数即可,但是这个函数定义位置最好是定义在函数内部,可以是在 border 内,outer 外的位置,或者 outer 内、inner外,或者 inner内,但是笔者觉得定义在 border 外,outer 外 比较好。非常不建议定义在 inner 内,或者函数外面。因为前者会使得程序看起来很臃肿,而定义在推荐位置,程序框架看起来就比较清晰明了,额外添加的功能模块、装饰器、内部函数相互独立不会干扰。定义在外面,会把额外功能功能模块暴露在外,有点不安全,并且逻辑上也不太好,因为这些额外功能模块就是专属于这个装饰器的。因此最好能统一放在一起。在逻辑上也很好理解他们是一个整体模块,并且方便他人使用(仅仅需要导入该函数即可使用)

3、我们在函数装饰的时候就进行传参而不是在装饰之后调用 fun 函数的时候再进行传参是因为这样做可以遵循“开放-封闭”原则。如果我们舍弃最外层接收选择参数的函数,而直接使用装饰器进行装饰,并在调用内部函数的时候再传入选择性参数。这样就改变了原来的函数调用规则,需要进行多余的传参。这样可能会得到我们预料之外的结果,而且不利于我们的维护。

3.1、开放-封闭原则:开放就是指原本定义好的功能模块,我们可以直接拿来调用、增加功能。封闭就是指,我们可以用,但是不能对功能模块的原油内荣进行修改。就好比你借用别人的东西,你可以用,但是不能搞破坏。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值