Python 装饰器详解(下)

2021/12/11 1:17:38

本文主要是介绍Python 装饰器详解(下),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Python 装饰器详解(下)

转自:https://blog.csdn.net/qq_27825451/article/details/84627016,博主仅对其中 demo 实现中不适合python3 版本的语法进行修改,并微调了排版,本转载博客全部例程博主均已亲测可行。

Python 3.8.5

ubuntu 18.04

声明:此文章为,python装饰器详解——下篇,上一篇文章中,即详解装饰器——中篇 ,已经详细讲解了两大类装饰器,即函数装饰器、类装饰器的应用实例,并且分析了它们在运行的过程中的本质,给出了类装饰器的一般模板,本文将以实际例子为依托,讲解剩下的两个内容(闭包和装饰器的嵌套),其中,闭包是重点,包括闭包的诞生背景,闭包的定义、作用、与装饰器的关系与区别。该系列文章共分为 上、中、下 三篇。此为第三篇。

一、闭包诞生的背景——closure

1. 一个意想不到的窘境

很多的语言都存在闭包(closure),我们也常常听起这样的概念,但是你真的理解它了吗?东它的本质吗?在讲闭包之前,我打算从一个简单的情况说起。请先看一个例子:

func_list = []
for i in range(3):
    def myfunc(a):
        return i+a
    func_list.append(myfunc)  #定义三个函数,将三个函数存放在一个列表中
 
for f in func_list:           #调用列表中的三个函数
        print(f(1))

上面的运行结果是1 2 3 吗?但是真是的运行结果确实3 3 3。这是为什么?粗略的分析,第一个函数返回的应该是0+1,第二个返回的应该是1+1 ,第三个返回的应该是 2+1 啊,那为什么会出现这样的结果呢?从结果上分析,应该三个函数都是返回的 2+1,这是为什么呢?因为函数定义在循环内部,虽然每一次看起来好像 i 分别为 0、1、2,实际上因为函数是没有办法去保存这个变化的i 的,也就是说,i,是在函数外面发生变化的,函数里面的i会一直随着i的变化而变化,直到最终这个i不变化了,那函数里面的i是多少就是多少了。总结起来就一句话:

循环体内定义的函数是无法保存循环执行过程中的不停变化的外部变量的,即普通函数无法保存运行环境!还是不理解?

再看一个简单的例子:

a=100
 
def myfunc(b):
    return a+b
 
print(myfunc(200))
 
a=200
print(myfunc(200))

上面的代码大家都懂,运行结果为300 400。我们可以发现,因为函数内部有用到外面的a,所以函数运行的结果会随着这个a的变化而变化,直到外面的a不变了为止,否则光函数传递的参数是确定的还不够,还要取决于a。我们用两个比较通俗的层面去理解:

  1. 函数内部使用到了a,b,但是a却不是函数本身具备的财产,我虽然可以使用,但是我却不能决定它,a变化了,函数的结果就跟着变化了,直到a取最终的值,否则函数都是变化的。(你不确定,我就永远没有办法确定,你虽然就在我我身边,但是我却不能真正掌控你,这种感觉难道不难受吗?)

  2. 用书面语言说,函数没有办法保存它的运行环境,什么意思,在上面的两个例子里面,函数的运行环境都是这个模块(即py文件),也就是说,在这个运行环境里面的一切,函数都是没有办法做主的,函数能够做主只有他自身的局部作用域(包括形参)。

2. 窘境的解决办法

func_list = []
for i in range(3):
    def decorator(i):      #定义一个外层函数,这里之所以使用decorator,是为了后面与“装饰器进行比较
        def wrapper(a):    #定义一个内层函数,定义为wrapper是为了后面的比较
            return i + a
        return wrapper
    func_list.append(decorator(i))  
        
for f in func_list:
    print(f(1))

运行结果为 1 2 3 。关于为什么后面再详细讲解,这里先提供一种解决思路。

二、闭包的定义及应用

1. 闭包的定义

在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。—— 维基百科

2. 闭包的作用

闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性(保存运行环境与变量状态

3. 闭包的特征

上面的描述还是不够精炼,有没有几个特别的特征,让人一眼就看出来它就是闭包呢?

  1. 必须要有函数的嵌套。而且外层函数必须返回内层函数,但是内层函数可以不返回值,也可以返回值;外层函数给内层函数提供了一个 “包装起来的运行环境”,在这个“包装的”运行环境里面,内层函数可以完全自己做主。这也是称之为闭包的原因了。

  2. 内层函数一定要用到外层函数中定义的变量。如果只满足了特征(1),也不算是闭包,一定要用到外层“包装函数”的变量,这些变量称之为 “自由变量”

3. 闭包的代码解析

依然以上面的那么例子而言,我们提出了解决窘境的办法,那我们现在来解释这个解决办法到底做了什么工作。

func_list = []
for i in range(3):
    def decorator(i):      #定义一个外层函数,这里之所以使用decorator,是为了后面与“装饰器进行比较
        def wrapper(a):    #定义一个内层函数,定义为wrapper是为了后面的比较
            return i + a
        return wrapper
    func_list.append(decorator(i))  
        
for f in func_list:
    print(f(1))

这里列表中存出的就是三个包装函数decorator(1)decorator(2)decorator(3),其实相当于三个如下的定义:

def decorator(i=1):
    def wrapper(a):
        return i+a

因为这里wrapper的运行环境为decorator,不再是全局的环境,所以在wrapper的环境中,i 是固定的,不会再变化,故而当然能够自己做主了。

三、闭包的细节

首先明确闭包的两个核心特征:函数嵌套自由变量

其次明确闭包的两个核心功能:保存函数的运行环境状态保存闭包环境内的局部变量

1. 闭包的细节实现

看一个简单的闭包的例子,为了与前面的系列文章(中篇)的装饰器进行比较,这里也采用中篇中的案例,我要为一个两数相加的运算加密:

def decorator(c):  #外层函数,产生包装环境——即闭包
    d=200           #c d 都是包装环境中的局部变量——即自由变量
    def wrapper(a,b):  #内层函数
        return (a+b)*c/d
    return wrapper
 
wrapper=decorator(150)
print(wrapper(100,300))

运行结果为:300.0

  1. **为什么说它保存了函数的运行环境?**这里针对函数是内层函数即wrapper,它的运行环境是decorator提供的,也就是说decorator的环境是保存的,什么意思呢,其实就是通过一句话,

    wrapper=decorator(150)
    

    也就是说,这里wrapper运行所依赖的 c 就固定是150了,d 就固定是200了,不会再改变,无论我再给wrapper 传递什么参数,cd 是不会在变化的。当然如果我重新再执行一次wrapper=decorator(250),相当于是又创建了一个新的包装环境,这里的 c 就是250了。

  2. **为什么说它能够保存闭包函数内的局部变量?**众所周知,函数的局部变量会随着函数的调用结束而销毁,那么为什么局部变量能够保存呢?这里所说的局部变量指的是闭包函数的局部变量,即上面的 cd。也就是说,我这里的 cd 是保存着的,即使我已经执行wrapper(100,300)执行完毕。

2. 自由变量的查看

我们说闭包函数的局部变量是保存着的,那如何查看呢?我们可以通过内层函数的一个属性__closure__查看。

print(wrapper.__closure__)
print(wrapper.__closure__[0].cell_contents)
print(wrapper.__closure__[1].cell_contents)

结果如下:

(<cell at 0x7f2c886802b0: int object at 0x5558ee7e6fe0>, <cell at 0x7f2c88680370: int object at 0x5558ee7e7620>)
150
200

可以看到 __closure__ 属性返回一个元组,而150和200则分别对应自由变量 cd

总结:内层函数的__closure__属性返回一个元组;通过 wrapper.__closure__[i].cell_contents 查看第几个自由变量的值

注意:如果闭包函数没有返回wrapper,即外层函数没有返回内层函数,此时内层函数是没有__closure__属性的。

总结:现在可以体会为什么说闭包保存局部变量了吧,这里的c d 作为局部变量,在函数调用结束后还能够查看到它的值,这还不是保存,那什么是保存呢?

3. 闭包的一般模板

def decorator(*arg,**kargs):  #外层函数,产生包装环境——即闭包
    #自由变量区域                 # 包含形参,都是包装环境中的局部变量——即自由变量
    def wrapper(a,b):  #内层函数
        return (a+b)*c/d
    return wrapper
 
wrapper=decorator(150)      #创建唯一的闭包环境
wrapper(100,300)            #内层函数的调用

四、闭包与装饰器的比较

1. 相同点

  1. 都是函数的嵌套,分为外层函数和内层函数,而且外层函数要返回内层函数

  2. 代码的实现逻辑大同小异

  3. 二者都可以实现增加额外功能的目的——比如上面的“加法加密运算”

2. 不同点

  1. 外层函数不同,装饰器的外层函数称之为decorator,闭包的外层函数称之为闭包函数closure

  2. 外层函数的目的不同,装饰器的外层函数主要是提供函数形参function,闭包的形参主要目的是提供自由变量。

  3. 二者的特征不一样。装饰器的外层函数可以不提供自由变量,但是闭包的的外层函数一定要提供自由变量,因为如果不提供自由变量,必报的存在就毫无意义了,即内层函数所依赖的变量却在闭包中根本没有,那还要闭包干什么?

  4. 二者的主要目的不同。装饰器的目的:代码重用+额外功能。闭包的主要目的:保存函数的运行环境+保存闭包的局部变量。虽然二者可以有一些交集。

  5. 闭包和装饰器本质上还是不一样的,但是从形式上来说,大致可以认为闭包是装饰器的子集。记住:仅仅是从形式上哦

3. 如何理解“闭包”与“装饰器”的本质不一样,但是形式类似?

关于形式类似,这里就不说了,参见前面的两篇文章和这篇文章里面的模板即可,发现他们长得很像。

为什么说本质不一样?

  1. 因为对与装饰器而言,我必须要给外层函数 decorator 传递一个基本参数 function,只有这样,我才可以写成function=decorator(function)或者是 @decorator 的形式,如果没有这个参数,会显示以下错误:

     decorator() takes 0 positional arguments but 1 was given
    

    decorator 我必须要定义一个 function 参数,否则就会显示定义没有参数,但给了它一个参数这种错误,因为 function=decorator(function) 或者是 @decorator 这就相当于给了他一个参数。

    不仅如此,装饰器会改变函数 function 本身的 __name__ 属性,参见前文。

  2. 但是对于闭包,外层函数就没有这些要求,也不是一定要定义一个 function 参数,甚至我也可以不定义参数。至于两者的本质区别,学懂了的小伙伴应该可以自己好好体会了。

五、装饰器的嵌套

关于装饰器的多层嵌套,理解起来相对于比较复杂,本文先做一个预告,将在系列文章的下一篇,也就是第四篇进行深入详解,有需要的可以关注一下。
传送门:https://blog.csdn.net/qq_27825451/article/details/102457152。



这篇关于Python 装饰器详解(下)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程