查看原文
其他

Python高级编程——装饰器Decorator超详细讲解(下篇之python闭包详解)

草yang年华 机器学习与python集中营 2021-09-10

送你小心心记得关注我哦!!

进入正文

全文摘要

装饰器decorator,是python语言的重要特性,上面一章节详细讲解了python装饰器的中篇内容,主要讲解了python中常见的函数装饰器与类装饰器的互相装饰,分析了各个装饰器的本质到底是什么,简单说明了装饰器的缺点。本文将介绍装饰器系列文章下篇,详细介绍Python闭包的各部分类容,包括背景、定义、意义、与装饰器的区别等内容。

重新声明,python高级编程——装饰器系列文章一共分为上、中、下、补充篇,本文讲解下篇,补充篇会在后面发出,请记得持续关注哦!文章偏长,阅读全文约30min。

上篇请参考:Python高级编程——装饰器Decorator超详细讲解(中篇)

全文目录

01 python闭包诞生的背景——closure

    1.1 一个意料之外的窘境

    1.2 窘境的解决办法

02 python闭包的定义和应用

    2.1 闭包的定义

    2.2 闭包的作用

    2.3 闭包的特征

    2.4 闭包的代码解析

03 python闭包的实现细节

    3.1 闭包的实现细节

    3.2 自由变量的查看方法

    3.3 闭包的一般“模板”

04 闭包与装饰器的对比

05 多层装饰器与装饰器的嵌套(补充篇预告)


python装饰器详解(下篇)


此文章为,python装饰器详解——下篇,上一篇文章中,即详解装饰器——中篇 ,已经详细讲解了两大类装饰器,即函数装饰器、类装饰器的应用实例,并且分析了它们在运行的过程中的本质,给出了类装饰器的一般模板,本文将以实际例子为依托,讲解python闭包,闭包是很多函数是编程语言重点,包括闭包的诞生背景,闭包的定义、作用、与装饰器的关系与区别。该系列文章共分为 上、中、下 、补充四篇。此为第三篇。


01

python闭包诞生的背景 

01 python闭包诞生的背景1.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文件),也就是说,在这个运行环境里面的一切,函数都是没有办法做主的,函数能够做主只有他自身的局部作用域(包括形参)。

01 python函数装饰器1.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 。关于为什么后面再详细讲解,这里先提供一种解决思路。


02

闭包的定义和应用 

02 闭包的定义和应用2.1 闭包的定义


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

02 闭包的定义和应用2.2 闭包的作用


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


02 闭包的定义和应用2.3 闭包的特征


闭包的特征:

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

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

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


02 闭包的定义和应用2.4 闭包的代码解析


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


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 是固定的,不会再变化,故而当然能够自己做主了。


03

闭包的实现细节 


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

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

03 闭包的实现细节3.1 闭包的实现细节


看一个简单的闭包的例子,为了与前面的系列文章(中篇)的装饰器进行比较,这里也采用中篇中的案例,中篇文章,请参考:

Python高级编程——装饰器Decorator超详细讲解(中篇)

我要为一个两数相加的运算加密:

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传递什么参数,c和d是不会在变化的。当然如果我重新再执行一次wrapper=decorator(250),相当于是又创建了一个新的包装环境,这里的c就是250了。

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

03 闭包的实现细节3.2 自由变量如何查看


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

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

返回如下结果:

(<cell at 0x000001E1303168E8: int object at 0x000000005E5A7EB0>, <cell at 0x000001E130316738: int object at 0x000000005E5A84F0>)  #__closure__属性返回一个元组

150   #对应第一个自由变量c

200   #对应第二个自由变量d


★总结:

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

★注意:

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

★总结:

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

03 闭包的实现细节3.3 python闭包的一般模板


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

wrapper=decorator(150)      #创建唯一的闭包环境
wrapper(100,300)            #内层函数的调用


04

闭包与装饰器的对比 


 装饰器(decorator)闭包(lexical closure)

相同点

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

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

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

不同点

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

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

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

(4)二者的主要目的不同。装饰器的目的:代码重用+额外功能

闭包的主要目的:保存函数的运行环境+保存闭包的局部变量。虽然二者可以有一些交集。

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


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

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

Python高级编程——装饰器Decorator超详细讲解(中篇)

Python高级编程——装饰器Decorator超详细讲解(上篇)


 为什么说本质不一样?

(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参数,甚至我也可以不定义参数。至于两者的本质区别,学懂了的小伙伴应该可以自己好好体会了。



装饰器缺点

总结

本文从一个简单的问题作为引入,系统讲解了python的闭包的详细功能与代码实现,主要从三个方面入手即可:

python闭包的定义

python闭包的作用

python闭包的特征

补充:关于装饰器的嵌套,多层装饰器的运行原理、运行过程,我会在系列文章:Python高级编程——装饰器Decorator详解(补充篇)中继续讲解,有兴趣的继续关注!



下一篇预告:

多层装饰器的嵌套(补充篇)

闭包的多层装饰器的运行过程、多层装饰器是如何实现嵌套和跳转的、多层装饰器的代码实现等内容。

有兴趣的小伙伴持续关注哦!

····

电子书籍分享

《吴恩达机器学习中文版》

《动手学深度学习》

下载链接:

链接:https://pan.baidu.com/s/11OK1rxtsR0wEz36YfTpYHQ 

提取码:k0k6 

····

推 荐 阅 读

Python高级编程——装饰器Decorator超详细讲解(中篇)

Python高级编程——装饰器Decorator超详细讲解(上篇)

python开发者必备,史上最详细的 jupyter 教程——第三篇

python开发者必备,史上最详细的 ipython 教程——第一篇



2018/12/03

Monday

小伙伴们,看了这一篇关于python闭包的文章是不是有所收获呢,如果把你还有什么其他疑问,也可以添加我微信,或者是进群一起交流。另外公众号里面收藏有海量资源,如果你有需要,就添加我的公众号哦,里面分享有海量资源,包含各类数据、教程等,后面会有更多面经、资料、数据集等各类干货等着大家哦,重要的是全都是免费、无套路分享,有兴趣的小伙伴请持续关注!












: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存