失物报告单
LOST-006
报告人
登机广播负责人 徐主管
丢了什么
三个登机口引导函数。用循环分别为 A1、B7、C3 创建了一个,可三个都让乘客去 C3。
结案条件
确认三个函数各自带着什么,并让每个函数记住自己的登机口。
天空岛国际机场 行李中心
早上的广播刚结束,本该去三个不同登机口的乘客,全都站在了 C3 前面。徐主管说,引导函数做了三个,引导也应该是三种才对。
班长举起一个随身行李箱。“函数也会随身带着箱子。问题是里面装的是值,还是名牌。”今天,我们来打开函数随身带着的那个小箱子:闭包。
就像第 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>.tagmake_tag,都会执行 def tag():,创建一个新的函数对象。t() 的那一刻,make_tag 早已结束。按第 5 课的规则,make_tag 的局部名字 owner 也应该一起消失了。t() 却记得 "kim"。已经结束的函数的名字,内层函数是怎么拿住的?__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_contents | cell 当前指向的对象 |
outer.__code__.co_cellvars | 外层函数这边做成 cell 的名字 |
在标签板上,make_tag 运行期间,栏里的 owner 显示的是打开 cell 后看到的值。看看栏消失之后,function tag 的 owner 一行是不是仍然通过 cell 抓着 'kim'。
标签板
步骤 1 / 0
下一行将执行: 0
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
def make_tag(owner):
notes = ["memo"] * 1000
def tag():
return owner
return tag
print(make_tag("kim").__code__.co_freevars)
# ('owner',)小钉提示
→ 闭包拿着的不是值的副本,而是 cell,也就是挂名牌的那个位置。所以在外面移动这个名字,闭包看到的值也会跟着变。
这是报告人的代码:
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])
# Truegate 始终是 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
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
修正方法是把“创建那一刻的值”另外存进函数里。
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) 这样把值覆盖掉。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 4counter() 都会产生一个新 cell。c 和 c2 互不知道对方的数字。nonlocal n,n += 1 会让 n 成为局部名字,从而报 UnboundLocalError(和第 5 课的原因相同)。标签板
步骤 1 / 0
下一行将执行: 0
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
同样的事也可以用类来做。状态存放在实例的属性里(第 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 |
| 适合的场合 | 一个动作,少量状态 | 多个动作,多项状态 |
接收一个函数、返回一个新函数的闭包,叫作装饰器。原来的函数装在内层函数的 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给装饰器传参数、用类实现装饰器等更深入的话题,会在生成器与装饰器的课程里讲。
只要让每个函数装好创建时的登机口就行了。
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 和默认参数装入创建时的值,问题解决。
[lambda: i for i in range(3)] 改成 [lambda: i * 10 for i in range(3)],结果是什么?用 __closure__ 确认一共有几个 cell。counter(),让它同时返回 tick() 和 reset() 两个函数。用 is 确认两个函数用的是不是同一个 cell。count_calls。次数放在 wrapper 的属性上,还是放在 cell 里?两种都试试。班长的工作手册
函数也是对象。可以创建函数并返回它。
内层函数用到的外层名字装在 cell 里,函数通过 __closure__ 带着这些 cell。
闭包拿着的不是值,而是 cell(挂名牌的位置)。调用时才打开:延迟绑定。
循环中创建的函数共用一个 cell。修正:lambda i=i: i、functools.partial。
用 nonlocal 移动 cell 里的标签,就得到带状态的函数。状态多了就用类。
装饰器是接收函数、返回函数的闭包。别忘了 functools.wraps。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。