作用域与 LEGB:函数如何找到名字
核心概念
作用域(scope)决定了名字(变量)在哪里可见、如何查找。Python 使用 LEGB 规则查找名字:
- Local:本地作用域(函数内部)
- Enclosing:外层函数作用域(嵌套函数)
- Global:全局作用域(模块级别)
- Built-in:内置作用域(Python 内置名字)
查找顺序:L → E → G → B,找到第一个匹配就停止。
本地作用域(Local)
函数内部定义的名字是本地的:
def func():
x = 10 # 本地变量
print(x)
func() # 10
print(x) # NameError: name 'x' is not defined函数参数也是本地变量:
def greet(name): # name 是本地变量
message = f"Hello, {name}"
print(message)
greet("Alice") # Hello, Alice
print(name) # NameError全局作用域(Global)
模块顶层定义的名字是全局的:
x = 100 # 全局变量
def func():
print(x) # 读取全局变量
func() # 100全局变量的读与写
读取全局变量:直接使用
counter = 0
def show_counter():
print(counter) # 读取全局 counter
show_counter() # 0修改全局变量:需要 global 关键字
counter = 0
def increment():
counter = counter + 1 # 错误!
increment() # UnboundLocalError问题分析:
counter = ...使 Python 认为counter是本地变量- 右侧
counter + 1试图读取本地counter,但它尚未赋值 - 产生
UnboundLocalError
正确写法:
counter = 0
def increment():
global counter # 声明使用全局 counter
counter = counter + 1
increment()
print(counter) # 1global 关键字告诉 Python:counter 指向全局作用域的变量,不创建本地变量。
何时使用全局变量
常见场景:
- 模块级常量(通常大写)
- 配置参数
- 简单脚本中的共享状态
最佳实践:
- 尽量避免修改全局变量(使用参数和返回值)
- 只读全局常量不需要
global关键字 - 复杂程序用类或闭包管理状态
外层作用域(Enclosing)
嵌套函数可以访问外层函数的变量:
def outer():
x = "outer"
def inner():
print(x) # 访问外层函数的 x
inner()
outer() # outer多层嵌套
def level1():
x = "L1"
def level2():
x = "L2"
def level3():
print(x) # 找到 level2 的 x
level3()
level2()
level1() # L2查找顺序:level3(L) → level2(E) → level1(E) → 全局(G) → 内置(B)
nonlocal 关键字
修改外层函数变量需要 nonlocal:
def outer():
count = 0
def inner():
count = count + 1 # 错误!UnboundLocalError
print(count)
inner()
outer() # UnboundLocalError正确写法:
def outer():
count = 0
def inner():
nonlocal count # 声明使用外层的 count
count = count + 1
print(count)
inner()
inner()
print(f"Final count: {count}")
outer()
# 1
# 2
# Final count: 2nonlocal 告诉 Python:count 指向外层作用域的变量,不创建本地变量。
nonlocal vs global:
nonlocal:指向最近的外层函数作用域global:指向模块级全局作用域
内置作用域(Built-in)
Python 内置名字(如 len, print, int)在内置作用域:
def func():
print(len([1, 2, 3])) # len 是内置函数
func() # 3可以覆盖内置名字(不推荐):
def func():
len = 10 # 覆盖内置 len
print(len) # 10
# print(len([1, 2, 3])) # TypeError: 'int' object is not callable
func()全局覆盖:
len = "not a function"
print(len([1, 2, 3])) # TypeError最佳实践:避免使用内置名字作为变量名。
LEGB 实战示例
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x)
inner()
print(x)
outer()
print(x)输出:
local
enclosing
global解释:
inner()中的print(x):找到本地x = "local"outer()中的print(x):找到本地x = "enclosing"- 全局的
print(x):找到全局x = "global"
变量生命周期
本地变量:函数调用时创建,返回时销毁
def func():
x = 10
print(x)
func() # 10
func() # 10(重新创建 x)全局变量:模块加载时创建,程序结束时销毁
闭包变量:外层函数返回时不销毁(下一节详解)
常见陷阱
陷阱 1:循环变量作用域
for i in range(3):
pass
print(i) # 2(i 在循环外仍然可见)Python 的 for 循环不创建新作用域,循环变量泄漏到外层。
陷阱 2:列表推导中的变量
x = "global"
result = [x for x in range(3)]
print(x) # Python 3: "global"(不泄漏)Python 3 中列表推导有独立作用域,不影响外层变量。(Python 2 会泄漏)
陷阱 3:默认参数在定义时求值
def func(x, items=[]):
items.append(x)
return items
print(func(1)) # [1]
print(func(2)) # [1, 2],不是 [2]!默认值 [] 在函数定义时创建一次,所有调用共享。使用 None 作为默认值。
练习
预测输出:不运行代码,预测输出,然后验证:
pythonx = 10 def func(): x = 20 print(f"Inside func: {x}") func() print(f"Outside func: {x}")全局计数器:编写函数
increment_global()和reset_global(),分别递增和重置全局计数器counter。nonlocal 实践:修复以下代码,使
increment()能够修改外层的count:pythondef make_counter(): count = 0 def increment(): count = count + 1 return count return increment counter = make_counter() print(counter()) # 应输出 1 print(counter()) # 应输出 2LEGB 顺序:给定以下代码,解释每个
print(x)输出什么以及为什么:pythonx = "global" def outer(): x = "outer" def middle(): x = "middle" def inner(): print(x) # (1) inner() print(x) # (2) middle() print(x) # (3) outer() print(x) # (4)避免内置名字:以下代码有什么问题?如何修复?
pythondef calculate_length(list): sum = 0 for item in list: sum = sum + 1 return sum
完成练习后,打开本章答案页核对。
下一步
理解了作用域规则,下一节学习闭包:函数如何捕获外层变量,闭包的生命周期和实际应用。
回到第二章概览 · 返回Python 学习路线