在 Python 中,变量的作用域(Scope) 决定了我们可以在程序的哪个部分访问和操作哪个变量。不理解作用域可能会导致令人困惑的 UnboundLocalError 或意外的行为。Python 使用一种称为 LEGB 的规则来解析变量名。这篇博文将深入探讨 LEGB 规则,并通过大量案例帮助你彻底掌握它。

什么是 LEGB?

LEGB 是一个缩写词,代表了 Python 解释器在查找变量定义时所遵循的四个层次顺序:

  1. L - Local (局部作用域)

  2. E - Enclosing (闭包函数外的函数作用域)

  3. G - Global (全局作用域)

  4. B - Built-in (内建作用域)

解释器会按照 L -> E -> G -> B 的顺序由内向外地查找变量。如果在某个层次找到了变量,就停止搜索;如果直到最外层都没找到,则会抛出 NameError 异常。

下面我们来逐一分解这四个作用域。


1. Local (局部作用域 | 当前函数内部)

局部作用域指的是在函数内部定义的变量。这些变量只能在定义它们的函数内部被访问。当函数执行结束后,其内部的局部变量通常会被销毁。

案例 1:简单的局部变量

python

def greet():
    message = "Hello, World!"  # message 是局部变量
    print(message)

greet()       # 输出: Hello, World!
print(message) # 报错: NameError: name 'message' is not defined

在上面的例子中,message 是在 greet 函数内部定义的,因此它是局部变量,只能在函数内部使用。


2. Enclosing (闭包函数外的函数作用域 | 嵌套函数)

当一个函数(嵌套函数)被定义在另一个函数(外层函数)的内部时,就产生了闭包作用域。嵌套函数可以访问其外层函数的变量。

案例 2:理解 Enclosing 作用域

python

def outer_func():
    enclosing_var = "I'm from outer function"  # 外层函数变量 (Enclosing)

    def inner_func():
        # inner_func 可以访问 outer_func 的变量
        print("Inner function says:", enclosing_var) 

    inner_func()

outer_func() # 输出: Inner function says: I'm from outer function

案例 3:经典的闭包(Closure)

python

def make_multiplier(x): # x 对于 inner_func 是 Enclosing 变量
    def inner_func(y):
        return x * y   # 内部函数记住了外部函数的变量 x
    return inner_func

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5)) # 输出: 10 (2 * 5)
print(triple(5)) # 输出: 15 (3 * 5)

inner_func 可以访问其 Enclosing 作用域(即 make_multiplier 函数)中的变量 x,即使 make_multiplier 已经执行完毕。这就是闭包的强大之处。


3. Global (全局作用域 | 模块级别)

在 Python 模块(一个 .py 文件)的顶层定义的变量是全局变量。它们在整个模块中都是可访问的,包括模块内的所有函数。

案例 4:访问全局变量

python

global_var = "I'm a global variable"  # 全局变量

def my_function():
    # 函数内部可以读取全局变量
    print("Inside function:", global_var) 

my_function() # 输出: Inside function: I'm a global variable
print("Outside function:", global_var) # 输出: Outside function: I'm a global variable
关键点:global 关键字

在函数内部,如果你想要修改全局变量(而不是创建一个新的同名局部变量),你必须使用 global 关键字进行声明。

案例 5:修改全局变量(需要 global

python

count = 0

def increment():
    global count  # 声明我们要修改的是全局变量 count
    count += 1
    print("Count inside function:", count)

increment() # 输出: Count inside function: 1
increment() # 输出: Count inside function: 2
print("Count outside function:", count) # 输出: Count outside function: 2

案例 6:不使用 global 的后果

python

count = 0

def increment_wrong():
    # 没有使用 global 关键字,Python 认为 count 是一个新的局部变量
    count = count + 1  # 在执行赋值操作前,局部变量 count 还未被定义,因此这里会报错
    print(count)

increment_wrong() # 报错: UnboundLocalError: local variable 'count' referenced before assignment

4. Built-in (内建作用域 | Python 内置模块)

这是最外层的作用域,包含了 Python 的内置函数和异常,如 print()len()listValueError 等。我们可以在任何地方直接使用它们,而无需导入任何模块。

案例 7:使用内建函数

python

# len 是内建作用域中的函数
my_list = [1, 2, 3]
length = len(my_list) # 这里查找 len,最终在 Built-in 作用域找到
print(length) # 输出: 3

注意:虽然很少见,但你也可以覆盖内建函数(强烈不推荐!)。

python

# 警告:不要在实际项目中这样做!
def len(x): # 这里我们用一个全局的 len 覆盖了内建的 len
    return 999

my_list = [1, 2, 3]
print(len(my_list)) # 输出: 999 (现在使用的是我们自定义的 len)

# 要恢复使用内建函数,可以删除这个自定义版本
del len
print(len(my_list)) # 输出: 3 (现在使用的是内建的 len)

综合案例与 LEGB 查找顺序

让我们看一个综合案例,清晰地展示 LEGB 的查找顺序。

案例 8:完整的 LEGB 演示

python

x = "global x"  # Global

def outer():
    x = "enclosing x"  # Enclosing (对于 inner 来说)

    def inner():
        x = "local x"  # Local
        print(x)        # 首先查找 Local,找到了 "local x"

    inner()
    print(x)            # 在 outer 的作用域,找到了自己的 x "enclosing x"

outer()
print(x)                # 在全局作用域,找到了 "global x"
# 输出:
# local x
# enclosing x
# global x

案例 9:当内层没有定义时,向外查找

python

x = "global x"

def outer():
    x = "enclosing x"

    def inner():
        # inner 内部没有定义 x,所以向上查找 Enclosing 作用域
        print(x) # 输出: enclosing x

    inner()

outer()

案例 10:nonlocal 关键字

与 global 类似,nonlocal 关键字用于在嵌套函数中修改外层(非全局)函数的变量。

python

def outer():
    count = 0
    print("Outer count before:", count) # 输出: 0

    def inner():
        nonlocal count # 声明我们要修改的是外层(Enclosing)的 count,而不是全局的
        count += 1
        print("Inner count:", count) 

    inner()
    print("Outer count after:", count) # 输出: 1

outer()
# 如果没有 nonlocal count 这一行,代码会报 UnboundLocalError

总结与要点

作用域 定义位置 关键字
Local (L) 函数内部 -
Enclosing (E) 嵌套函数的外层函数 nonlocal
Global (G) 模块顶层 global
Built-in (B) Python 内置模块 -
  1. 查找顺序: LEGB (Local -> Enclosing -> Global -> Built-in)。

  2. 赋值操作即定义:在函数内部对变量进行赋值(如 x = 10),Python 会默认将其视为创建新的局部变量,除非使用了 global 或 nonlocal 关键字明确声明。

  3. 只读不写:如果只是引用(读取)变量,Python 可以顺利地按照 LEGB 规则向外查找。但若要修改,则需要关键字声明。

  4. 避免命名冲突:良好的编程习惯是避免使用与全局变量或内置函数同名的局部变量,这可以提高代码的可读性并减少错误。

理解 LEGB 规则是成为一名合格 Python 程序员的关键一步,它能帮助你更好地组织代码、理解闭包和装饰器,并有效地调试变量作用域相关的问题。

更多推荐