Python的闭包是什么意思?

闭包算是编程语言里一个比较常见的概念,但说实话,这个名词有点晦涩。在查看了半天网上的资料后,还是有点不明就里。

我疑惑的点主要是:这个东西是用来解决什么问题的?或者说,他的作用是什么?

先说作用

查阅了很多资料后,总结有下面几个作用:

  • 在某局部变量的作用外,依然可以访问到此局部变量
  • 可以避免使用全局变量,从而减少可能带来的影响(很多文章把此项也称之为:保存当前的运行环境)
  • 可以把多参数的函数变成单参数的函数(大多数文章把此项称之为:可以根据外部作用域的局部变量得到不同的结果)

下面举例子来分别说一下这几个功能。

1. 访问

In [33]: def test_1():
    ...:     name = 'Tom'
    ...:

In [35]: name
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-35-9bc0cb2ed6de> in <module>
----> 1 name

NameError: name 'name' is not defined

In [48]: def test_2():
    ...:     name = 'Tom'
    ...:     def test_3():
    ...:         print(name)
    ...:     return test_3

In [49]: test = test_2()

In [50]: test
Out[50]: <function __main__.test_2.<locals>.test_3()>

In [51]: print(test)
<function test_2.<locals>.test_3 at 0x000001F9195D5EE8>

In [52]: test()
Tom

可以看到第一个例子中,是没办法获取到name的值的;

第二个例子中,就可以得到name的值,当然,也不是直接访问的。

实现的原理就是利用了局部变量的作用域,既然外面访问不到局部变量name,那么就从函数里去访问,间接的得到这个值。

在第二个例子中,调用test_2()的时候,就产生了一个闭包:test_3()。这个地方其实是闭包的一个典型的表现形式,然而看起来依然晦涩。所以私以为,从作用上来讲,逻辑更顺畅。

所以也有人这么称呼闭包:

闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来。

2. 可以避免使用全局参数

假如函数的功能是对一个list进行append/pop操作,执行N步后,返回其结果,我们可以这样实现:

In [54]: nums = [1 ,2, 3, 4]

In [55]: def append_num(x):
    ...:     nums.append(x)
    ...:

In [56]: append_num(5)

In [57]: nums
Out[57]: [1, 2, 3, 4, 5]

这个需求中,最重要的一步就是要保存执行的中间结果,所以需要用到全局变量。

但是很显然,这样实现并不优雅,当项目庞大或者多人合作的时候,全局变量可能带来灾难性的后果。同时,在函数中对全局变量进行操作,会导致过程不可控。

此时,闭包是一个很好的选择。也就是说闭包可以保存类似需求中的中间变量,如下:

In [59]: def append_nums(nums):
    ...:     def append_x(x):
    ...:         nums.append(x)
    ...:         return nums
    ...:     return append_x
    ...:

In [60]: tmp = append_nums([1])

In [61]: tmp.append_x(2)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-61-f86fca8c96cf> in <module>
----> 1 tmp.append_x(2)

AttributeError: 'function' object has no attribute 'append_x'

In [62]: tmp(2)
Out[62]: [1, 2]

In [63]: tmp(3)
Out[63]: [1, 2, 3]

In [64]: tmp
Out[64]: <function __main__.append_nums.<locals>.append_x(x)>

可以看到,在声明tmp之后,可以像调用函数一样直接把参数直接传递给tmp。同时,每次执行,都是在上一次执行的结果上append,达到了我们的目的,同时看起来比较优雅。

再看tmp,会发现tmp本身就是一个函数,所以调用的话和函数使用是一样的。

当然,这里也有一个明显的好处:可以声明不同的num,两者互不干扰。

In [65]: tmp_1 = append_nums([3])

In [66]: tmp_1(0)
Out[66]: [3, 0]

是不是感觉又有点像类和对象的关系~

一般来说,当只有一个内部方法的时候,闭包比类更合适、更简单。

同时,如果和1中的例子相比的话,是不是感觉复用性提高了很多?

有一个更常见的例子,就是棋牌游戏的

以一个类似棋盘游戏的例子来说明。假设棋盘大小为50*50,左上角为坐标系原点(0,0),我需要一个函数,接收2个参数,分别为方向(direction),步长(step),该函数控制棋子的运动。棋子运动的新的坐标除了依赖于方向和步长以外,当然还要根据原来所处的坐标点,用闭包就可以保持住这个棋子原来所处的坐标。

origin = [0, 0] # 坐标系统原点 
legal_x = [0, 50] # x轴方向的合法坐标 
legal_y = [0, 50] # y轴方向的合法坐标 

def create(pos): 
 def player(direction,step): 
  # 这里应该首先判断参数direction,step的合法性,比如direction不能斜着走,step不能为负等 
  # 然后还要对新生成的x,y坐标的合法性进行判断处理,这里主要是想介绍闭包,就不详细写了。 
  new_x = pos[0] + direction[0]*step 
  new_y = pos[1] + direction[1]*step 
  pos[0] = new_x 
  pos[1] = new_y 
  #注意!此处不能写成 pos = [new_x, new_y],原因在上文有说过 
  return pos 
 return player 
  
player = create(origin) # 创建棋子player,起点为原点 
print player([1,0],10) # 向x轴正方向移动10步 
print player([0,1],20) # 向y轴正方向移动20步 
print player([-1,0],10) # 向x轴负方向移动10步

3. 减少参数

这一点,其实在上面的例子中就能看出来:

在使用了闭包这个特性之后,声明变量后,再使用的话,只需要传递里面函数的参数就好了。

再说定义

维基百科:

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。

引用了自由变量的函数是什么?

def print_msg():
    msg = "python"
    def printer():
        print(msg)
    return printer

test = print_msg()
test()

这个例子中的printer其实就是一个引用了自由变量/外部临时变量的函数。

通常情况来讲,msg属于print_msg的局部变量,只能再print_msg()执行的时候调用。

但上面的例子中,test声明的时候,就是print_msg()执行的时候;但当test()执行的时候,我们依然拿到了msg的值。这就是闭包的一个特点,现在看是不是1中的例子更清楚些了?

那什么时候用闭包呢?除去上面举例子的几个场景,还有一个更为常见的:装饰器。更详细的说,其实装饰器是闭包的一个应用场景。

def make_wrap(func):
    def wrapper(*args):
        print("before function")
        func(*args)
        print("after function")
    return wrapper

@make_wrap
def print_msg(msg):
    print(msg)

>>> print_msg("Hello")
before function
Hello
after function

最后说一些坑

1. 闭包无法修改外部函数的局部变量

In [69]: def outer():
    ...:     x = 1
    ...:     def inner():
    ...:         x = 2
    ...:         print(x)
    ...:     print(x)
    ...:     inner()
    ...:     print(x)
    ...:

In [70]: outer()
1
2
1

2. 闭包无法直接访问外部函数的局部变量

In [71]: def outer():
    ...:     x = 1
    ...:     def inner():
    ...:         x = x + 2
    ...:         return x
    ...:     return inner
    ...:

In [72]: f = outer()

In [73]: f()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-73-c43e34e6d405> in <module>
----> 1 f()

<ipython-input-71-4b6c486c99d4> in inner()
      2     x = 1
      3     def inner():
----> 4         x = x + 2
      5         return x
      6     return inner

UnboundLocalError: local variable 'x' referenced before assignment

也就是说,inner函数可以访问x,但无法改变他的值。原因也很好说,Python本来就这样设计的。。

Python 就是这样设计的,它认为在函数体中,如果对变量有赋值操作,则证明这个变量是一个局部变量,并且它只会从局部变量中去读取数据。这样设计可以避免我们在不知道的情况下,获取到全局变量的值,从而导致一些错误数据的出现。
《流畅的Python》

那么非要这么做,怎么办?Python3中有2种方法

In [74]: def outer():
    ...:     x = 1
    ...:     def inner(x=x):
    ...:         x = x + 2
    ...:         return x
    ...:     return inner
    ...:

In [75]:

In [75]: f = outer()

In [76]: f()
Out[76]: 3

In [80]: def outer():
    ...:     x = 1
    ...:     def inner():
    ...:         nonlocal x
    ...:         x = x + 1
    ...:         return x
    ...:     return inner
    ...:

In [81]: outer()()
Out[81]: 2

3. 循环体中,不存在作用域的概念

In [82]: nums = []

In [83]: for i in range(3):
    ...:     def f():
    ...:         return i
    ...:     nums.append(f)
    ...:

In [84]: nums
Out[84]: [<function __main__.f()>, <function __main__.f()>, <function __main__.f()>]

In [85]: nums[1]
Out[85]: <function __main__.f()>

In [86]: nums[1]()
Out[86]: 2

In [87]: nums[0]()
Out[87]: 2

In [88]: nums[2]()
Out[88]: 2

理想的结果是0,1,2,实际结果2,2,2。

这个其实是Python的一个特性,很常见,懒加载。声明的时候并不执行,当调用的时候才执行。

当然也很好解决:

# 直接赋值
In [90]: nums = []

In [91]: for i in range(3):
    ...:     def f():
    ...:         return i
    ...:     nums.append(f())
    ...:
    ...:

In [92]: nums[0]
Out[92]: 0

In [93]: nums[1]
Out[93]: 1

In [94]: nums[2]
Out[94]: 2

# 和上述例子一样的话,可以采用下面的方式
In [97]: nums = []

In [98]: for i in range(3):
    ...:     def f(i=i):
    ...:         return i
    ...:     nums.append(f)
    ...:

In [99]: nums[0]
Out[99]: <function __main__.f(i=0)>

In [100]: nums[0]()
Out[100]: 0

In [101]: nums[1]()
Out[101]: 1

In [102]: nums[2]()
Out[102]: 2

参考

https://wiki.jikexueyuan.com/project/explore-python/Functional/closure.html

https://segmentfault.com/a/1190000004461404

https://www.liaoxuefeng.com/wiki/1022910821149312/1023021250770016

https://foofish.net/python-closure.html

https://blog.csdn.net/Yeoman92/article/details/67636060

https://zhuanlan.zhihu.com/p/22229197

https://www.imooc.com/article/38716

https://www.jianshu.com/p/3502bdf5485e

https://segmentfault.com/a/1190000008955952

posted @ 2020-06-22 10:55  wswang  阅读(1532)  评论(2编辑  收藏  举报