小白学 Python
AI 编程指南GitHub
© 2026 小白学 Python · 基于 walter201230/Python 教程
课程目录关于本站联系方式隐私政策GitHub

闭包

完整讲解 · 4 段教学·配 3 道练习题·预计 25 分钟

本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。

教学 01 / 04

第十五节:闭包

这一节我们通过解决一个实际需求来认识闭包。

需求:累加学习时间

我们需要一直记录自己的学习时间,以分钟为单位。比如:

  • 第一次学习了 2 分钟,返回 2
  • 隔了一会儿又学习了 10 分钟,返回 12(累加)
  • 一直累加下去……

最直觉的写法是用一个全局变量记录时间,然后用一个函数往里加:

python到闯关页运行这段 →
time = 0

def insert_time(min):
    time = time + min
    return time

print(insert_time(2))
print(insert_time(10))

认真想一下,这段代码会有什么问题呢?

直接报错

其实,这段代码在 Python 里会报错:

UnboundLocalError: local variable 'time' referenced before assignment

为什么?因为在 Python 中,如果一个函数里给一个名字赋值了,这个名字就被认为是局部变量——哪怕外面有同名的全局变量。函数内 time = time + min 这一行,time 已经被当成局部变量了,但右边又先去取它的值,所以报「先用后定义」。

用 global 关键字?

如果确实要在函数中引用并修改全局变量,可以用 global 关键字:

python到闯关页运行这段 →
time = 0

def insert_time(min):
    global time
    time = time + min
    return time

print(insert_time(2))
print(insert_time(10))

输出:

2
12

可是啊,这里使用了全局变量——开发中能避免就尽量避免。

为什么?

  • 不同模块、不同函数都能自由访问、修改全局变量,会造成不可预知的 bug
  • 全局变量降低了模块之间的通用性——大家都依赖它
  • 全局变量降低了代码的可读性——阅读者不知道某个名字到底是哪儿来的

那有没有更优雅的办法呢?这就是闭包登场的时候了。

教学 02 / 04

一、什么是闭包

1、用闭包解决累加问题

我们直接看代码:

python到闯关页运行这段 →
def study_time(time):
    def insert_time(min):
        nonlocal time
        time = time + min
        return time

    return insert_time


f = study_time(0)
print(f(2))
print(f(10))

输出:

2
12

这就是闭包——「函数里嵌套函数,内层函数返回出去带着外层变量」。

2、闭包的定义

当一个内部函数被当作对象返回时,它夹带了外部函数的局部变量,就形成了一个闭包。

换句话说:内部函数的局部作用域中可以访问外部函数局部作用域中的变量,这种行为就叫闭包(Closure)。

回头看上面的代码:

python到闯关页运行这段 →
def study_time(time):              # 外层函数:接收一个 time
    def insert_time(min):          # 内层函数:实现累加逻辑
        nonlocal time
        time = time + min
        return time

    return insert_time             # 外层返回内层函数对象(在 Python 中,函数也是对象)


f = study_time(0)                  # 调用 study_time,f 现在等于 insert_time 函数
print(f(2))                        # 相当于调用 insert_time(2)
print(f(10))                       # 相当于调用 insert_time(10)

f 不是数字,不是字符串——它是一个带着 time 这个变量的函数。每次调用 f 时,那个 time 还在它身边,所以累加可以一直延续。

3、闭包的好处

  • 避免使用全局变量 —— 数据藏在闭包里,不污染全局
  • 把函数和它操作的数据「绑定」起来 —— 类似一个超轻量的对象
  • 是后面要学的装饰器的基础

听起来挺玄,下一节我们会重点讲让闭包修改外层变量的关键字 nonlocal,并做几道练习。

练习 1 / 3加法器工厂去闯关页做这题 →
教学 03 / 04

二、nonlocal 关键字

上一节代码里有一行 nonlocal time,它是干嘛的?

1、为什么需要 nonlocal

回顾一下作用域规则:

  • 函数里给一个名字赋值 → 这个名字就是局部变量
  • 想引用全局变量并修改它 → 用 global
  • 想引用外层函数(不是全局!)的变量并修改它 → 用 nonlocal

闭包的场景就是「内层函数想改外层函数的变量」,刚好对应 nonlocal。

2、对比:不写 nonlocal 会怎样?

python到闯关页运行这段 →
def make_counter():
    count = 0
    def counter():
        count = count + 1   # 没写 nonlocal —— 报 UnboundLocalError
        return count
    return counter

直接调 make_counter()() 会报错,原因和第一节里全局变量那个例子一样。

加上 nonlocal:

python到闯关页运行这段 →
def make_counter():
    count = 0
    def counter():
        nonlocal count
        count = count + 1
        return count
    return counter

c = make_counter()
print(c())
print(c())
print(c())

输出:

1
2
3

每次调用 c(),闭包里的 count 都会增加并被记住——一个独立的计数器就这么做出来了。

3、只读不需要 nonlocal

如果只是读取外层变量、不修改它,是不用 nonlocal 的:

python到闯关页运行这段 →
def make_greeter(name):
    def greet():
        print(f"你好,{name}")    # 只读,不需要 nonlocal
    return greet

g = make_greeter("两点水")
g()

输出:

你好,两点水

一句话总结:要在内层函数里给外层变量重新赋值,就要 nonlocal;只读不用。

练习 2 / 3用闭包实现计数器去闯关页做这题 →
教学 04 / 04

三、闭包的实用场景

1、工厂函数

闭包可以根据参数生成定制化的函数——典型用法是「加法器工厂」:

python到闯关页运行这段 →
def make_adder(n):
    def adder(x):
        return x + n
    return adder

add5 = make_adder(5)
add10 = make_adder(10)

print(add5(3))     # 8
print(add10(3))    # 13

make_adder(5) 和 make_adder(10) 返回了两个不同的函数,但它们各自带着自己的 n,互不干扰。这就是闭包的「绑定」效果。

2、计数器、累加器

刚才学的 make_counter 就是这一类——把状态藏在闭包里,外面看不见、也改不了。

python到闯关页运行这段 →
def make_counter():
    count = 0
    def counter():
        nonlocal count
        count = count + 1
        return count
    return counter

c1 = make_counter()
c2 = make_counter()
print(c1())   # 1
print(c1())   # 2
print(c2())   # 1,c1 和 c2 互不影响

每个 make_counter() 调用都创建一个独立的闭包,各自维护自己的 count。

3、验证一个函数是不是闭包

所有函数都有一个 __closure__ 属性。普通函数它是 None;闭包它是一个由 cell 组成的元组,cell 的 cell_contents 就是闭包绑定的变量。

python到闯关页运行这段 →
def make_counter():
    count = 0
    def counter():
        nonlocal count
        count = count + 1
        return count
    return counter

c = make_counter()
print(c.__closure__ is not None)       # True
c()
c()
print(c.__closure__[0].cell_contents)  # 2

输出:

True
2

4、闭包 vs 类

闭包的过程其实好比类(父函数)生成实例(闭包)。父函数只在调用时执行,执行完毕后其环境就会释放——但闭包还在,因为内层函数把它「带走」了。

对那些需要重用、但又不足以定义为类的小功能,用闭包比写类更轻量、更灵活。

下一章我们学装饰器——它就是闭包最经典的应用。

练习 3 / 3用闭包累加学习时间去闯关页做这题 →

读完了?动手练一遍才算真会。

去闯关模式练习 →