失物报告单
LOST-005
报告人
安检口运营组 韩主管
丢了什么
统计已安检箱子数量的计数器。最上面明明写着 scanned = 0,可一调用 scan() 就报 UnboundLocalError。
结案条件
解释函数为什么看不到外面的名字,并把计数器改成能正确计数。
天空岛国际机场 行李中心
都美兰班长用手指点着墙上的机场平面图。“登机口、航站楼、机场、国家。通行证从里往外查。里面查到了,外面就不看了。”
今天的报告是安检口的计数器。在外面放了 scanned = 0,打算在函数里一次加一,结果第一行就报错说“没有这个值”。班长指着函数的最后一行:“凶手在这儿。给它赋值的那一行。”
读取一个名字时,Python 会从里往外依次查找四个区域,在第一个找到的地方停下。
| 顺序 | 区域 | 是什么 | 放到机场里就是 |
|---|---|---|---|
| L | 局部(Local) | 当前正在执行的函数内部 | 登机口 |
| E | 外层函数(Enclosing) | 包住这个函数的外层函数们 | 航站楼 |
| G | 全局(Global) | 定义该函数的模块 | 机场 |
| B | 内置(Built-in) | len、print、list 这类基本名字 | 国家 |
zone = "G"
def terminal():
gate = "E"
def boarding():
seat = "L"
print(seat, gate, zone, len("abc"))
boarding()
terminal()
# L E G 3seat 在 boarding 内部(L)找到,gate 在外层函数 terminal(E)找到。zone 在模块(G)找到,len 在内置(B)找到。NameError。boarding,gate 也还是去 terminal 里找。下面的作用域地图不运行代码,只是读一读,就能显示每个名字在哪个区域找到。最后那个 broken 函数,是下一节的主角。
作用域地图
1
zone = "G"
2
3
def terminal():
4
gate = "E"
5
def boarding():
6
seat = "L"
7
print(seat, gate, zone, len("abc"))
8
boarding()
9
10
def broken():
11
print(zone)
12
zone = "L"
要找的名字
seat @ terminal 看看。seat 是内层函数 boarding 的局部名字,在外面的 terminal 里看不到。查找的方向永远是从里往外。zone @ broken 在 L 处显示“在这里找到”。这表示这个名字被定为该函数的局部名字,并不表示里面已经有值。这是报告人的代码:
scanned = 0
def scan(bag):
print(bag, scanned)
scanned = scanned + 1
scan("T1")
# UnboundLocalError: cannot access local variable 'scanned' where it is not associated with a value报错的不是第 5 行,而是第 4 行的 print。第 5 行根本还没执行。即便如此,Python 也不会去外面找第 4 行的 scanned。
规则只有一条:只要在函数内任何地方给一个名字赋值,这个名字在整个函数里就是局部名字。这个决定不是在运行时做出的,而是在编译函数时。
def scan(bag):
print(bag, scanned)
scanned = scanned + 1
print(scan.__code__.co_varnames)
# ('bag', 'scanned')
print(issubclass(UnboundLocalError, NameError))
# Trueco_varnames 是编译器定下的局部名字清单,scanned 已经在里面了。scanned”,而这个标签还没挂到任何箱子上。错误信息说的正是这件事。UnboundLocalError 是 NameError 的一种。不会执行的行也照样算数:
x = 10
def f():
print(x)
if False:
x = 1
f()
# UnboundLocalError: cannot access local variable 'x' where it is not associated with a value“赋值”不只有 =。+=、for x in ...、import x、def x():、class x:、with ... as x、except ... as x、del x,都会让这个名字成为该函数的局部名字。
在下面的标签板上比较三种写法。第一种是报告人的代码,最后一步会显示错误。
标签板
步骤 1 / 0
下一行将执行: 0
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
小钉提示
→ 看到 UnboundLocalError,就往报错那一行的下面看。同一个函数里,一定有哪里在给这个名字赋值。
global 与 nonlocal(7 分钟)想给外层区域的名字赋值,就要事先声明。
scanned = 0
def scan(bag):
global scanned
scanned = scanned + 1
scan("T1")
scan("T2")
print(scanned)
# 2def terminal():
boarded = 0
def board():
nonlocal boarded
boarded += 1
board()
board()
return boarded
print(terminal())
# 2global x:在这个函数里,x 是模块的名字。赋值时移动的是模块里的标签。nonlocal x:在这个函数里,x 是最近一层外层函数的名字。不能用于模块的名字。nonlocal 要指向的名字,运行之前就会报 SyntaxError: no binding for nonlocal 'x' found。global x,会得到 SyntaxError: name 'x' is assigned to before global declaration。只是读取的话,不需要声明。5-1 里的 boarding 没有声明就读取了 gate 和 zone。只有赋值时才需要声明。
global 会悄悄改动函数外部的状态,让函数难以单独测试。通常更好的做法是把值作为参数传进来,再返回新值。nonlocal 会在第 6 课的闭包里再次用到。
在 Python 里,会创建新区域的是函数(def、lambda)、推导式和类体。if、for、while、with、try 代码块都不会创建区域。
for i in range(3):
pass
print(i)
# 2
if True:
inside = "if"
print(inside)
# if
sq = [n * n for n in range(3)]
print(sq)
# [0, 1, 4]
print(n)
# NameError: name 'n' is not defined| 会创建 | 不会创建 |
|---|---|
def、lambda、列表/集合/字典推导式、生成器表达式、class 体 | if、for、while、with、try |
for 结束后,循环变量 i 仍保留着最后一个值(2)。在循环里创建的名字也一样。n 不会漏到外面。从 3.12 开始,CPython 不再为推导式单独创建函数,而是内联展开执行(PEP 709),但名字不外漏的规则没有变。内置名字位于最外层(B)。所以一旦在全局创建了同名的名字,之后就会先找到全局的那个。
list = ["T1", "T2"]
print(list("ab"))
# TypeError: 'list' object is not callablelist 这个标签在全局挂到了一个列表箱子上,于是 list("ab") 想调用这个列表,失败了。内置的 list 并没有消失,只是被遮住了。
list = ["T1", "T2"]
del list
print(list("ab"))
# ['a', 'b']
import builtins
print(builtins.list is list, builtins.len("abc"))
# True 3list,内置的 list 就又能看到了。builtins 模块里。即使被遮住,也随时可以用 builtins.list 取出来。shout = print 之后执行 del print,会得到 NameError: name 'print' is not defined。全局根本没有叫 print 的标签,也就无从摘起。不要把 list、dict、str、id、type、input、max、sum 这类名字当变量用。它们会在整个文件里遮住内置函数。
locals()、globals()、vars()、dir()(4 分钟)globals():模块区域的字典。在模块里调用时,它就是真正的存储本身。locals():当前区域的名字。在模块里和 globals() 相同。vars(obj):对象的 __dict__。不带参数调用时和 locals() 相同(第 7 课)。dir():只返回排好序的名字列表。从 3.13 开始,函数里的 locals() 含义变得明确了(PEP 667):每次调用都返回一个新字典,是那一刻的快照。修改这个字典,局部变量并不会变。
def check():
y = 1
locals()["y"] = 99
print(y, locals()["y"])
snap = locals()
y = 2
print(snap["y"], locals()["y"])
check()
# 1 1
# 1 2
def where(a):
b = 2
return dir(), vars()
print(where(1))
# (['a', 'b'], {'a': 1, 'b': 2})locals()["y"] = 99 改的只是刚生成的快照,变量 y 仍然是 1。snap 保留着拍下那一刻的值(1)。再次调用的 locals() 才能看到 2。locals(),用字典。修好计数器的方法有两种:直接移动外面的标签(global),或者把值传进来、再返回新值。
scanned = 0
def scan(bag):
global scanned
print(bag, scanned)
scanned = scanned + 1
scan("T1")
scan("T2")
print(scanned)
# T1 0
# T2 1
# 2
def scan_count(bag, count):
print(bag, count)
return count + 1
total = 0
total = scan_count("T1", total)
total = scan_count("T2", total)
print(total)
# T1 0
# T2 1
# 2韩主管选了第二种。这样即使安检口增加到好几台,也能每台分别计数。班长补充道:“每个区域都有自己的标签。想移动别人区域里的标签,先出示通行证。”
结案报告 — LOST-005。函数下方的 scanned = ... 让 scanned 成了整个函数的局部名字,于是第一行就报 UnboundLocalError。改为把值作为参数传入再返回,问题解决。
boarding 里,print 下面加一行 zone = "L",会怎样?在作用域地图上和 zone @ broken 对比一下。nonlocal,写一个往外层函数的列表里 append 来计数的计数器。为什么不声明也行?(提示:移动标签与改动箱子)sum = 0,再调用 sum([1, 2, 3]),会报什么错?用 builtins 修好它。班长的工作手册
名字按 L → E → G → B 的顺序查找,在第一个找到的地方停下。
区域不是由在哪里调用决定的,而是由写在哪里决定的。
在函数内任何地方赋值,这个名字在整个函数里都是局部名字。UnboundLocalError 由此而来。
只有移动外面的标签时才用 global、nonlocal。只读取时不需要。
循环和 if 不创建区域,函数和推导式会创建。
list = … 会遮住内置的 list。内置名字在 builtins 里。
函数里的 locals() 是快照(3.13,PEP 667)。修改它,变量不变。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。