作用域
约 1388 字大约 5 分钟
2026-05-27
作用域决定 "一个名字在哪儿可见、写它会改到哪一层"。Python 用 LEGB 规则查找名字,并在函数 编译期 就定好哪些名字是局部变量——很多 UnboundLocalError 都来自这里
LEGB 规则
查找名字时按这个顺序:
| 优先级 | 层级 | 含义 |
|---|---|---|
| 1 | Local | 当前函数内 |
| 2 | Enclosing | 外层嵌套函数 |
| 3 | Global | 模块顶层 |
| 4 | Built-in | 内置名,如 len / print |
LEGB 查找顺序
x = "global"
def outer() -> None:
x = "enclosing"
def inner() -> None:
x = "local"
print(x) # local
inner()
print(x) # enclosing
outer()
print(x) # global内层的赋值默认只绑定当前层,不会自动改外层
Local:局部作用域
函数内赋值的名字、以及参数,都是局部变量
局部变量不可外泄
def demo() -> None:
local_var = 100
print(local_var)
demo()
# print(local_var) # NameError
def greet(name: str) -> None:
message = f"Hello, {name}"
print(message)
greet("Alice")
# print(name) # NameError每次调用函数都会新建一帧局部命名空间,调用结束即可回收(闭包引用的除外)
Global:全局作用域
模块顶层的名字。函数内 读取 全局变量通常可以直接读;赋值 默认会创建局部变量
读取全局变量
count = 0
def show() -> None:
print(count) # 读全局,OK
show()直接 count += 1 会触发 UnboundLocalError(既读又写,被当成局部)。要用全局名字重新绑定,需要 global:
global 修改全局变量
count = 0
def increment() -> None:
global count
count += 1
print(count)
increment() # 1
increment() # 2
print(count) # 2global 声明的是 "这个名字走全局命名空间",不是 "创建全局变量的另一种语法"。能少用就少用,优先返回值或对象属性传递状态
故意触发错误可以取消下面注释自行试:
count = 0
def wrong() -> None:
count += 1 # UnboundLocalError
# wrong()Enclosing:闭包作用域
嵌套函数可以读外层函数的变量,这是闭包的基础
读取 enclosing 变量
def outer() -> None:
x = "outer"
def inner() -> None:
print(x) # 读 enclosing
inner()
outer()内层若要 改写 外层绑定,用 nonlocal(不能用来改全局,全局用 global):
nonlocal 与闭包计数器
def make_counter():
count = 0
def increment() -> int:
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3increment 被返回后,外层的 count 仍然活着,因为内层函数对象引用了它——这就是闭包 "带走状态" 的机制
| 关键字 | 改写目标 |
|---|---|
global | 模块全局 |
nonlocal | 最近一层 enclosing(不含全局) |
编译期判定局部变量
Python 在编译函数体时扫描赋值:只要函数体内某处对 x 赋值(含 +=),整个函数里的 x 都当局部变量处理
x = 10
def demo() -> None:
print(x) # 想读全局,但下面有赋值 → 全程当局部
x = 20
# demo() # UnboundLocalError: local variable 'x' referenced before assignment修正:
用 global 修正 UnboundLocalError
x = 10
def demo() -> None:
global x
print(x)
x = 20
demo()
print(x) # 20或避免改全局,改为返回新值
默认参数的求值时机
默认参数在 函数定义时 求值,只求一次
可变默认参数被共享
def add_item(item: int, items: list[int] = []) -> list[int]:
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2],共享了同一个默认列表正确写法是用不可变哨兵:
None 哨兵修复默认参数
def add_item(item: int, items: list[int] | None = None) -> list[int]:
if items is None:
items = []
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [2]None、数字、字符串、元组作默认值是安全的;list / dict / set 要警惕
作用域速查
四种作用域写法对照
# 1. 只读全局
g = 1
def f1() -> None:
print(g)
f1()
# 2. 嵌套只读 enclosing
def f2():
n = 1
def inner() -> int:
return n + 1
return inner
print(f2()())
# 3. 改 enclosing
def f3():
n = 1
def inner() -> int:
nonlocal n
n += 1
return n
return inner
inc = f3()
print(inc(), inc())
# 4. 改 global
def f4() -> None:
global g
g += 1
f4()
print(g)类体作用域不是 enclosing
类语句执行时会建一个临时命名空间,但 方法函数并不会把类体当成 LEGB 的 E 层。方法里要访问类属性,应写类名或 self / cls,不能指望像嵌套函数那样直接读类体里的名字
类体名字对方法不可见
class Demo:
x = 10
def show(self) -> None:
# print(x) # NameError:不会去类体里找
print(Demo.x, self.x)
Demo().show()类与实例属性的完整规则见 类和对象
内置名遮蔽
LEGB 最后一层是 built-in。若在局部或全局把 list / str / type 等绑成别的对象,同作用域内就调不到真正的内置构造器
不要遮蔽内置名
def broken() -> None:
list = [1, 2, 3] # 局部名字 list
print(list)
# print(list((4, 5))) # TypeError: 'list' object is not callable
broken()
def ok() -> None:
items = [1, 2, 3]
print(list((4, 5))) # 正常的内置 list
ok()闭包与循环变量
for 循环变量在函数作用域里只有 一个 绑定,循环结束后名字停在最后一次的值。若 lambda / 内层函数延迟读取它,会全部看到同一个最终值
循环变量与闭包
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2]
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # 默认参数在定义时绑定当前 i
print([f() for f in funcs]) # [0, 1, 2]同一问题在 lambda 表达式 里也会遇到,解决思路相同:默认参数绑定,或 functools.partial
