关于Python闭包的一切

开发 后端
任何把函数当做一等对象的语言,它的设计者都要面对一个问题:作为一等对象的函数在某个作用域中定义,但是可能会在其他作用域中调用,如何处理自由变量?

[[402091]]

本文转载自微信公众号「dongfanger」,作者dongfanger。转载本文请联系dongfanger公众号。

任何把函数当做一等对象的语言,它的设计者都要面对一个问题:作为一等对象的函数在某个作用域中定义,但是可能会在其他作用域中调用,如何处理自由变量?

自由变量(free variable),未在局部作用域中绑定的变量。

为了解决这个问题,Python之父Guido Van Rossum设计了闭包,有如神来之笔,代码美学尽显。在讨论闭包之前,有必要先了解Python中的变量作用域。

变量作用域

先看一个全局变量和自由变量的示例:

  1. >>> b = 6 
  2. >>> def f1(a): 
  3. ...     print(a) 
  4. ...     print(b) 
  5. ...      
  6. >>> f1(3) 

函数体外的b为全局变量,函数体内的b为自由变量。因为自由变量b绑定到了全局变量,所以在函数f1()中能正确print。

如果稍微改一下,那么函数体内的b就会从自由变量变成局部变量:

  1. >>> b = 6 
  2. def f1(a): 
  3. ...     print(a) 
  4. ...     print(b) 
  5. ...     b = 9 
  6. ...      
  7. >>> f1(3) 
  8. Traceback (most recent call last): 
  9.   File "<input>", line 1, in <module> 
  10.   File "<input>", line 3, in f1 
  11. UnboundLocalError: local variable 'b' referenced before assignment 

在函数f1()后面加上b = 9报错:局部变量b在赋值前进行了引用。

这不是缺陷,而是Python设计:Python不要求声明变量,而是假定在函数定义体中赋值的变量是局部变量。

如果想让解释器把b当做全局变量,那么需要使用global声明:

  1. >>> b = 6 
  2. >>> def f1(a): 
  3. ...     global b 
  4. ...     print(a) 
  5. ...     print(b) 
  6. ...     b = 9 
  7. ...      
  8. >>> f1(3) 

闭包

回到文章开头的自由变量问题,假如有个叫做avg的函数,它的作用是计算系列值的均值,用类实现:

  1. class Averager(): 
  2.      
  3.     def __init__(self): 
  4.         self.series = [] 
  5.          
  6.     def __call__(self, new_value): 
  7.         self.series.append(new_value) 
  8.         total = sum(self.series) 
  9.         return totle / len(self.series) 
  10.  
  11. avg = Averager() 
  12. avg(10)  # 10.0 
  13. avg(11)  # 10.5 
  14. avg(12)  # 11.0 

类实现不存在自由变量问题,因为self.series是类属性。但是函数实现,进行函数嵌套时,问题就出现了:

  1. def make_averager(): 
  2.     series = [] 
  3.      
  4.     def averager(new_value): 
  5.         # series是自由变量 
  6.         series.append(new_value) 
  7.         total = sum(series) 
  8.         return totle / len(series) 
  9.      
  10.     return averager 
  11.  
  12. avg = make_averager() 
  13. avg(10)  # 10.0 
  14. avg(11)  # 10.5 
  15. avg(12)  # 11.0 

函数make_averager()在局部作用域中定义了series变量,它的内部函数averager()的自由变量series绑定了这个值。但是在调用avg(10)时,make_averager()函数已经return返回了,它的局部作用域也消失了。没有闭包的话,自由变量series一定会报错找不到定义。

那么闭包是怎么做的呢?闭包是一种函数,它会保留定义时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍然能使用那些绑定。

如下图所示:

闭包会保留自由变量series的绑定,在调用avg(10)时继续使用这个绑定,即使make_averager()函数的局部作用域已经消失。

nonlocal

把上面示例的需求稍微优化下,只存储目前的总值和元素个数:

  1. def make_averager(): 
  2.     count = 0 
  3.     total = 0 
  4.      
  5.     def averager(new_value): 
  6.         count += 1 
  7.         total += new_value 
  8.         return total / count 
  9.          
  10.     return averager 

运行后会报错:局部变量count在赋值前进行了引用。因为count +=1等同于count = count + 1,存在赋值,count就变成局部变量了。total也是如此。

这里如果把count和total通过global关键字声明为全局变量,显然是不合适的,它们作用域最多只扩展到make_averager()函数内。为了解决这个问题,Python3引入了nonlocal关键字声明:

  1. def make_averager(): 
  2.     count = 0 
  3.     total = 0 
  4.      
  5.     def averager(new_value): 
  6.         nonlocal count, total 
  7.         count += 1 
  8.         total += new_value 
  9.         return total / count 
  10.          
  11.     return averager 

nonlocal的作用是把变量标记为自由变量,即使在函数中为变量赋值了,也仍然是自由变量。

注意,对于列表、字典等可变类型来说,添加元素不是赋值,不会隐式创建局部变量。对于数字、字符串、元组等不可变类型以及None来说,赋值会隐式创建局部变量。示例:

  1. def make_averager(): 
  2.     # 可变类型 
  3.     count = {} 
  4.  
  5.     def averager(new_value): 
  6.         print(count)  # 成功 
  7.         count[new_value] = new_value 
  8.         return count 
  9.  
  10.     return averager 

可变对象添加元素不是赋值,不会隐式创建局部变量。

  1. def make_averager(): 
  2.     # 不可变类型 
  3.     count = 1 
  4.  
  5.     def averager(new_value): 
  6.         print(count)  # 报错 
  7.         count = new_value 
  8.         return count 
  9.  
  10.     return averager 

count是不可变类型,赋值会隐式创建局部变量,报错:局部变量count在赋值前进行了引用。

  1. def make_averager(): 
  2.     # None 
  3.     count = None 
  4.  
  5.     def averager(new_value): 
  6.         print(count)  # 报错 
  7.         count = new_value 
  8.         return count 
  9.  
  10.     return averager 

count是None,赋值会隐式创建局部变量,报错:局部变量count在赋值前进行了引用。

小结

 

本文先介绍了全局变量、自由变量、局部变量的概念,这是理解闭包的前提。闭包就是用来解决函数嵌套时,自由变量如何处理的问题,它会保留自由变量的绑定,即使局部作用域已经消失。对于不可变类型和None来说,赋值会隐式创建局部变量,把自由变量转换为局部变量,这可能会导致程序报错:局部变量在赋值前进行了引用。除了使用global声明为全局变量外,还可以使用nonlocal声明把局部变量强制变为自由变量,实现闭包。

 

责任编辑:武晓燕 来源: dongfanger
相关推荐

2020-09-11 10:55:10

useState组件前端

2021-02-28 09:47:54

软件架构软件开发软件设计

2021-02-19 23:08:27

软件测试软件开发

2018-11-23 11:17:24

负载均衡分布式系统架构

2020-10-14 08:04:28

JavaScrip

2023-04-20 10:15:57

React组件Render

2022-08-21 17:35:31

原子多线程

2018-01-17 09:15:52

负载均衡算法

2022-08-17 06:25:19

伪共享多线程

2023-04-12 14:04:48

光纤网络

2022-04-02 09:38:00

CSS3flex布局方式

2018-01-05 14:23:36

计算机负载均衡存储

2023-02-10 08:44:05

KafkaLinkedIn模式

2023-07-10 10:36:17

人工智能AI

2021-08-09 14:40:02

物联网IOT智能家居

2018-06-15 23:00:56

2022-07-15 14:58:26

数据分析人工智能IT

2022-12-30 11:24:21

2021-10-05 21:03:54

BeautifulSo 爬虫

2023-12-11 16:36:09

点赞
收藏

51CTO技术栈公众号