小白学 Python
AI 编程指南
© 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加法器工厂

请定义一个函数 make_adder(n):

  • 接收一个数字 n
  • 返回一个内层函数 adder(x),作用是返回 x + n

然后:

  1. 用 make_adder(10) 生成 add10
  2. print(add10(5)) —— 应该输出 15
  3. 用 make_adder(100) 生成 add100
  4. print(add100(5)) —— 应该输出 105

输出应该是:

15
105
💡 提示(3 条)
  1. 内层函数只读 n,不修改它,所以不需要 nonlocal

  2. make_adder(10) 和 make_adder(100) 是两个不同的闭包,各自带着不同的 n

  3. 完整代码:

    def make_adder(n):
        def adder(x):
            return x + n
        return adder
    
    add10 = make_adder(10)
    print(add10(5))
    add100 = make_adder(100)
    print(add100(5))
    
在编辑器里写这题,跑判分、看参考答案去闯关页做这题 →
教学 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用闭包实现计数器

请定义一个函数 make_counter():

  • 内部维护一个 count = 0
  • 返回一个内层函数 counter(),每次调用 counter() 时让 count 加 1 并返回新值(记得用 nonlocal)

然后:

  1. c = make_counter()
  2. 连续调用 3 次 c(),每次都 print 它的返回值

输出应该是:

1
2
3
💡 提示(3 条)
  1. 外层函数定义 count = 0,内层函数用 nonlocal count 才能改它

  2. 外层函数 return 内层函数对象(注意:return counter,不是 return counter())

  3. 完整代码:

    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())
    
在编辑器里写这题,跑判分、看参考答案去闯关页做这题 →
教学 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用闭包累加学习时间

回到本章开头的需求——用闭包累加学习时间。

请定义函数 study_time(time):

  • 接收初始 time
  • 返回一个内层函数 insert_time(min),每次调用时把 min 累加到 time 上并返回当前总时间(记得用 nonlocal)

然后:

  1. f = study_time(0)
  2. print(f(2)) —— 学了 2 分钟
  3. print(f(10)) —— 又学了 10 分钟
  4. print(f(5)) —— 又学了 5 分钟

输出应该是:

2
12
17
💡 提示(3 条)
  1. 外层 study_time 接收 time 作为参数;内层 insert_time 通过 nonlocal time 修改它(参数名 min 沿用原教程写法,它会临时遮住内置函数 min()——真实项目里建议用 minutes 这类不冲突的名字)

  2. 外层函数 return 内层函数对象 insert_time(不要带括号)

  3. 完整代码:

    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))
    print(f(5))
    
在编辑器里写这题,跑判分、看参考答案去闯关页做这题 →

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

去闯关模式练习 →