ZetCode

Python 变量作用域

最后修改于 2025 年 4 月 2 日

变量作用域决定了在程序中的哪个位置可以访问一个变量。Python 使用 LEGB(Local, Enclosing, Global, Built-in)规则来进行名称解析。理解作用域对于编写无错误代码至关重要。本指南通过实际示例涵盖了所有作用域类型。正确的作用域管理可以防止命名冲突和意外行为。

LEGB 规则概述

Python 按照 LEGB 顺序(局部、嵌套、全局、内置)来解析变量名。当引用一个变量时,Python 会按顺序搜索这些作用域。本示例演示了每个作用域级别。理解 LEGB 是 Python 编程的基础。

legb.py
# Global scope
global_var = "I'm global"

def outer_function():
    # Enclosing scope
    enclosing_var = "I'm enclosing"
    
    def inner_function():
        # Local scope
        local_var = "I'm local"
        print(local_var)        # Local
        print(enclosing_var)    # Enclosing
        print(global_var)       # Global
        print(len)             # Built-in (len function)
    
    inner_function()

outer_function()

# Additional example: LEGB in action
x = "global"

def test():
    x = "enclosing"
    
    def inner():
        x = "local"
        print(x)  # What gets printed?
    
    inner()

test()  # Prints "local"

该示例展示了所有四个 LEGB 作用域。`inner_function` 首先在其局部作用域中找到 `local_var`。`enclosing_var` 来自嵌套的 `outer_function` 作用域。`global_var` 在模块级别的全局作用域中找到。`len` 解析为内置函数。

附加示例演示了 LEGB 的优先级——最内层作用域的变量具有优先权。当执行 `print(x)` 时,Python 在检查嵌套、全局或内置作用域之前会找到“local”。这种层次结构适用于 Python 中的所有名称查找。

全局作用域

全局作用域变量在模块级别定义,并在整个模块中可访问。函数可以读取全局变量,但需要 `global` 关键字才能修改它们。本示例展示了正确的全局变量用法。理解全局变量有助于管理模块范围的状态。

global_scope.py
# Global variable
counter = 0

def increment():
    global counter  # Declare we're using the global counter
    counter += 1

def show_counter():
    print(f"Counter: {counter}")  # Reading global doesn't need declaration

increment()
show_counter()  # Counter: 1
increment()
show_counter()  # Counter: 2

# Additional example: Shadowing
value = 10

def demo():
    value = 20  # Creates a local variable, doesn't modify global
    print(f"Local value: {value}")

demo()  # Local value: 20
print(f"Global value: {value}")  # Global value: 10

`counter` 变量是全局的,在函数之间共享。`increment` 使用 `global` 修改它,而 `show_counter` 只读取它。没有 `global` 声明,Python 会创建一个局部变量。

遮蔽示例表明,在函数内部赋值给一个名称默认会创建一个局部变量,即使存在一个同名的全局变量。全局变量保持不变。这种行为可以防止意外修改全局变量。

局部作用域

局部作用域变量在函数内部定义,并且只能在该函数内部访问。它们仅在函数执行期间存在。本示例演示了局部变量的行为。正确使用局部变量可以促进封装并防止命名冲突。

local_scope.py
def calculate():
    # Local variables
    x = 10
    y = 20
    result = x + y
    print(f"Inside function: {result}")

calculate()
# print(x)  # Would raise NameError: x is not defined

# Additional example: Separate local scopes
def func1():
    var = "func1 variable"
    print(var)

def func2():
    var = "func2 variable"
    print(var)

func1()  # func1 variable
func2()  # func2 variable

# Additional example: Temporary variables
def process_data(data):
    temp = data * 2  # Local temporary variable
    return temp.upper()

print(process_data("test"))  # TESTTEST

变量 `x`、`y` 和 `result` 只存在于 `calculate` 函数内部。尝试在外部访问它们会引发 `NameError`。每次函数调用都会创建新的局部变量,即使是递归调用。

函数具有独立的局部作用域——`func1` 和 `func2` 都可以使用 `var` 而不会发生冲突。局部变量非常适合临时计算,例如 `process_data` 中的 `temp`,它在函数结束后消失。

嵌套(非局部)作用域

嵌套作用域是指嵌套函数外部作用域中的变量。`nonlocal` 关键字允许修改这些变量。本示例演示了嵌套函数的作用域。理解嵌套作用域对于闭包和装饰器至关重要。

nonlocal_scope.py
def outer():
    message = "Original"  # Enclosing scope variable
    
    def inner():
        nonlocal message  # Declare we're using the enclosing message
        message = "Modified"
        print(f"Inner: {message}")
    
    print(f"Before inner: {message}")
    inner()
    print(f"After inner: {message}")

outer()

# Additional example: Multiple levels
def level1():
    x = 1
    
    def level2():
        x = 2
        
        def level3():
            nonlocal x  # Refers to level2's x
            x = 3
        
        level3()
        print(f"Level2 x: {x}")  # Shows modified value
    
    level2()
    print(f"Level1 x: {x}")  # Original unchanged

level1()

# Additional example: Without nonlocal
def counter():
    count = 0
    
    def increment():
        # count += 1  # Would raise UnboundLocalError
        nonlocal count
        count += 1
        return count
    
    return increment

c = counter()
print(c(), c(), c())  # 1 2 3

`outer`/`inner` 示例展示了 `nonlocal` 如何允许 `inner` 修改嵌套作用域中的 `message`。如果没有它,Python 会创建一个新的局部变量。

`nonlocal` 搜索的是最近的嵌套作用域,而不是全局作用域。计数器示例演示了一个常见的闭包模式,其中嵌套函数维护状态。这种技术对于创建函数工厂非常强大。

内置作用域

内置作用域包含 Python 的内置函数和异常(如 `len`、`range`、`ValueError`)。这些名称始终可用。本示例展示了内置作用域的交互。理解内置项可以防止意外的遮蔽。

builtin_scope.py
# Built-in functions
print(len("Python"))  # 6
print(max(1, 5, 2))  # 5

# Shadowing built-ins (generally discouraged)
def test():
    len = 10  # Shadows built-in len locally
    print(len)  # 10

test()
print(len("test"))  # Still works globally

# Additional example: Restoring access
def custom_len():
    # Can still access built-in if needed
    orig_len = __builtins__.len
    print(orig_len("hello"))  # 5

custom_len()

# Additional example: Common shadowing mistakes
def calculate(input):
    sum = 0  # Shadows built-in sum()
    for num in input:
        sum += num
    # return sum(input)  # Would error
    return sum

print(calculate([1, 2, 3]))  # 6

内置项在任何地方都可用,除非被遮蔽。`test` 函数演示了局部变量如何在函数作用域内遮蔽内置名称,但在全局范围内,内置项仍然可访问。

遮蔽 `sum`、`list` 或 `dict` 等内置项很常见,但可能导致令人困惑的错误。`__builtins__` 模块在需要时提供对原始内置项的访问。选择描述性名称以避免意外遮蔽。

类作用域

类作用域是类属性和方法的特殊命名空间。实例变量通过 `self` 访问。本示例演示了类和实例的作用域。正确的作用域管理对于面向对象的 Python 至关重要。

class_scope.py
class MyClass:
    # Class variable
    class_var = "I'm a class variable"
    
    def __init__(self, instance_var):
        # Instance variable
        self.instance_var = instance_var
    
    def show_vars(self):
        print(f"Class var: {self.class_var}")
        print(f"Instance var: {self.instance_var}")
        # Local variable
        temp = "local value"
        print(f"Local var: {temp}")

obj1 = MyClass("Instance 1")
obj2 = MyClass("Instance 2")

obj1.show_vars()
obj2.show_vars()

# Modifying class variable affects all instances
MyClass.class_var = "Modified class var"
obj1.show_vars()

# Additional example: Name resolution in methods
x = "global x"

class Test:
    x = "class x"
    
    def method(self):
        x = "method x"
        print(x)          # method x
        print(self.x)     # class x
        print(globals()['x'])  # global x

Test().method()

`class_var` 由所有实例共享,而 `instance_var` 对每个对象都是唯一的。`show_vars` 方法演示了如何访问两者,以及其局部变量 `temp`。类变量可以通过类或实例访问。

方法变量遵循 LEGB 规则,实例命名空间(通过 `self` 访问)作为一个额外的作用域。`Test` 类展示了在名称重叠时如何访问不同作用域级别的变量。类作用域在 LEGB 层次结构中介于全局和局部之间。

推导式和 Lambda 中的作用域

推导式和 lambda 有自己不同于常规函数的作用域规则。本示例演示了这些特殊情况。理解这些细微差别可以防止函数式 Python 中的意外行为。

comprehensions.py
x = 10

# List comprehension has its own scope
squares = [x**2 for x in range(5)]
print(squares)  # [0, 1, 4, 9, 16]
print(x)       # 10 (unchanged)

# Lambda expressions
y = 20
adder = lambda z: y + z  # Lambda can access enclosing scope
print(adder(5))         # 25

# Additional example: Generator expression
multiplier = 3
gen = (x * multiplier for x in range(5))
print(list(gen))  # [0, 3, 6, 9, 12]

# Additional example: Dictionary comprehension
keys = ['a', 'b', 'c']
values = [1, 2, 3]
d = {k: v for k, v in zip(keys, values)}  # New scope for k, v
print(d)  # {'a': 1, 'b': 2, 'c': 3}

与 Python 2.7 不同,列表推导式不会将其迭代变量(示例中的 `x`)泄漏到外部作用域。Lambda 可以访问外部作用域中的变量,如 `adder` 函数中的 `y` 所示。

生成器表达式在作用域方面与列表推导式类似。字典推导式同样保护其迭代变量。这些函数式结构在允许访问所需的外部变量的同时,保持了清晰的作用域隔离。

作用域最佳实践

优先使用局部变量而不是全局变量,以尽量减少副作用。谨慎使用 `global` 和 `nonlocal`,并在使用时进行文档记录。避免遮蔽内置名称以防止混淆。保持函数小巧且专注,以管理作用域的复杂性。使用描述性名称来减少跨作用域的命名冲突。

资料来源

从以下资源了解更多信息:Python 作用域文档Real Python LEGB 指南内置函数

作者

我叫 Jan Bodnar,是一位充满热情的程序员,拥有丰富的编程经验。自 2007 年以来,我一直撰写编程文章。迄今为止,我已撰写了 1,400 多篇文章和 8 本电子书。我在教授编程方面拥有十多年的经验。

列出所有 Python 教程