Python 闭包小记

闭包就是可以读取其余函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,因此闭包能够理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部链接起来的桥梁。javascript

上面这段话引自百度百科,涛涛以为对于闭包的解释通俗易懂,言简意赅。java

 

对于 Python ,涛涛目前研究不是很深,尚在学习当中,因此如下对 Python 闭包的解释案例多引自其余大神,本身也就搬搬砖,特此写下这篇,巩固一下本身的知识储备。python

 

首先列出闭包的必要条件:闭包

一、闭包函数必须返回一个函数对象app

二、闭包函数返回的那个函数必须引用外部变量(通常不能是全局变量),而返回的那个函数内部不必定要return函数

 

如下是几个经典的闭包案例:学习

 1 # ENV>>> Python 3.6
 2     # NO.1
 3     def line_conf(a, b):  4         def line(x):  5             return a * x + b  6         return line  7     
 8     # NO.2
 9     def line_conf(): 10         a = 1
11         b = 2
12  
13         def line(x): 14             print(a * x + b) 15         return line 16  
17     # NO.3
18     def _line_(a,b): 19         def line_c(c): 20             def line(x): 21                 return a*(x**2)+b*x+c 22             return line 23         return line_c

1、函数中的做用域this

Python中函数的做用域由def关键字界定,函数内的代码访问变量的方式是从其所在层级由内向外的,如“热身”中的第一段代码:spa

1   def line_conf(a, b): 2         def line(x): 3             return a * x + b 4         return line

 

嵌套函数line中的代码访问了a和b变量,line自己函数体内并不存在这两个变量,因此会逐级向外查找,往上走一层就找到了来自主函数line_conf传递的a, b。若往外直至全局做用域都查找不到的话代码会抛异常。debug

 

注意:无论主函数line_conf下面嵌套了多少个函数,这些函数都在其做用域内,均可以在line_conf做用域内被调用。

 

思考上面这段代码实现了什么功能?

1     #定义两条直线
2     line_A = line_conf(2, 1) #y=2x+b
3     line_B = line_conf(3, 2) #y=3x+2
4     
5     #打印x对应y的值
6     print(line_A(1)) #3
7     print(line_B(1)) #5

 

是否感受“哎哟,有点意思~”,更有意思的在后面呢。

 

如今不使用闭包,看看须要多少行代码实现这个功能:

1     def line_A(x): 2         return 2*x+1
3     def line_B(x): 4         return 3*x+2
5     
6     print(line_A(1)) #3
7     print(line_B(1)) #5

不包括print语句的代码是4行,闭包写法是6行,看起来有点不对劲啊?怎么闭包实现须要的代码量还多呢?别急,我如今有个需求:

 

 再定义100条直线!

 

那么如今谁的代码量更少呢?很明显这个是能够简单计算出来的,采用闭包的方式添加一条直线只须要加一行代码,而普通作法须要添两行代码,定义100条直线两种作法的代码量差为:100+6 -(100*2+4) = -98。须要注意的是,实际环境中定义的单个函数的代码量多达几十上百行,这时候闭包的做用就显现出来了,没错,大大提升了代码的可复用性!

 

注意:闭包函数引用的外部变量不必定就是其父函数的参数,也能够是父函数做用域内的任意变量,如“热身”中的第二段代码:

1 def line_conf(): 2         a = 1
3         b = 2
4  
5         def line(x): 6             print(a * x + b) 7         return line

2、如何显式地查看“闭包”

接上面的代码块:

1     L = line_conf() 2     print(line_conf().__closure__) #(<cell at 0x05BE3530: int object at 0x1DA2D1D0>,
3     # <cell at 0x05C4DDD0: int object at 0x1DA2D1E0>)
4     for i in line_conf().__closure__: #打印引用的外部变量值
5         print(i.cell_contents) #1 ; #2

__closure__属性返回的是一个元组对象,包含了闭包引用的外部变量。

 

 若主函数内的闭包不引用外部变量,就不存在闭包,主函数的_closure__属性永远为None:

 1     def line_conf():  2         a = 1
 3         b = 2
 4         def line(x):  5             print(x+1)  #<<<------
 6         return line  7     L = line_conf()  8     print(line_conf().__closure__) # None
 9     for i in line_conf().__closure__: #抛出异常
10         print(i.cell_contents)


若主函数没有return子函数,就不存在闭包,主函数不存在_closure__属性:

1 def line_conf(): 2         a = 1
3         b = 2
4         def line(x): 5             print(a*x+b) 6         return a+b #<<<------
7     L = line_conf() 8     print(line_conf().__closure__) # 抛出异常 

3、为什么叫闭包?

 

先看代码:

1     def line_conf(a): 2         b=1
3         def line(x): 4             return a * x + b 5         return line 6  
7     line_A = line_conf(2) 8     b=20
9     print(line_A(1))  # 3

如你所见,line_A对象做为line_conf返回的闭包对象,它引用了line_conf下的变量b=1,在print时,全局做用域下定义了新的b变量指向20,最终结果仍然引用的line_conf内的b。这是由于,闭包做为对象被返回时,它的引用变量就已经肯定(已经保存在它的__closure__属性中),不会再被修改。

 

 是的,闭包在被返回时,它的全部变量就已经固定,造成了一个封闭的对象,这个对象包含了其引用的全部外部、内部变量和表达式。固然,闭包的参数例外。

 

4、闭包能够保存运行环境

 

思考下面的代码会输出什么?

1     _list = [] 2     for i in range(3): 3         def func(a): 4             return i+a 5  _list.append(func) 6     for f in _list: 7         print(f(1))


1 , 2,  3吗?若是不是又该是什么呢?    结果是3, 3, 3 。

 

由于,在Python中,循环体内定义的函数是没法保存循环执行过程当中的不停变化的外部变量的,即普通函数没法保存运行环境!想要让上面的代码输出1, 2, 3并不难,“术业有专攻”,这种事情该让闭包来:

 1     _list = []  2     for i in range(3):  3         def func(i):  4             def f_closure(a):  # <<<---
 5                 return i + a  6             return f_closure  7         _list.append(func(i))  # <<<---
 8         
 9     for f in _list: 10         print(f(1))


5、闭包的实际应用

 

如今你已经逐渐领悟“闭包”了,趁热打铁,再来一个小例子:

 

 1     def who(name):  2         def do(what):  3             print(name, 'say:', what)  4  
 5         return do  6  
 7     lucy = who('lucy')  8     john = who('john')  9  
10     lucy('i want drink!') 11     lucy('i want eat !') 12     lucy('i want play !') 13     
14     john('i want play basketball') 15     john('i want to sleep with U,do U?') 16  
17     lucy("you just like a fool, but i got you!")

看到这里,你也能够试着本身写出一个简单的闭包函数。

 

OK,如今来看一个真正在实际环境中会用到的案例:

 

 一、【闭包实现快速给不一样项目记录日志】

 1     import logging  2     def log_header(logger_name):  3         logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(name)s] %(levelname)s %(message)s',  4                             datefmt='%Y-%m-%d %H:%M:%S')  5         logger = logging.getLogger(logger_name)  6  
 7         def _logging(something,level):  8             if level == 'debug':  9  logger.debug(something) 10             elif level == 'warning': 11  logger.warning(something) 12             elif level == 'error': 13  logger.error(something) 14             else: 15                 raise Exception("I dont know what you want to do?" ) 16         return _logging 17  
18     project_1_logging = log_header('project_1') 19  
20     project_2_logging = log_header('project_2') 21  
22     def project_1(): 23  
24         #do something
25         project_1_logging('this is a debug info','debug') 26         #do something
27         project_1_logging('this is a warning info','warning') 28         # do something
29         project_1_logging('this is a error info','error') 30  
31     def project_2(): 32  
33         # do something
34         project_2_logging('this is a debug info','debug') 35         # do something
36         project_2_logging('this is a warning info','warning') 37         # do something
38         project_2_logging('this is a critical info','error') 39  
40  project_1() 41     project_2()

 

 

1 #输出
2 2018-05-26 22:56:23 [project_1] DEBUG  this is a debug info 3 2018-05-26 22:56:23 [project_1] WARNING  this is a warning info 4 2018-05-26 22:56:23 [project_1] ERROR  this is a error info 5 2018-05-26 22:56:23 [project_2] DEBUG  this is a debug info 6 2018-05-26 22:56:23 [project_2] WARNING  this is a warning info 7 2018-05-26 22:56:23 [project_2] ERROR  this is a critical info

这段代码实现了给不一样项目logging的功能,只需在你想要logging的位置添加一行代码便可。

 

扩展: python中的装饰器特性就是利用闭包实现的,只不过用了@做为语法糖,使写法更简洁。若是掌握了闭包,接下来就去看一下装饰器,也会很快掌握的。

python闭包到底有什么做用

 

一、global关键字的做用

若是在函数中须要修改全局变量,则须要使用该关键字,具体参见下面例子。

1 variable=100
2 def function(): 3     print(variable) #在函数内不对全局变量修改,直接访问是没问题的,不会报错
4 function() #输出100

 

1 variable=100
2 def function(): 3     result = variable + 111
4     print(result) #在函数内不对全局变量修改,直接访问是没问题的,不会报错
5 function() #输出100

 

1 variable = 100
2 def function(): 3     variable += 111
4     print(variable)  # 显示local variable 'variable' referenced before assignment。
5                      # 即在函数局部做用域中直接改变全局变量的值会报错
6 function()

 

1 variable = 100
2 def function(): 3     variable = 1000  # 此时修改variable变量的值不会报错,由于已经在函数局部做用域内从新定义variable,会覆盖全局variable。
4     variable += 111
5     print(variable) 6 function()  # 输出1111
7 print(variable)  # 输出100,虽然函数内部从新覆盖了variable,可是全局variable并未变,依然仍是100

 

那若是再也不函数内部从新为全局变量赋值,又想改变全局变量的值,应该怎么作呢?这就要使用global关键字了,以下。

 

1 variable = 100
2 def function(): 3     global variable  # 使用global关键字,代表variable是全局的,此时就能够了直接在函数局部做用域内改变variable的值了
4     variable += 111
5     print(variable)  # 输出211
6 function() 7 print(variable)  # 输出211,这和上面的不同了,发现全局变量variable自己也改变了

 

总结:global的做用就是在“函数局部做用域”内声明表示一个全局变量,从而能够在函数内部修改全局变量的值(不然只能访问不能修改),并且函数内部改变的全局变量的值也会改变。

 

二、函数局部做用域

 

函数的局部做用域是不可以保存信息的,即在函数内部声明变量在函数调用结束以后函数里面保存的信息就被销毁了,包括函数的参数,以下。

1 def fun(step): 2     num = 1
3     num += step 4     print(num) 5 i = 1
6 while (i < 5): 7     fun(3)  # 连续调用函数4次,每次输出的值都是4,即3+1,这说明每次调用fun函数以后,函数内部定义局部变量num就被销毁了,
8 # 没有保存下来,说明函数的局部做用域被销毁了。那若是要保存函数的局部变量,怎么办呢?引入“闭包”。
9 i += 1

 

三、闭包——装饰器的本质也是闭包

 

“闭包”的本质就是函数的嵌套定义,即在函数内部再定义函数,以下所示。

 

“闭包”有两种不一样的方式,第一种是在函数内部就“直接调用了”;第二种是“返回一个函数名称”。

 

(1)第一种形式——直接调用

1 def Maker(name): 2     num=100
3     def func1(weight,height,age): 4         weight+=1
5         height+=1
6         age+=1
7         print(name,weight,height,age) 8 func1(100,200,300) #在内部就直接调用“内部函数”
9 Maker('feifei') #调用外部函数,输出 feifei 101 201 301

(2)第二种形式——返回函数名称

 1 def Maker(name):  2     num=100
 3     def func1(weight,height,age):  4         weight+=1
 5         height+=1
 6         age+=1
 7         print(name,weight,height,age)  8     return func1 #此处不直接调用,而是返回函数名称(Python中一切皆对象)
 9 maker=Maker('feifei') #调用包装器
10 maker(100,200,300) #调用内部函数


(3)“闭包”的做用——保存函数的状态信息,使函数的局部变量信息依然能够保存下来,以下。

 

 1 def Maker(step): #包装器
 2     num=1
 3     def fun1(): #内部函数
 4         nonlocal num #nonlocal关键字的做用和前面的local是同样的,若是不使用该关键字,则不能再内部函数改变“外部变量”的值
 5         num=num+step #改变外部变量的值(若是只是访问外部变量,则不须要适用nonlocal)
 6         print(num)  7     return fun1  8 #=====================================#
 9 j=1
10 func2=Maker(3) #调用外部包装器
11 while(j<5): 12 func2() #调用内部函数4次 输出的结果是 四、七、十、13
13 j+=1

 

从上面的例子能够看出,外部装饰器函数的局部变量num=一、以及调用装饰器Maker(3)时候传入的参数step=3都被记忆了下来,因此才有1+3=四、4+3=七、7+3=十、10+3=13.

 

从这里能够看出,Maker函数虽然调用了,可是它的局部变量信息却被保存了下来,这就是“闭包”的最大的做用——保存局部信息不被销毁。

 

四、nonlocal关键字的做用

 

该关键字的做用和local的做用相似,就是让“内部函数”能够修改“外部函数(装饰器)”的局部变量值。详细信息这里不作讨论。

 

以上就是 Python 闭包学习记录的内容,在此特别感谢 CSDN 的 chaseSpace-L 和 LoveMIss-Y 两位大神的文章和解释!

相关文章
相关标签/搜索