R

ReflexKR

Reflex 官方文档

名牌机场:Python 对象模型

·

课程概览

1

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

2

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

3

箱中箱:容器与复制

4

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

5

区域与通行证:作用域

6

随身行李:闭包

7

箱子规格书:类与实例

8

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

9

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

10

安检口:属性访问的秘密

11

无主的箱子:内存与 GC

12

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

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

时间 3:20–4:10

区域与通行证:作用域

失物报告单

LOST-005

近在眼前却找不到的名字

报告人

安检口运营组 韩主管

丢了什么

统计已安检箱子数量的计数器。最上面明明写着 scanned = 0,可一调用 scan() 就报 UnboundLocalError。

结案条件

解释函数为什么看不到外面的名字,并把计数器改成能正确计数。

天空岛国际机场 行李中心

都美兰班长用手指点着墙上的机场平面图。“登机口、航站楼、机场、国家。通行证从里往外查。里面查到了,外面就不看了。”

今天的报告是安检口的计数器。在外面放了 scanned = 0,打算在函数里一次加一,结果第一行就报错说“没有这个值”。班长指着函数的最后一行:“凶手在这儿。给它赋值的那一行。”

5-1. 查找名字的顺序:LEGB(9 分钟)

读取一个名字时,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 3
  • seat 在 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"

要找的名字

boarding 里的四个名字分别在 L、E、G、B 找到。broken 里的 zone 明明外面有,却停在了 L。
  • 选一下 seat @ terminal 看看。seat 是内层函数 boarding 的局部名字,在外面的 terminal 里看不到。查找的方向永远是从里往外。
  • zone @ broken 在 L 处显示“在这里找到”。这表示这个名字被定为该函数的局部名字,并不表示里面已经有值。

5-2. 赋值的位置决定作用域(10 分钟)

这是报告人的代码:

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))
# True
  • co_varnames 是编译器定下的局部名字清单,scanned 已经在里面了。
  • 所以第 4 行找的是“局部标签 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

正在准备实验台…

刚刚变化的

封口的箱子(不可变)

第一种写法里,scan 栏中只有 bag 标签,scanned 标签是空的。global 用的是外面的标签;第三种则把值作为参数接收,再返回出去。

小钉提示

→ 看到 UnboundLocalError,就往报错那一行的下面看。同一个函数里,一定有哪里在给这个名字赋值。

5-3. 直接移动外面的标签:global 与 nonlocal(7 分钟)

想给外层区域的名字赋值,就要事先声明。

scanned = 0

def scan(bag):
    global scanned
    scanned = scanned + 1

scan("T1")
scan("T2")
print(scanned)
# 2
def terminal():
    boarded = 0
    def board():
        nonlocal boarded
        boarded += 1
    board()
    board()
    return boarded

print(terminal())
# 2
  • global 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 课的闭包里再次用到。

5-4. 会创建区域的和不会创建的(5 分钟)

在 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),但名字不外漏的规则没有变。

5-5. 遮住内置名字(5 分钟)

内置名字位于最外层(B)。所以一旦在全局创建了同名的名字,之后就会先找到全局的那个。

list = ["T1", "T2"]
print(list("ab"))
# TypeError: 'list' object is not callable

list 这个标签在全局挂到了一个列表箱子上,于是 list("ab") 想调用这个列表,失败了。内置的 list 并没有消失,只是被遮住了。

list = ["T1", "T2"]
del list
print(list("ab"))
# ['a', 'b']

import builtins
print(builtins.list is list, builtins.len("abc"))
# True 3
  • 摘掉全局标签 list,内置的 list 就又能看到了。
  • 内置名字放在 builtins 模块里。即使被遮住,也随时可以用 builtins.list 取出来。
  • 这也是第 1 课练习 3 的答案:shout = print 之后执行 del print,会得到 NameError: name 'print' is not defined。全局根本没有叫 print 的标签,也就无从摘起。

不要把 list、dict、str、id、type、input、max、sum 这类名字当变量用。它们会在整个文件里遮住内置函数。

5-6. 窥看区域: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(),用字典。

结案(10 分钟)

修好计数器的方法有两种:直接移动外面的标签(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。改为把值作为参数传入再返回,问题解决。

课后练习

  1. 在 5-1 的 boarding 里,print 下面加一行 zone = "L",会怎样?在作用域地图上和 zone @ broken 对比一下。
  2. 不用 nonlocal,写一个往外层函数的列表里 append 来计数的计数器。为什么不声明也行?(提示:移动标签与改动箱子)
  3. 在模块最上面写 sum = 0,再调用 sum([1, 2, 3]),会报什么错?用 builtins 修好它。

班长的工作手册

  • 名字按 L → E → G → B 的顺序查找,在第一个找到的地方停下。

  • 区域不是由在哪里调用决定的,而是由写在哪里决定的。

  • 在函数内任何地方赋值,这个名字在整个函数里都是局部名字。UnboundLocalError 由此而来。

  • 只有移动外面的标签时才用 global、nonlocal。只读取时不需要。

  • 循环和 if 不创建区域,函数和推导式会创建。

  • list = … 会遮住内置的 list。内置名字在 builtins 里。

  • 函数里的 locals() 是快照(3.13,PEP 667)。修改它,变量不变。

← 上一课

第4课 · 交出箱子的时候:函数与参数

下一课 →

第6课 · 随身行李:闭包

订阅新课时和新课程通知

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

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

问答

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

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

R

ReflexKR

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

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

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

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

Built with Reflex