python闭包

本文深入讲解了Python中的闭包概念,通过示例阐述闭包的形成机制,以及它如何保持对外部变量的引用。通过`dis`模块解析,揭示了闭包的底层实现。此外,还讨论了闭包在实际编程中的陷阱和解决方法,包括循环中的匿名函数和参数传递问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       当一个函数中嵌套定义了一个内层函数,且该内层函数引用了外层函数的变量,那么该内层函数就是一个闭包,如下所示。

def outer():
    x = 1
    y = 2
    def inner():
        print(x)
    return inner

f1 = outer()
f1() # out --> 1

        怎么理解闭包(Closure)这个词?闭包指的是这样一种对象:将某个对象和对象所在的部分环境信息打包在一起,形成一个新的对象,这个包含了原对象所在的执行环境信息的对象就是闭包。在python中,闭包中的原对象指的是原始内层函数,原对象所在的环境信息指的就是变量x, 新的对象(闭包)就是代码中的内层函数inner,原始inner函数在底层实现,对用户隐藏;所以outer函数最后返回的inner函数就是一个闭包。此时,我们也称outer函数为工厂函数,或者更准确的称为闭包工厂函数(生产闭包的工厂函数)。

        由于闭包会保存所在执行环境的信息,上例中,闭包inner保存了outer的本地变量x的引用,所以尽管outer函数执行结束返回inner闭包给f1,f1依然可以正常运行,因为其已经保存了x的引用。

        对于本地命名空间,当函数退出后,在内存中便不会存在;虽然x属于outer函数的本地命名空间,但是由于嵌套函数引用了x,实际上在底层实现上,x和y并不属于同一个结构体(即使属于同一个命名空间,但由于命名空间是一个抽象概念,不对应具体实体,所以并不矛盾),outer结束后,y所属的结构体会被消除,但是x所属的结构体依然会保留,所以其中的对象也会在内存中继续保留。

        我们可以通过python的dis模块解析outer函数,通过解释器执行的字节码来证实,变量x和y属于不同结构体。如下所示:

dis.dis(outer)
  2           0 LOAD_CONST               1 (1)
              2 STORE_DEREF              0 (x)

  3           4 LOAD_CONST               2 (2)
              6 STORE_FAST               0 (y)

  4           8 LOAD_CLOSURE             0 (x)
             10 BUILD_TUPLE              1
             12 LOAD_CONST               3 (<code object inner at 0x0000021E53973870, file "<ipython-input-104-67473ff04f11>", line 4>)
             14 LOAD_CONST               4 ('outer.<locals>.inner')
             16 MAKE_FUNCTION            8 (closure)
             18 STORE_FAST               1 (inner)

  6          20 LOAD_FAST                1 (inner)
             22 RETURN_VALUE

Disassembly of <code object inner at 0x0000021E53973870, file "<ipython-input-104-67473ff04f11>", line 4>:
  5           0 LOAD_GLOBAL              0 (print)
              2 LOAD_DEREF               0 (x)
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

        可以看到,对应代码x=1的字节码是block 2,其中通过STORE_DEREF指令将变量x存储起来;对比变量y,对应的是block 3,对变量y的存储指令是STORE_FAST,两种不同的存储指令对应不同结构体;存储一般的本地变量到本地命名空间就是通过STORE_FAST,而存储这种被闭包引用的本地变量,也叫闭包的自由变量,通过STORE_DEREF指令。

        最后再看一个例子,该例子既可以加深对闭包的理解,也可以当作一个小陷阱来学习。下面的例子中,for循环中将匿名函数放进了一个列表中并返回,可能有的人会误认为输出结果是[0,1,2,3,4,5,6,7,8,9],但是返回的列表中的函数输出的结果都是一样的,全部都是9。这是因为函数只有在调用的时候才会进行变量值的获取,在outer返回的闭包列表中,每个闭包保存的i的值是一样的,都是最后一个循环下i的值,因此每个闭包的调用执行结果都是一样的。

def outer(n):
    funcs = []
    for i in range(n):
        func_tmp = lambda : i
        funcs.append(func_tmp)
    return funcs

funcs = outer(10)
print([f() for f in funcs])
# out --> [9, 9, 9, 9, 9, 9, 9, 9, 9, 9]

        那么如果我们想得到[0,1,2,3,4,5,6,7,8,9]这样的结果应该怎么做呢?一种做法是,将每次循环下i的值作为参数传入,这样由于lambda语句和def一样是可执行的,每次执行的时候都会保存一份当前参数值引用的副本,从而每次循环下匿名函数参数对应的是不同值的引用,从而执行的时候便会得到不同的结果,具体如下所示。

def outer(n):
    funcs = []
    for i in range(n):
        func_tmp = lambda x=i: x
        funcs.append(func_tmp)
    return funcs

funcs = outer(10)
print([f() for f in funcs])
# out --> [0,1,2,3,4,5,6,7,8,9]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值