重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
很多初次接触到python的小伙伴可能并不理解闭包是什么,为什么有闭包,闭包有什么用,那么今天博主就从这三点来为大家讲解一下python的闭包
为成都等地区用户提供了全套网页设计制作服务,及成都网站建设行业解决方案。主营业务为成都网站建设、成都做网站、成都网站设计,以传统方式定制建设网站,并提供域名空间备案等一条龙服务,秉承以专业、用心的态度为用户提供真诚的服务。我们深信只要达到每一位用户的要求,就会得到认可,从而选择与我们长期合作。这样,我们也可以走得更远!
一、闭包是什么
官方定义:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
我的理解
在一个函数的内部嵌套了一个函数,并且这个函数引用了外部函数中的局部变量,那么这个内部函数就称为闭包
Python 中的闭包
Python 中闭包的实现得益于Python是一门面向对象编程的语言,函数在Python中也不例外,在Python中函数也是作为对象(函数对象)的方式进行存在
下面我们来看一个例子你就清楚了:
在这里我们首先定义了一个函数,然后我们使用Python中的内部方法(type),type(args)的作用是查看传入参数args的类型,我们可以看到func的类型就是一个函数对象
我们都知道函数是可以返回值的,所以在Python中函数对象也可以作为值进行返回,这样就为闭包的实现提供了基础.
下面我们来看一下Python的闭包实现过程:
在这里我们首先定义了一个名为outer的函数,然后在这个函数的内部我们又定义了一个名为"out_value"的变量,然后又在函数的内部定义了一个名为inner的内部函数,在inner函数内我们又引用了外部函数(outer)的变量,这样我们就称inner为闭包
我们再来看看闭包的一些属性
我们调用并执行outer函数并将其返回值赋给变量f,我们通过打印可以看到,变量f的类型为函数类型,因为是函数对象所以f必然是具有 ‘_call_’ 属性的(就是函数后面的() 也就是将函数执行) 然后我们再来看f变量的名称,可以看到f是名为inner的函数对象,为什么会这样是因为,在outer函数执行完毕后,在其返回的时候我们将在outer函数内部定义的inner函数对象进行返回了,所以f变量的名称为inner
我们既然了解了python中闭包的基本实现方法,那我们再来多看几个例子:
上面这个例子大家在没有看答案的情况下,大家猜一下结果会是怎样呢,可能有部分小伙伴已经看透了这其中的猫腻,那部分没有看出的小伙伴心中的结果是不是
0
1
2
这样呢? 那我这里就不卖关子了,其实上面的这种结果是错误的,正确的结果是:
2
2
2
为什么会这样呢?
那是因为在outer函数内,inner相对于outer来说其只是内部定义的一个函数,变量 i 的作用域还是在outer中,所以
现在inner对于outer来说并不是闭包,所以 i 的改变是可以改变inner内部变量i的值的.当循环最后执行完毕时,i变量的值是2 这时i就不会改变了,然后在返回的是这个闭包列表,所以打印出来的应该全是2
那么我们再来看一个例子,假如现在我有一个需求,内容是我需要有一个函数,我在调用这个函数时打印出当前是第几次调用,如果了解迭代器的小伙伴应该实现起来很轻松,但是这里需要用闭包的方式实现这个函数,怎么实现呢.
可能有小伙伴就有了下面的思路:
郑州人流专科医院 http://www.03912316666.com/
但是很遗憾的告诉你,这样的做法是错误的,不妨我们来看一下运行结果:
分析错误原因,大概的意思就是 局部变量“cnt”在赋值之前引用 ,它告诉我们不能在引用后进行赋值,因为这个变量的作用域还是在外部函数内的,那怎样解决这样的问题呢,Python就提供了很好的一个保留字用来声明 非局部变量( nonlocal 关键字)
我们来看一下修改后的执行效果:
可以看到通过nonlocal 声明变量cnt之后就得到了我们想要的结果
下面是官方对闭包过程中变量作用域的一些解释:
““Cell” objects are used to implement variables referenced by multiple scopes. For each such variable, a cell object is created to store the value; the local variables of each stack frame that references the value contains a reference to the cells from outer scopes which also use that variable. When the value is accessed, the value contained in the cell is used instead of the cell object itself. This de-referencing of the cell object requires support from the generated byte-code; these are not automatically de-referenced when accessed. Cell objects are not likely to be useful elsewhere.”
“Cell”对象用于实现由多个作用域引用的变量。对于每个这样的变量,创建一个cell对象来存储值;引用该值的每个堆栈帧的局部变量包含对来自外部作用域的单元格的引用,外部作用域也使用该变量。当访问该值时,将使用单元格中包含的值,而不是单元格对象本身。单元格对象的取消引用需要生成的字节码的支持;这些不会在访问时自动取消引用。单元格对象不太可能在其他地方有用。
这样大家应该就明白了.
二、为什么要有闭包
闭包避免了使用全局变量,此外,闭包允许将函数与其所操作的某些数据(环境)关连起来。这一点与面向对象编程是非常类似的,在面对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。
一般来说,当对象中只有一个方法时,这时使用闭包是更好的选择。
下面我们使用两种不同的方法来实现同一种需求,在比较下就可以知道那种方式更有优越性:
需求: 我们有一个名为Number 的数字对象,我们要对其实现加法运算的法则
用类来实现:
用闭包来实现
我们可以看到用闭包的方式来实现时可以使得代码更为的简便,这就是为什要使用闭包的原因.
闭包的用处 ——装饰器
既然前面讲了这么多闭包的知识,和应用,那么这里就来说一下闭包在Python中最大的一个用处,那就是"装饰器".
一听这个名字就知道装饰器的作用,装饰器嘛那肯定是用来装饰的嘛,那它是用来装饰什么的呢,其实就是用来装饰Python中的一些对象的.
可能小伙伴在了解装饰器前,在接触到类的时候就看见有的类方法上面有一些特殊的符号.
像这样的
像这样用 @ 符号进行修饰的关键字其实就是装饰器.
那么我们先来定义一个简单的装饰器来看一下:
上面的 decorator便是我们自定义的一个装饰器,在下面我们定义的f函数我们对其使用了装饰器进行装饰,
我们发现原本f函数内只有 “>>>> 正在执行” 这一行打印信息,但经过装饰器装饰后,就变成了两行的打印信息,
是不是很神奇,凡是都是有原因的,现在就来揭秘一下这里面的玄机.
其实 @ 符号在这里的作用就是 让程序自动执行一条这样的语句 “f = decorator(f)” 不妨我们来看看就知道了:
咦 我们发现我们定义的函数f它的名字被换成了wrapper,这不正是我们装饰器函数返回闭包嘛,这样结合前面的闭包知识,大家发现装饰器其实也没那么难嘛.
好了我们再来看一些更复杂的装饰器加深理解:
带参的装饰器:
上面我们实现来在装饰器中传入参数的做法,我们为当前的函数取了一个名字,然后在执行的时候将其打印出来,我们发现要进行传参时,我们整个函数的嵌套变为了三层,当然和前面不带参的装饰器的工作机理是差不多的,换汤不换药. 带参数的装饰器无非在使用的过程中进行了如下的操作 f = set_name(“Nick”)(f) 这样一说应该就明白了.
装饰器需要装饰的函数带参问题:
(1) 第一种做法:
显然这样的做法做出来的装饰器通用性并不强,如果我们需要装饰的函数参数一发生变化那我们的装饰器就不能使用了,这肯定是我们不能接受的.
(1) 第二种做法:
和前一种方法效果一样,但是使用包裹参数和包裹关键字参数进行传参,通用性就强很多了,所以推荐这样写.
我们在讲第一个装饰器的时候大家就发现了一个问题,那就是函数经过装饰器修饰后,原来的函数名称就发生了改变,统一的都命名成了我们装饰器内部定义的函数名,那我如果还是想要原来的函数名怎么办呢,别急有下面的第一种做法:
这样做其实就是强行改变其对象的名称,这样的做法肯定就显得不是那么优雅了,在Python中也是不推崇的.
我们再来看下面一种更为优雅的写法:
这下就对了,正如老爹所说我们要用魔法来对付魔法,这里用装饰器的来解决装饰器的问题,就显得优雅许多了,
如果感兴趣的小伙伴不妨自己思考一下functools.wraps 这个装饰器的实现思路,然后自己动手实现一次,相信对你学习装饰器有一定的帮助.这里介于篇幅的原因就不讲解实现的思路了.
下面来最后一个例子,我们用类的方式来实现一次装饰器看看:
对于类中的_call_ 方法在前面已经提过在这里就不讲了,这里的装饰器的调用过程和前面第一种装饰器的调用过程是一样的 f = MyDecorator(f) 相当于f就是一个由MyDecorator类实例化出来的一个对象,当这个对象f在执行f()时就触发了MyDecorator中的__call__方法.