打爆Python 闭包的狗头

很多初学 Python 的朋友都会有一个疑问:

函数执行完以后,里面的局部变量不是应该消失了吗?
那为什么有些函数还能“记住”之前的数据?

这个问题背后,涉及 Python 里一个非常重要的概念:闭包。闭包听起来有点抽象,但它其实并不神秘。简单来说,闭包就是:

一个函数记住了它定义时所在环境中的变量。


一、先看一段容易猜错的代码

我们先来看一段代码:

def outer():
    x = 10

    def inner():
        nonlocal x
        x += 1
        return x

    return inner


print(outer()())
print(outer()())

你觉得输出结果是什么?

很多人看到这种“函数里面嵌套函数”的写法,会马上想到闭包。

于是可能会认为:

inner() 会记住外层函数里的变量 x
第一次调用时 x 从 10 变成 11,
第二次调用时 x 应该从 11 变成 12。

所以很多人的第一反应是输出:

11
12

但实际结果是:

11
11

这就有点奇怪了。不是说闭包可以记住外层变量吗?为什么这里看起来没有记住呢?我认为这其实正是理解闭包的关键之处。继续看下去吧,不难。


二、为什么 outer()() 两次都是 11?

关键在于:

print(outer()())
print(outer()())

这两行代码中,outer() 被调用了两次,每调用一次 outer(),都会创建一套全新的局部环境。也就是说,每次执行 outer() 时,都会重新创建一个新的 x

x = 10

同时,也会重新创建一个新的 inner() 函数。所以第一行:

print(outer()())

可以理解成:

temp_func = outer()
print(temp_func())

这里发生了什么?

第一步,调用 outer()

temp_func = outer()

这时创建了一个新的 x,初始值是 10,然后返回一个 inner 函数。

第二步,调用这个返回的函数:

print(temp_func())

于是 x += 1,结果变成 11

所以第一行输出:

11

第二行:

print(outer()())

又重新调用了一次 outer()

这一次不是继续使用上一次的 x,而是又创建了一个全新的 x = 10

所以第二次也是:

10 + 1 = 11

因此输出结果是:

11
11

可以用一个比喻来理解:

每次执行 outer()(),就像重新买了一台新的计数器。
每台计数器的初始值都是 10。
你每次只按了一下,所以每次都是 11。

所以这里并不是闭包失效了,而是你每次都创建了一个新的闭包。它们之间互不影响。


三、正确用法:先把返回的函数保存下来

如果我们希望闭包真正“记住上一次的值”,就不能每次都重新调用 outer()

正确的写法是:

def outer():
    # 外层函数中的局部变量
    # 正常情况下,outer() 执行结束后,x 应该被销毁
    # 但因为 inner() 使用了它,所以它会被闭包保存下来
    x = 10

    def inner():
        # nonlocal 表示:
        # 这里使用的 x 不是 inner() 自己的局部变量,
        # 而是外层函数 outer() 里的 x。
        #
        # 易错点:
        # 如果要在 inner() 里面修改外层变量 x,
        # 就必须写 nonlocal x。
        # 否则 Python 会把 x 当成 inner() 内部的新局部变量,
        # 从而导致报错。
        nonlocal x

        # 每调用一次 inner(),都会让同一个 x 加 1
        x += 1

        # 返回修改后的 x
        return x

    # 返回 inner 函数本身,而不是执行 inner()
    #
    # 易错点:
    # 这里写的是 return inner,不是 return inner()
    #
    # return inner:
    #   把函数本身返回出去,后面可以继续调用。
    #
    # return inner():
    #   立刻执行 inner 函数,并返回执行结果。
    return inner


# outer() 执行后,返回 inner 函数
# counter 接住了这个 inner 函数
#
# 关键点:
# counter 保存的是一个“带着 x 的函数”
# 这就是闭包
counter = outer()

# 第一次调用 counter()
# 实际上是在调用 inner()
# x 从 10 变成 11
print(counter())

# 第二次调用的还是同一个 inner()
# 所以 x 会在上一次的基础上继续加 1
# x 从 11 变成 12
print(counter())

# 第三次调用的仍然是同一个 inner()
# x 从 12 变成 13
print(counter())

输出结果:

11
12
13

这段代码的关键不是 inner() 里面的逻辑有多复杂,而是这一句:

counter = outer()

这一句做了两件事:

第一,执行 outer()

第二,把 outer() 返回的 inner 函数保存到变量 counter 中。

从这以后,我们每次执行:

counter()

其实都是在调用同一个 inner() 函数。

既然是同一个 inner() 函数,那么它引用的也是同一个 x

所以 x 会持续累加:

10 -> 11 -> 12 -> 13

而不是每次都从 10 重新开始。


四、到底什么是闭包?

闭包其实并不神秘。

在 Python 中,如果满足下面几个条件,就可以形成闭包:

  1. 有一个外层函数;
  2. 外层函数里面定义了一个内层函数;
  3. 内层函数使用了外层函数中的变量;
  4. 外层函数把内层函数返回出去;
  5. 返回出去的内层函数仍然可以访问外层函数的变量。

例如:

def outer():
    x = 10

    def inner():
        return x

    return inner

这里的 inner 就是一个闭包。

因为:

  • inner 定义在 outer 里面;
  • inner 使用了外层函数中的变量 x
  • outerinner 返回了出去;
  • 即使 outer() 执行结束,inner 仍然能访问 x

我们可以把闭包理解成:

闭包 = 函数 + 它记住的外部变量

或者更形象一点:

闭包就像一个随身带着“记事本”的函数。
函数负责执行逻辑,记事本负责保存数据。

在计数器例子中:

counter = outer()

得到的 counter 就不是一个普通函数。

它是一个带着变量 x 的函数。

这个 x 就是它的“记忆”。


五、为什么外层函数结束后,变量还活着?

很多人最疑惑的地方是:

x 明明是 outer() 里的局部变量,为什么 outer() 执行完之后,x 没有被销毁?

按照普通理解,函数执行结束后,里面的局部变量应该被清理。

这句话本身没有错。

但是闭包是一个特殊情况。

原因是:

外层函数中的变量被内层函数引用了。

在普通函数里,局部变量只在函数执行期间存在。

例如:

def test():
    a = 100
    return a

test() 执行结束后,如果没有其他地方引用 a,那么这个局部变量对应的对象就可能被回收。但是在闭包中情况不同。

例如:

def outer():
    x = 10

    def inner():
        return x

    return inner


func = outer()

这里 outer() 虽然执行完了,但是它返回的 innerfunc 保存了下来。

inner 又引用了外层变量 x

所以 Python 不能直接把 x 清理掉。

因为 inner 以后可能还要用它。


六、闭包背后的 cell 对象

Python 为了让内层函数在外层函数结束后仍然能访问外层变量,会把这些被引用的变量保存到一个特殊的对象里。

这个对象通常叫做 cell 对象

可以简单理解成:

func / counter
      ↓
inner 函数
      ↓
闭包中的 cell 对象
      ↓
外层变量 x

也就是说,x 并不是简单地作为普通局部变量存在。

它被 Python 放进了闭包相关的 cell 对象中。

只要内层函数还存在,这个 cell 对象就还存在。

只要 cell 对象还存在,里面保存的 x 就还存在。

所以,闭包能记住外层变量,并不是魔法。

它背后是 Python 帮我们保存了这些变量。


七、再从垃圾回收机制理解一下

在 Python 中,对象什么时候会被回收,主要取决于它还有没有被引用。

Python 的内存管理中有一个非常重要的机制,叫做 引用计数

简单来说:

如果一个对象还被其他变量、函数或对象引用着,它就不会被当成垃圾回收。

来看这个例子:

counter = outer()

执行完之后,引用关系大概是这样的:

counter
   ↓
inner 函数
   ↓
闭包中的 cell 对象
   ↓
x 对应的对象

所以:

  • 只要 counter 还存在,inner 函数就还存在;
  • 只要 inner 函数还存在,它引用的闭包变量就还存在;
  • 只要闭包变量还存在,x 对应的对象就不会被回收。

因此,虽然 outer() 已经执行结束了,但 x 并不会马上消失。

因为在 Python 看来:

x 还“有人用”,它不是垃圾。

只有当这条引用链断开时,相关对象才有机会被回收。

例如:

del counter

counter 被删除后,如果没有其他变量再引用这个闭包函数,那么 inner 函数以及它保存的闭包变量 x,才有机会被 Python 的垃圾回收机制清理掉。

所以要注意:

闭包的本质不是“局部变量永远不会销毁”。
而是“局部变量被内层函数引用后,会随着闭包一起继续存在”。

只要闭包还被外部引用,这些变量就不会被回收。


八、用 __closure__ 看看闭包里保存了什么

我们可以通过函数的 __closure__ 属性,查看一个函数是否携带了闭包变量。

例如:

def outer():
    x = 10

    def inner():
        return x

    return inner


func = outer()

print(func.__closure__)
print(func.__closure__[0].cell_contents)

输出类似:

(<cell at 0x000001F2A3B4C5D0: int object at 0x00007FF...>,)
10

这里的:

func.__closure__

保存的就是闭包引用的变量信息。

而:

func.__closure__[0].cell_contents

可以查看 cell 对象里具体保存的内容。

在这个例子中,看到的是:

10

这就说明:

outer() 虽然已经执行结束了,但变量 x 仍然被 inner 函数记住了。

如果一个函数没有形成闭包,那么它的 __closure__ 通常是 None

例如:

def hello():
    return "hello"


print(hello.__closure__)

输出:

None

说明这个函数没有携带外部变量。


九、nonlocal 是做什么用的?

在前面的计数器代码里,我们用了 nonlocal

def outer():
    x = 10

    def inner():
        nonlocal x
        x += 1
        return x

    return inner

为什么要写:

nonlocal x

原因是:

我们在 inner() 里面不仅要读取 x,还要修改 x

如果只是读取外层变量,不需要写 nonlocal

例如:

def outer():
    name = "Python"

    def inner():
        return name

    return inner

这里 inner() 只是读取 name,没有修改它,所以不需要 nonlocal

但是如果你要修改外层变量,就必须写 nonlocal

如果不写,会发生什么?

def outer():
    x = 10

    def inner():
        x += 1
        return x

    return inner


counter = outer()
print(counter())

运行后会报错:

UnboundLocalError: local variable 'x' referenced before assignment

为什么?

因为 Python 在分析函数时,只要看到函数内部有赋值操作,比如:

x += 1

它就会认为 x 是当前函数 inner() 的局部变量。

但问题是:

x += 1

本质上相当于:

x = x + 1

右边需要先读取 x,然后再加 1,最后赋值给 x

可是 Python 已经把 x 当成 inner() 的局部变量了,而这个局部变量在读取之前还没有被赋值。

所以就报错了。

加上:

nonlocal x

就是明确告诉 Python:

这个 x 不是 inner() 里的局部变量,
而是外层函数 outer() 里的变量。
我要修改的是外层那个 x

所以总结一下:

只读取外层变量,不需要 nonlocal
要修改外层变量,必须使用 nonlocal


十、return innerreturn inner() 的区别

闭包里还有一个非常容易出错的地方:

return inner

和:

return inner()

这两个写法差别非常大。

1. return inner

def outer():
    x = 10

    def inner():
        return x

    return inner

这里返回的是函数本身。

所以外部可以这样接收:

func = outer()
print(func())

也就是说,func 现在就是 inner 函数。

你可以以后再调用它。

这正是闭包常见的写法。


2. return inner()

def outer():
    x = 10

    def inner():
        return x

    return inner()

这里返回的不是函数,而是 inner() 的执行结果。

也就是说,outer() 执行时,会马上调用 inner(),然后把结果返回出去。

例如:

result = outer()
print(result)

输出:

10

这时 result 保存的是数字 10,不是函数。

所以你不能再写:

result()

否则会报错:

TypeError: 'int' object is not callable

所以一定要记住:

return inner:返回函数本身,形成闭包。
return inner():立即执行函数,返回执行结果。


十一、闭包有什么用?

闭包最常见的作用是:保存状态

也就是说,让一个函数在多次调用之间,保留一些数据。


1. 用闭包实现计数器

def make_counter():
    count = 0

    def counter():
        nonlocal count
        count += 1
        return count

    return counter


counter = make_counter()

print(counter())
print(counter())
print(counter())

输出:

1
2
3

这里的 count 就是被闭包保存起来的状态。

它不会暴露成全局变量,也不会被外部随意修改。

如果不用闭包,我们可能会写成全局变量:

count = 0

def counter():
    global count
    count += 1
    return count

这样虽然也能实现计数,但 count 是全局变量,任何地方都可以修改它,代码变复杂后就容易出问题。

闭包的好处是:

既能保存状态,又能避免使用全局变量。


2. 用闭包生成定制函数

比如我们想生成一个“乘法器”:

def make_multiplier(n):
    def multiplier(x):
        return x * n

    return multiplier


double = make_multiplier(2)
triple = make_multiplier(3)

print(double(10))
print(triple(10))

输出:

20
30

这里:

double = make_multiplier(2)

得到的是一个记住了 n = 2 的函数。

所以:

double(10)

相当于:

10 * 2

而:

triple = make_multiplier(3)

得到的是一个记住了 n = 3 的函数。

所以:

triple(10)

相当于:

10 * 3

这就是闭包的一个典型应用:

根据不同参数,生成不同功能的函数。


3. 用闭包封装私有变量

闭包还可以用来隐藏数据,避免变量被外部直接修改。

例如,我们写一个简单的钱包:

def create_wallet():
    money = 100

    def pay(amount):
        nonlocal money

        if amount <= money:
            money -= amount
            return f"支付成功,余额:{money}"
        else:
            return "余额不足"

    return pay


wallet = create_wallet()

print(wallet(30))
print(wallet(50))
print(wallet(30))

输出:

支付成功,余额:70
支付成功,余额:20
余额不足

在这个例子中,money 是外层函数里的变量。

外部不能直接访问它。

我们不能直接写:

money = 100000

去修改闭包内部的 money

外部只能通过:

wallet(30)

间接操作它。

这就实现了一种简单的数据封装。

当然,在 Python 中,更常见的封装方式是类。

但闭包也可以在一些轻量场景下,起到类似“私有变量”的效果。


十二、闭包的优点和缺点

闭包很好用,但也不是没有代价。


优点一:可以保存状态

闭包可以让函数记住上一次执行后的数据。

这非常适合:

  • 计数器;
  • 缓存;
  • 配置函数;
  • 回调函数;
  • 装饰器。

例如计数器中的 count,就会随着函数多次调用一直保存下来。


优点二:可以减少全局变量

如果不用闭包,很多状态可能会被写成全局变量。

全局变量一多,代码就容易混乱。

而闭包可以把变量藏在函数内部,只通过返回的内层函数来操作它。

这样可以减少变量污染,也能降低被误修改的风险。


优点三:可以生成定制函数

通过外层函数传入不同参数,可以生成不同功能的函数。

比如:

double = make_multiplier(2)
triple = make_multiplier(3)

这两个函数逻辑相似,但携带的参数不同,所以行为也不同。

这在函数式编程中非常常见。


缺点一:可能增加内存占用

闭包会让外层函数中的某些变量继续存在。

如果闭包引用了很大的对象,并且闭包一直没有被释放,那么这些对象也不会被垃圾回收。

例如:

def outer():
    big_data = [0] * 10000000

    def inner():
        return len(big_data)

    return inner


func = outer()

只要 func 还存在,big_data 就不会被回收。

所以闭包虽然方便,但也要注意不要无意中长期引用大对象。


缺点二:逻辑可能不够直观

闭包中的变量生命周期比普通局部变量更复杂。

普通局部变量一般随着函数执行结束就消失。

但闭包变量会因为被内层函数引用而继续存在。

对于初学者来说,如果闭包嵌套得太多,代码就会变得不好理解。

所以闭包适合用在确实需要“保存状态”或者“生成函数”的场景中。

不建议为了炫技而滥用。


十三、再回头看最开始的问题

现在我们再回头看最开始这段代码:

def outer():
    x = 10

    def inner():
        nonlocal x
        x += 1
        return x

    return inner


print(outer()())
print(outer()())

为什么输出是:

11
11

因为每次执行:

outer()()

都会重新调用一次 outer()

每次调用 outer(),都会创建一个新的 x = 10,也会创建一个新的 inner 函数。

所以两次调用之间并没有共享同一个 x

而下面这种写法:

counter = outer()

print(counter())
print(counter())
print(counter())

输出:

11
12
13

是因为:

counter = outer()

只调用了一次 outer()

返回的 inner 函数被 counter 保存下来。

后面每次调用:

counter()

都是在调用同一个 inner

所以它操作的是同一个 x

这就是闭包能够“记住状态”的真正原因。


十四、一句话总结闭包

闭包的核心可以用一句话概括:

闭包就是一个函数记住了它定义时所在环境中的变量。

普通函数执行完后,局部变量通常就结束了。

但闭包不一样。

如果外层函数中的变量被内层函数引用,并且内层函数被返回到外部继续使用,那么这些变量就会随着闭包一起继续存在。

它就像给函数装了一个“记忆胶囊”。

函数负责执行逻辑。

闭包变量负责保存状态。

如果你看到这种结构:

def outer():
    x = 10

    def inner():
        return x

    return inner

就可以想到:

inner 不只是一个函数,
它还带着外层变量 x 的记忆。

理解了闭包,后面再学习装饰器、函数式编程、回调函数,就会轻松很多。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇