python闭包

主要解决:

  1. 什么是闭包?
  2. 闭包有什么作用?
  3. 为什么要使用闭包?
  4. 判断闭包
  5. 延迟绑定问题

在认识闭包之前,需要介绍一些背景知识,比如作用域,嵌套函数等概念。

1. 背景知识

1.1. 作用域

作用域是程序运行时变量可被访问的范围,定义在函数内的变量是局部变量,局部变量的作用范围只能是函数内部范围内,它不能在函数外引用。定义在模块最外层的变量是全局变量,它是全局范围内可见的,当然在函数里面也可以读取到全局变量的。

num = 10 # 全局变量
def foo():
    name = 'tom'
    print(num)  # 10
print(name)  # NameError: name 'name' is not defined

1.2. 嵌套函数

函数不仅可以定义在模块的最外层,还可以定义在另外一个函数的内部,像这种定义在函数里面的函数称之为嵌套函数(nested function)。

def print_msg():
    # print_msg 是外围函数
    msg = "zen of python"

    def printer():
        # printer是嵌套函数
        print(msg)
    printer()
# 输出 zen of python
print_msg()

对于嵌套函数,它可以访问到其外层作用域中声明的变量,比如代码示例中的变量 msg 可以被嵌套函数 printer 正常访问。

2. 什么是闭包

这段代码和前面例子的效果完全一样,同样输出 "zen of python"。不同的地方在于内部函数 printer 直接作为返回值返回了。

def print_msg():
    # print_msg 是外围函数
    msg = "zen of python"
    def printer():
        # printer 是嵌套函数
        print(msg)
    return printer

another = print_msg()
# 输出 zen of python
another()

一般情况下,函数中的局部变量仅在函数的执行期间可用,一旦print_msg()执行过后,我们认为msg变量将不再可用(因为msg是函数print_msg函数的局部变量)。然而,在这里,当我们执行完成print_msg之后,再调用another的时候,msg变量的值依然正常输出了,这就是闭包的作用,它使得局部变量在函数外被访问称为可能。

2.1. 维基百科中的解释

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

这里的another就是一个闭包,闭包本质上是一个函数,它包含两个部分,在这里其实就是printer函数和变量msg。闭包使得这些变量的值始终保存在内存中

2.2. 为什么使用闭包

闭包避免了使用全局变量,闭包允许将函数与其所操作的某些数据关联起来。这一点与面向对象编程是非常类似的,在面对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

一般来说,当对象中只有一个方法时,这时使用闭包是更好的选择。来看一个例子:

def adder(x):
    def wrapper(y):
        return x + y
    return wrapper

adder5 = adder(5)
# 输出 15
adder5(10)

如果使用面向对象编程,相对应的为:

class adder:
    def __init__(self, x):
        self.x = x

    def wrapper(self, y):
        return self.x+y

adder5 = adder(5)
adder5.wrapper(10)

装饰器也是基于闭包的一种应用场景

3. 判断函数是否为闭包?

所有函数都有一个closure属性,如果函数是一个闭包的话,那么它的返回值是一个由cell对象组成的元组对象。cell对象中的cell_contents属性就是闭包中的自由变量的值。

def adder(x):
    z = 10
    def wrapper(y):
        return x + y + z
    return wrapper

adder5 = adder(5)

print(adder5.__closure__)
# (<cell at 0x000001B6F181B9A8: int object at 0x00007FFD67B69210>, <cell at 0x000001B6F182BA08: int object at 0x00007FFD67B692B0>)

可以发现有两个cell对象,可以理解为两个自由变量。如下面的代码所示,可以发现对应的就是x和z这两个自由变量的值。

adder5.__closure__[0].cell_contents   # 5
adder5.__closure__[1].cell_contents   # 10

3.1. 延迟绑定

首先,看下面一段代码:

def multipliers():
    return [lambda x : i*x for i in range(4)]

print ([m(2) for m in multipliers()] )

上面的代码相当于

def multipliers():
    funcs = []
    for i in range(4):
        def ll(x):
            return i * x
        funcs.append(ll)
    return funcs

a = multipliers()
def multipliers():
    funcs = []
    for i in range(4):
        def ll(x):
            return i * x
        funcs.append(ll)
    return funcs

a = multipliers()
for m in a:
    print(m.__closure__[0].cell_contents)   # 3 因为i的值此时为3.
    print(m(2))

运行代码,multipliers()函数返回的是一个列表对象,列表中包含4个元素,每一个是lambda匿名函数。对于ll函数而言,i这个变量在执行multipliers的时候仍然存在于内存中,并且经过4次循环之后,i的值变成了3.此时运行嵌套的ll函数的时候,才会查找i的值。

解决方案:因为发生这种原因主要是因为i的值是ll函数外部的变量,从而导致在调用的时候才在内存中找这个值。因此,可以将i作为一个参数传递给ll函数,此时函数内部的i就不会受到外部i值的影响。

def multipliers():
    funcs = []
    for i in range(4):
        def ll(x,i=i):
            return i * x
        funcs.append(ll)
    return funcs

a = multipliers()
for m in a:
    print(m(2))

此时,通过在for循环的时候,就传递i的值,此时i不依赖与外部的i,此时函数ll不是一个闭包(因为ll内部没有调用外部的参数)。

4. 参考

  1. 主要参考here
  2. 延迟绑定here
全部评论

相关推荐

11-18 15:57
门头沟学院 Java
最终归宿是测开:这个重邮的大佬在重邮很有名的,他就喜欢打92的脸,越有人质疑他,他越觉得爽😂
点赞 评论 收藏
分享
10-13 17:47
门头沟学院 Java
wulala.god:图一那个善我面过,老板网上找的题库面的
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务