课程
刚写了几行代码就看到 “name is not defined(名称未定义)” 或 “local variable referenced before assignment(在赋值前引用了局部变量)” 真让人挫败。代码看起来没问题,但 Python 并不认同。根因几乎总是作用域:某个名字在何处可见,以及此刻它指向哪个对象。
一旦理解了 Python 的名称查找——通常概括为 LEGB 规则——这些错误就会变得可预测且易于修复。本文将以循序渐进的方式讲解 Python 的变量与作用域。
什么是变量?
变量是绑定到对象的名字。在 Python 中,您使用赋值运算符 = 创建绑定。名称本身无类型;它们可以指向任何对象(字符串、整数、列表、函数等)。
customer_name = "Ava Torres"
order_count = 1
total_cents = 1 + 2 * 300 # evaluated first, then bound
命名变量时遵循以下规则与约定,以避免语法错误和隐蔽的 bug。
- 仅使用字母、数字和下划线;名称不能以数字开头。
- 不要使用
True、for或class等关键字作为名称。
- 避免遮蔽内置名,如
list、dict、sum或max。
- 为可读性优先使用
snake_case(PEP 8)。
这些示例演示了不应做的事情以及 Python 抛出的错误:
first string value = "First string" # spaces not allowed
# SyntaxError: invalid syntax
1st_value = 10 # cannot start with a digit
# SyntaxError: invalid decimal literal
True = "yes" # cannot assign to a keyword
# SyntaxError: cannot assign to True
Python 作用域如何工作(LEGB)
作用域是名称可见的代码区域。当您使用一个名称时,Python 按以下顺序查找(LEGB):
- Local:当前函数的作用域。
- Enclosing:任意外层函数的作用域(用于嵌套函数)。
- Global:模块的顶层(模块命名空间)。
- Built-in:Python 在
builtins中定义的名称(例如len、print)。
名称存在于命名空间中(可将其视为将名称映射到对象的字典)。作用域关乎 Python 在代码中的某一点为解析名称会查阅哪些命名空间。
局部作用域
在函数内赋值的名称,除非另有声明,都是该函数的局部名称。它们在函数外不可见。
def show_order_id():
order_id = 42
print("inside function:", order_id)
show_order_id()
print("outside function:", order_id) # NameError
Python 在编译时决定作用域。如果一个函数在其主体的任意位置对某个名称进行了赋值,那么该函数内对该名称的所有使用都会被视为局部——这会导致常见的 UnboundLocalError,当您在赋值前读取它时就会发生:
discount_rate = 0.10 # module-level (global)
def price_with_discount(amount_cents):
print("configured discount:", discount_rate) # looks local because of the assignment below
discount_rate = 0.20 # assignment makes 'discount_rate' local in this function
return int(amount_cents * (1 - discount_rate))
# UnboundLocalError: cannot access local variable 'discount_rate' where it is not associated with a value
若要在函数中使用模块级名称,要么避免对其赋值,要么将其标记为 global(下文介绍)。
外层作用域(闭包)
嵌套函数可以看到其直接外层函数中的名称。若要重新绑定这样的名称(而不只是读取),请声明为 nonlocal。
def make_step_counter():
count = 0 # enclosing scope for 'increment'
def increment():
nonlocal count # rebind the 'count' in the nearest enclosing function
count += 1
return count
return increment
step = make_step_counter()
print(step()) # 1
print(step()) # 2
没有 nonlocal 时,在 increment() 中对 count 赋值会创建一个新的局部名称,从而让外层的 count 保持不变。
全局作用域
在模块顶层赋值的名称属于该模块的全局命名空间。任何函数都可以读取它们。若要在函数内部为模块级名称赋值,请将其声明为 global。
greeting = "Hello"
def greet_city(city_name):
print(greeting, city_name) # reads global
def set_greeting(new_greeting):
global greeting
greeting = new_greeting # rebinds global
greet_city("Nairobi") # Hello Nairobi
set_greeting("Hi")
greet_city("Nairobi") # Hi Nairobi
谨慎使用 global。更可取的做法是通过参数传递值并返回结果,以保持代码的可测试性与可预测性。
内置作用域(以及关于关键字的一点说明)
内置作用域包含 len、print、Exception 等名称。避免遮蔽它们,否则您将在该作用域中失去对相应内置的访问。
list = [1, 2, 3] # shadows the built-in 'list' constructor
list("abc") # TypeError: 'list' object is not callable
del list # fix by deleting the shadowing name
关键字(例如 if、for、def)是 Python 语法的一部分,而非内置命名空间,永远不能作为标识符使用。
代码块、循环与推导式
Python 的代码块语句与推导式具有特定的作用域行为,这常让从其他语言转来的开发者感到意外。
if/for/while/with 没有块级作用域
这些代码块中的赋值会影响其包含作用域(函数或模块)。循环变量在循环结束后也依然存在。
if True:
status = "ready"
print(status) # "ready"
for i in range(3):
pass
print(i) # 2 (the last value from the loop)
推导式会隔离其迭代变量
列表、字典与集合推导式会为循环变量创建自己的局部作用域。迭代变量不会泄漏到外层作用域。
numbers = [1, 2, 3]
[x for x in numbers]
print("x" in globals() or "x" in locals()) # False
Python 3.12(PEP 709)为提速而内联了推导式,同时保留了这种隔离;您依然可以获得清晰且不泄漏的循环变量,并享受更快的执行。
使用 global 关键字
当您需要在函数中重新绑定一个模块级变量时,将该名称声明为 global。将声明放在函数顶部附近,以便清楚地表明您要修改哪个变量。
tax_rate = 0.08
def configure_tax(rate):
global tax_rate
tax_rate = float(rate)
def total_with_tax(cents):
return int(cents * (1 + tax_rate))
configure_tax(0.10)
print(total_with_tax(1000)) # 1100
使用 nonlocal 关键字
使用 nonlocal 来重新绑定最近外层函数作用域中的名称。这在维护状态的闭包中很常见。
def make_accumulator(start=0):
total = start
def add(amount):
nonlocal total
total += amount
return total
return add
acc = make_accumulator()
print(acc(5)) # 5
print(acc(10)) # 15
对未在外层函数中定义的名称使用 nonlocal 会导致 SyntaxError。如果该名称是全局的,请改用 global。
locals() 与 globals() 在 2025
这些函数适用于检查与调试,而非用于更新变量。从 Python 3.13(PEP 667)开始,每次在函数中调用 locals() 都会返回一个独立的快照。编辑该快照不会改变真实的局部变量。我在 Python 3.13 上运行以下代码片段验证了这一行为:
def probe():
project = "alpha"
snap1 = locals()
snap1["project"] = "beta" # edits the snapshot only
observed = project # still "alpha"
snap2 = locals() # new snapshot
return snap1["project"], observed, snap2["project"]
print(probe()) # ('beta', 'alpha', 'alpha')
以类似方式使用 globals() 来访问模块命名空间。在编写应用代码时,优先使用显式的参数与返回值,而非动态查找。
常见陷阱与修复方法
以下失误占了我在实践中见到的大多数作用域相关错误。
UnboundLocalError源于在函数内赋值: 只要在函数内任意位置对名称赋值,Python 就将其视为局部名。要么将读取放到赋值之后、要么改名、要么按需添加 global/nonlocal。
- 误以为存在块级作用域: 在 if/for/while/with 中赋值的名称会在包含作用域中继续存在。使用更小的辅助函数来收紧作用域。
- 遮蔽内置名: 避免使用 list、dict、sum、id 或 min 等名称。选择更具描述性的名称,如
customer_list或min_allowed。
- 在闭包中忘记使用
nonlocal: 如果您打算更新外层函数的变量,请将其声明为 nonlocal。否则会创建一个新的局部变量,外层值不会改变。
- 混淆关键字与内置名: 关键字(语法)永远不能用作名称;内置名虽然可以被遮蔽,但不应如此做。
让作用域更简单的最佳实践
这些习惯能让代码更易读、易测、易调试。
- 倾向于小而聚焦的函数。在尽可能小的作用域中定义变量。
- 通过参数与返回值传入传出数据。尽量减少模块级状态。
- 有意识地使用闭包。记录嵌套函数捕获了哪些外部名称,确需重新绑定时使用 nonlocal。
- 选用具描述性且不冲突的名称。以单下划线开头(例如
_cache)表示内部使用。
- 将 locals()/globals() 视为只读诊断工具,而非配置机制。
结语
Python 通过按 Local → Enclosing → Global → Built-in 的顺序解析名称。理解这一顺序——以及何时使用 global 或 nonlocal——能消除 NameError 与 UnboundLocalError 等常见错误。让变量处于尽可能小的作用域,避免遮蔽内置名,并有意识地使用闭包。掌握这一心智模型后,您的函数将行为可预测,作用域不再带来意外。
