首页 > 技术文章 > Python 闭包小记

weijiutao 2019-04-02 13:02 原文

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

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

 

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

 

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

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

2、闭包函数返回的那个函数必须引用外部变量(一般不能是全局变量),而返回的那个函数内部不一定要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

一、函数中的作用域

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

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。若往外直至全局作用域都查找不到的话代码会抛异常。

 

注意:不管主函数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

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

接上面的代码块:

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__) # 抛出异常 

三、为何叫闭包?

 

先看代码:

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__属性中),不会再被修改。

 

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

 

四、闭包可以保存运行环境

 

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

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))


五、闭包的实际应用

 

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

 

 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、【闭包实现快速给不同项目记录日志】

 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闭包到底有什么作用

 

1、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的作用就是在“函数局部作用域”内声明表示一个全局变量,从而可以在函数内部修改全局变量的值(否则只能访问不能修改),而且函数内部改变的全局变量的值也会改变。

 

2、函数局部作用域

 

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

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

 

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

 

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

 

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

 

(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次 输出的结果是 4、7、10、13
13 j+=1

 

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

 

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

 

4、nonlocal关键字的作用

 

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

 

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

 

推荐阅读