作者整理笔记,记录备查


文章目录

  • 1、变量作用域
  • 2、闭包的概念
  • 3、闭包的作用
  • 3.1 读取函数内部的变量
  • 3.2 让函数内部的局部变量始终保持在内存中
  • 4、注意事项
  • 4.1 内存消耗
  • 4.2 使用场景
  • 4.3 闭包无法改变外部函数局部变量指向的内存地址
  • 4.4 返回函数引用循环变量
  • 5、闭包的__closure__属性



1、变量作用域

还是以局部函数中的例子做参考,假设要在函数外部得到函数内的局部变量a,由于python作用于的搜索顺序,无法通过调用outfuc直接实现,但是通过在内部定义infunc函数,并通过返回该函数对象可以实现对局部变量a的访问。
通过局部函数,可以在函数外部得到函数内的局部变量 这时outfunc 内部的所有局部变量,对infunc都是可见的。但是infunc内部的局部变量,对outfunc 是不可见的

#全局函数
def outfunc ():
    a = 'outvar'
    #局部函数
    def infunc():
        print(a) 
    #调用局部函数
    return infunc
#调用全局函数
get_a = outfunc() 
get_a() ## 访问变量a的值

以上infunc函数就形成了闭包,外部函数返回的不是一个具体的值,而是一个函数。一般情况下,返回的函数会赋值给一个变量,这个变量可以在后面被继续执行调用。

2、闭包的概念

维基百科:在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。 维基百科的定义要点如下:

  • 定义:闭包就是能够读取外部函数内的变量的函数。
  • 作用1:闭包将外层函数内的局部变量和外层函数的外部连接起来。
  • 作用2:将外层函数的变量持久地保存在内存中。

计算一个数的 n 次幂,使用闭包实现:

#闭包函数,其中 exponent 称为自由变量
def nth_power(exponent):
    def exponent_of(base):
        return base ** exponent
    return exponent_of # 返回值是 exponent_of 函数
square = nth_power(2) # 计算一个数的平方
cube = nth_power(3) # 计算一个数的立方
print(square(2))  # 计算 2 的平方
print(cube(2)) # 计算 2 的立方

在上面程序中,外部函数 nth_power() 的返回值是函数 exponent_of(),而不是一个具体的数值。

需要注意的是,在执行完 square = nth_power(2) 和 cube = nth_power(3) 后,外部函数 nth_power() 的参数 exponent 会和内部函数 exponent_of 一起赋值给 squre 和 cube,这样在之后调用 square(2) 或者 cube(2) 时,程序就能顺利地输出结果,而不会报错说参数 exponent 没有定义。

3、闭包的作用

3.1 读取函数内部的变量

借用以下案例,实现给content加tag的功能:

def tag(tag_name):
    def add_tag(content):
        return "<{0}>{1}</{0}>".format(tag_name, content)
    return add_tag
  
content = 'Hello'

add_tag = tag('a')
print add_tag(content)
# <a>Hello</a>

add_tag = tag('b')
print add_tag(content)
# <b>Hello</b>

具体使用什么tag_name要根据实际需求来定,对外部调用的接口已经确定,就是add_tag(content),如果使用面向接口的方式,可以这样写:

def add_tag(tag_name,content):
    return "<{0}>{1}</{0}>".format(tag_name, content)
add_tag('a','Hello') # <a>Hello</a>

上面程序确实可以实现相同的功能,不过使用闭包,在添加大量同类型的tag_name时,更加易读和方便,只需要传入content参数

3.2 让函数内部的局部变量始终保持在内存中

一般来说,函数内部的局部变量在这个函数运行完以后,就会被Python的垃圾回收机制从内存中清除掉。如果希望这个局部变量能够长久的保存在内存中,可以用闭包来实现这个功能。
以棋盘游戏的为例,假设棋盘大小为50*50,左上角为坐标系原点(0,0),建立一个函数,接收2个参数,分别为方向(direction),步长(step),该函数控制棋子的运动。 注:每次运动的起点都是上次运动结束的终点

def create(pos=[0,0]):
    
    def go(direction, step):
        new_x = pos[0]+direction[0]*step
        new_y = pos[1]+direction[1]*step
        
        pos[0] = new_x
        pos[1] = new_y
        
        return pos
    
    
    return go

player = create()
print(player([1,0],10))
print(player([0,1],20))
print(player([-1,0],10))
##
[10, 0]
[10, 20]
[0, 20]

这段代码中,player实际上就是闭包go函数的一个实例对象。

它一共运行了三次,第一次是沿X轴前进了10来到[10,0],第二次是沿Y轴前进了20来到 [10, 20],第三次是反方向沿X轴退了10来到[0, 20]。

这证明了,函数create中的局部变量pos一直保存在内存中,并没有在create调用后被自动清除。

为什么会这样呢?原因就在于create是go的父函数,而go被赋给了一个全局变量,这导致go始终在内存中,而go的存在依赖于create,因此create也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这个时候,闭包使得函数的实例对象的内部变量,变得很像一个类的实例对象的属性,可以一直保存在内存中,并不断的对其进行运算。

4、注意事项

使用闭包,需要注意几点:

4.1 内存消耗

由于闭包会使得函数中的变量都被保存在内存中,会增加内存消耗,所以不能滥用闭包,否则会造成程序的性能问题,可能导致内存泄露。
解决方法是,在退出函数之前,将不使用的局部变量全部删除。

4.2 使用场景

当要实现的功能比较简单的时候,可以用闭包。例如:

  • 当我们的代码中函数比较少的时候,可以使用闭包。但是如果我们要实现很多功能,还是要使用类(OOP)
  • 如果我们的对象中只有一个方法时,使用闭包是会比用类来实现更优雅。

这有点类似于,如果我们要实现比较简单的函数功能,通常使用 lambda 匿名函数比定义一个完整的function更加优雅,而且几乎不会损失可读性。类似的还有用列表解析式代替 for 循环。

4.3 闭包无法改变外部函数局部变量指向的内存地址

def outfunc ():
    x = 'outvar'
    #局部函数
    def infunc():
        x = 'invar' 
        print('outer x before call inner:', x, 'at', id(x))
    #调用局部函数
    print('outer x before call inner:', x, 'at', id(x))
    infunc()
    print('outer x before call inner:', x, 'at', id(x))

outfunc()
## 
outer x before call inner: outvar at 2176116975280 # outvar
inner x before call inner: invar at 2176116989104 # invar
outer x before call inner: outvar at 2176116975280  # outvar

可以看到,局部函数并没有改变外部函数中变量x的地址,这与函数的作用域有关。如果要让内层函数不仅可以访问,还可以修改外层函数的变量,那么需要用到nonlocal声明,使得内层函数不要在自己的命名空间创建新的x,而是操作外层函数命名空间的x

def outfunc ():
    x = 'outvar'
    #局部函数
    def infunc():
        nonlocal x
        x = 'invar' 
        print('inner x before call inner:', x, 'at', id(x))
    #调用局部函数
    print('outer x before call inner:', x, 'at', id(x))
    infunc()
    print('outer x before call inner:', x, 'at', id(x))

outfunc()
##
outer x before call inner: outvar at 2176116975280 # outvar
inner x before call inner: invar at 2176116989104 # outvar
outer x before call inner: invar at 2176116989104 # outvar地址改变

infunc改变了 outfunc 中变量x的内存地址

4.4 返回函数引用循环变量

返回闭包时,返回函数不要引用任何循环变量,或者后续会发生变化的变量 以下循环想得到1,4,9,但结果显然没有实现

def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()
print(f1(),f2(),f3()) ## 9 9 9

返回的函数并没有立刻执行,而是直到调用了f()才执行。因为在向列表中添加 func 的时候,i 的值没有固定到f的实例对象中,而仅是将计算公式固定到了实例对象中。等到了调用f1()、f2()、f3()的时候才去取 i的值,这时候循环已经结束,i 的值是3,所以结果都是9。

如果一定要使用循环,需要再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变

def count():
    def f(j):
        def g():
            return j*j
        return g
    fs = []
    for i in range(1, 4):
        fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
    return fs
f1, f2, f3 = count() ## 1 4 9

5、闭包的__closure__属性

闭包比普通的函数多了一个 closure 属性,该属性记录着自由变量的地址。当闭包被调用时,系统就会根据该地址找到对应的自由变量,完成整体的函数调用。

def nth_power(exponent):
    def exponent_of(base):
        return base ** exponent
    return exponent_of
square = nth_power(2)
#查看 __closure__ 的值
print(square.__closure__) # (<cell at 0x000001FAAB272C70: int object at 0x00007FFAB3332740>,)

参考:
[1] Wayne:用最简单的语言解释Python的闭包是什么? [2] 千山飞雪:深入浅出python闭包 [3] Python中作用域的搜索顺序