R

ReflexKR

Reflex 官方文档

名牌机场:Python 对象模型

·

课程概览

1

标签不是箱子:名字与对象

2

封口的箱子:可变与不可变

3

箱中箱:容器与复制

4

交出箱子的时候:函数与参数

5

区域与通行证:作用域

6

随身行李:闭包

7

箱子规格书:类与实例

8

机场标准流程:双下划线方法

9

规格书的规格书:继承与 MRO

10

安检口:属性访问的秘密

11

无主的箱子:内存与 GC

12

储物柜号码:哈希与相等,综合

名牌机场:Python 对象模型
第6课

时间 4:10–5:00

随身行李:闭包

失物报告单

LOST-006

三位乘客都去了同一个登机口

报告人

登机广播负责人 徐主管

丢了什么

三个登机口引导函数。用循环分别为 A1、B7、C3 创建了一个,可三个都让乘客去 C3。

结案条件

确认三个函数各自带着什么,并让每个函数记住自己的登机口。

天空岛国际机场 行李中心

早上的广播刚结束,本该去三个不同登机口的乘客,全都站在了 C3 前面。徐主管说,引导函数做了三个,引导也应该是三种才对。

班长举起一个随身行李箱。“函数也会随身带着箱子。问题是里面装的是值,还是名牌。”今天,我们来打开函数随身带着的那个小箱子:闭包。

6-1. 返回函数的函数(7 分钟)

就像第 1 课看到的,函数也是对象。可以绑定到名字上、放进列表、传给别的函数,也可以被返回。

def make_tag(owner):
    def tag():
        return "owner: " + owner
    return tag

t = make_tag("kim")
print(t())
# owner: kim
print(t.__name__, t.__qualname__)
# tag make_tag.<locals>.tag
  • 每次调用 make_tag,都会执行 def tag():,创建一个新的函数对象。
  • 调用 t() 的那一刻,make_tag 早已结束。按第 5 课的规则,make_tag 的局部名字 owner 也应该一起消失了。
  • 可 t() 却记得 "kim"。已经结束的函数的名字,内层函数是怎么拿住的?

6-2. 随身行李:cell 与 __closure__(9 分钟)

当内层函数用到外层函数的名字时,Python 会把这个名字装进一个叫单元(cell)的小箱子里。外层函数的栏和内层函数指向同一个 cell。即使外层函数结束、栏消失了,内层函数还拿着 cell,cell 和里面的值就留了下来。带着这组 cell 到处走的函数,就是闭包。

def make_tag(owner):
    def tag():
        return "owner: " + owner
    return tag

t = make_tag("kim")
print(t.__code__.co_freevars)
# ('owner',)
print(type(t.__closure__[0]).__name__)
# cell
print(t.__closure__[0].cell_contents)
# kim
print(make_tag.__code__.co_cellvars)
# ('owner',)
属性含义
inner.__code__.co_freevars内层函数从外面借用的名字(自由变量)
inner.__closure__每个名字对应一个 cell,组成的元组
cell.cell_contentscell 当前指向的对象
outer.__code__.co_cellvars外层函数这边做成 cell 的名字

在标签板上,make_tag 运行期间,栏里的 owner 显示的是打开 cell 后看到的值。看看栏消失之后,function tag 的 owner 一行是不是仍然通过 cell 抓着 'kim'。

标签板

步骤 1 / 0

下一行将执行: 0

正在准备实验台…

刚刚变化的

封口的箱子(不可变)

make_tag 结束后,它的栏消失了,但函数 tag 拿着的 cell 仍然抓着 'kim'。
  • 随身行李里只装内层函数真正用到的名字。外层函数的其他局部名字,会在函数结束时一起清理掉。
  • 反过来,装进去的东西会一直留到那个函数死去。哪怕内层函数只用过一次某个大列表,那个列表也会一起留下来。
def make_tag(owner):
    notes = ["memo"] * 1000
    def tag():
        return owner
    return tag

print(make_tag("kim").__code__.co_freevars)
# ('owner',)

小钉提示

→ 闭包拿着的不是值的副本,而是 cell,也就是挂名牌的那个位置。所以在外面移动这个名字,闭包看到的值也会跟着变。

6-3. 延迟绑定:三位乘客为什么都去了 C3(10 分钟)

这是报告人的代码:

def make_guides(gates):
    guides = []
    for gate in gates:
        guides.append(lambda: gate)
    return guides

gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['C3', 'C3', 'C3']
print(gs[0].__closure__[0] is gs[2].__closure__[0])
# True
  • 循环不创建区域(第 5 课)。转三圈的过程中,gate 始终是 make_guides 里的同一个名字,cell 也只有一个。
  • 三个 lambda 带走的都是这一个 cell。循环结束时,cell 里的值是 "C3"。
  • lambda 的函数体 gate 是在调用时才打开 cell 去看的,创建时并没有把值记下来。这叫延迟绑定(late binding)。

换成列表推导式也一样:

fs = [lambda: i for i in range(3)]
print([f() for f in fs])
# [2, 2, 2]
print(fs[0].__closure__[0] is fs[1].__closure__[0])
# True

即使没有循环,也会发生同样的事。创建函数之后在外面移动了名字,函数看到的值也会变。

def make():
    gate = "A1"
    def guide():
        return gate
    gate = "Z9"
    return guide

print(make()())
# Z9

在下面的标签板上,看三个函数的箭头如何汇聚到同一个 cell。在修正版里,每个函数都有自己的 defaults 元组。

标签板

步骤 1 / 0

下一行将执行: 0

正在准备实验台…

刚刚变化的

封口的箱子(不可变)

在 lambda: gate 中,三个函数的 gate 都指向同一个 cell。改成 gate=gate 后,每个函数在创建时的值都装进了默认参数。

修正方法是把“创建那一刻的值”另外存进函数里。

fs = [lambda i=i: i for i in range(3)]
print([f() for f in fs])
# [0, 1, 2]

from functools import partial

def guide(gate):
    return "go to " + gate

guides = [partial(guide, gate) for gate in ["A1", "B7", "C3"]]
print([g() for g in guides])
# ['go to A1', 'go to B7', 'go to C3']
  • lambda i=i: i:默认参数在定义时计算一次(第 4 课)。那一刻的 i 就存进了函数的 __defaults__。
  • functools.partial(guide, gate):把函数和那一刻的参数绑在一起,生成一个新的可调用对象。意图比默认参数的小技巧更清楚。
  • 默认参数的写法有个缺点:调用方可以像 f(5) 这样把值覆盖掉。

6-4. 带状态的函数:nonlocal 计数器(7 分钟)

cell 不只能读。用 nonlocal(第 5 课)声明后,内层函数就能移动 cell 里的标签。这样,函数就有了一份跨越多次调用延续下去的状态。

def counter():
    n = 0
    def tick():
        nonlocal n
        n += 1
        return n
    return tick

c = counter()
c()
c()
print(c(), c.__closure__[0].cell_contents)
# 3 3
c2 = counter()
print(c2(), c())
# 1 4
  • 每次调用 counter() 都会产生一个新 cell。c 和 c2 互不知道对方的数字。
  • 如果没有 nonlocal n,n += 1 会让 n 成为局部名字,从而报 UnboundLocalError(和第 5 课的原因相同)。

标签板

步骤 1 / 0

下一行将执行: 0

正在准备实验台…

刚刚变化的

封口的箱子(不可变)

每调用一次 tick,cell 里的值就从 0 变成 1、2。整数是不可变的,所以变的不是整数,而是 cell 里的标签移到了新的整数上。

同样的事也可以用类来做。状态存放在实例的属性里(第 7 课)。

class Counter:
    def __init__(self):
        self.n = 0
    def tick(self):
        self.n += 1
        return self.n

k = Counter()
k.tick()
print(k.tick(), k.n)
# 2 2
闭包类
状态在哪里cell实例的 __dict__
从外面查看只能通过 __closure__直接 k.n
适合的场合一个动作,少量状态多个动作,多项状态

6-5. 装饰器初体验(7 分钟)

接收一个函数、返回一个新函数的闭包,叫作装饰器。原来的函数装在内层函数的 cell 里。

def log(func):
    def wrapper(*args, **kwargs):
        print("call", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@log
def board(seat):
    return "seat " + seat

print(board("12A"))
# call board
# seat 12A
print(board.__name__)
# wrapper
  • @log 等同于 board = log(board)。名字 board 现在指向 wrapper 函数,原来的 board 则在 wrapper 的 cell func 里。
  • 所以 board.__name__ 变成了 wrapper。这会给调试和文档带来麻烦。

一行 functools.wraps 就能把原函数的名字、文档等信息搬过来。

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("call", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@log
def board(seat):
    return "seat " + seat

print(board.__name__, board.__wrapped__.__name__)
# board board

给装饰器传参数、用类实现装饰器等更深入的话题,会在生成器与装饰器的课程里讲。

结案(10 分钟)

只要让每个函数装好创建时的登机口就行了。

from functools import partial

def announce(gate):
    return "go to " + gate

def make_guides(gates):
    return [partial(announce, gate) for gate in gates]

gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['go to A1', 'go to B7', 'go to C3']

def make_guides_v2(gates):
    return [lambda gate=gate: "go to " + gate for gate in gates]

print([g() for g in make_guides_v2(["A1", "B7", "C3"])])
# ['go to A1', 'go to B7', 'go to C3']

从下一次广播起,三位乘客各自去了自己的登机口。徐主管走后,班长翻着 13 号传送带的记录,自言自语:“已经结束的函数留下的东西,只要有人把它装进随身行李拿着,就不会被扔掉。那两个箱子,说不定也是有人攥着呢。”

结案报告 — LOST-006。三个引导函数共同拿着循环变量 gate 的同一个 cell,调用时读到的都是最后的值 C3(延迟绑定)。用 partial 和默认参数装入创建时的值,问题解决。

课后练习

  1. 把 [lambda: i for i in range(3)] 改成 [lambda: i * 10 for i in range(3)],结果是什么?用 __closure__ 确认一共有几个 cell。
  2. 修改 counter(),让它同时返回 tick() 和 reset() 两个函数。用 is 确认两个函数用的是不是同一个 cell。
  3. 写一个统计函数被调用次数的装饰器 count_calls。次数放在 wrapper 的属性上,还是放在 cell 里?两种都试试。

班长的工作手册

  • 函数也是对象。可以创建函数并返回它。

  • 内层函数用到的外层名字装在 cell 里,函数通过 __closure__ 带着这些 cell。

  • 闭包拿着的不是值,而是 cell(挂名牌的位置)。调用时才打开:延迟绑定。

  • 循环中创建的函数共用一个 cell。修正:lambda i=i: i、functools.partial。

  • 用 nonlocal 移动 cell 里的标签,就得到带状态的函数。状态多了就用类。

  • 装饰器是接收函数、返回函数的闭包。别忘了 functools.wraps。

← 上一课

第5课 · 区域与通行证:作用域

下一课 →

第7课 · 箱子规格书:类与实例

订阅新课时和新课程通知

本课程新增课时或推出后续课程时,我们会发邮件通知你。

收集项目:电子邮箱 · 用途:本课程的新课时和上线通知 · 保存期限:直到你要求删除。不会用于其他用途,也不会提供给他人。回复任意通知邮件,我们会立即删除。

问答

关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。

以纯文本显示,不支持链接和格式。

R

ReflexKR

动手学 Python 和 Reflex。所有示例均已在 Reflex 0.10.0 上实际运行验证。

ReflexKR 免费,没有横幅广告。如果对你有帮助,欢迎请我们喝杯咖啡。 ☕ 请我们喝杯咖啡

联盟链接与赞助说明
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex