Python函数中的变量作用域详解

Python 变量作用域

目录

  1. 变量作用域的基本概念
  2. 局部变量 (Local Variables)
  3. 全局变量 (Global Variables)
  4. global 关键字
  5. 嵌套函数 (Nested Functions)
  6. nonlocal 关键字
  7. LEGB 规则
  8. 作用域冲突与优先级
  9. 内置函数冲突
  10. 最佳实践

变量作用域的基本概念

在 Python 中,变量作用域指的是变量在程序中可被访问的范围。了解作用域对于编写可维护、无 bug 的代码至关重要。Python 中主要有四种作用域:

  1. 局部作用域 (Local) – 函数内部定义的变量
  2. 嵌套作用域 (Enclosing) – 外部函数中定义的变量(对于嵌套函数)
  3. 全局作用域 (Global) – 模块级别定义的变量
  4. 内置作用域 (Built-in) – Python 内置的名称

局部变量 (Local Variables)

局部变量是在函数内部定义的变量,它们只能在该函数内部访问。

def my_function():
    x = 10  # 局部变量
    print(f"局部变量 x 的值是: {x}")

my_function()  # 输出: 局部变量 x 的值是: 10

# 尝试在函数外部访问局部变量会导致错误
try:
    print(x)
except NameError as e:
    print(f"错误: {e}")  # 输出: 错误: name 'x' is not defined

局部变量的特点

  • 在函数被调用时创建,在函数执行结束时销毁
  • 不同函数中的同名局部变量互不影响
  • 函数的参数也是局部变量
  • def func1():
        y = 20
        print(f"func1 中的 y: {y}")
    
    def func2():
        y = 30
        print(f"func2 中的 y: {y}")
    
    func1()  # 输出: func1 中的 y: 20
    func2()  # 输出: func2 中的 y: 30
    

    全局变量 (Global Variables)

    全局变量是在函数外部(模块级别)定义的变量,可以在整个模块中访问。

    # 全局变量
    global_var = 100
    
    def print_global():
        print(f"函数内部访问全局变量: {global_var}")
    
    print(f"全局作用域中的全局变量: {global_var}")  # 输出: 全局作用域中的全局变量: 100
    print_global()  # 输出: 函数内部访问全局变量: 100
    

    全局变量的特点

  • 在模块被导入时创建,程序结束时销毁
  • 可以在模块内的任何地方被读取(不使用特殊关键字)
  • 在函数内部不能直接修改全局变量(除非使用 global 关键字)
  • counter = 0
    
    def increment():
        # 尝试直接修改全局变量(这将创建新的局部变量)
        counter = counter + 1  # UnboundLocalError
        print(f"计数器: {counter}")
    
    try:
        increment()
    except UnboundLocalError as e:
        print(f"错误: {e}")  
        # 输出: 错误: local variable 'counter' referenced before assignment
    

    global 关键字

    global 关键字用于在函数内部声明一个变量是全局变量,允许我们在函数内部修改全局变量。

    counter = 0
    
    def increment():
        global counter  # 声明 counter 是全局变量
        counter = counter + 1
        print(f"计数器: {counter}")
    
    increment()  # 输出: 计数器: 1
    increment()  # 输出: 计数器: 2
    print(f"全局计数器: {counter}")  # 输出: 全局计数器: 2
    

    使用 global 的注意事项

    1. 如果只需要读取全局变量,不需要使用 global 关键字
    2. 使用 global 后,该名称将在整个函数中被视为全局变量
    3. global 声明必须在变量使用前出现
    x = 5
    
    def example():
        # 只读取全局变量,不需要 global
        print(f"全局 x 的值: {x}")  # 输出: 全局 x 的值: 5
        
    def bad_example():
        print(x)  # 这会引发错误,因为后面 x 被当作局部变量
        x = 10    # 这定义了一个局部变量 x
        
    def good_example():
        global x  # 声明 x 是全局变量
        print(f"修改前 x 的值: {x}")  # 输出: 修改前 x 的值: 5
        x = 10    # 修改全局变量
        print(f"修改后 x 的值: {x}")  # 输出: 修改后 x 的值: 10
        
    example()
    try:
        bad_example()
    except UnboundLocalError as e:
        print(f"错误: {e}")
        # 输出: 错误: local variable 'x' referenced before assignment
    good_example()
    print(f"函数外部 x 的值: {x}")  # 输出: 函数外部 x 的值: 10
    

    嵌套函数 (Nested Functions)

    嵌套函数是在另一个函数内部定义的函数。在 Python 中,嵌套函数可以访问其外部函数中定义的变量。

    def outer_function():
        outer_var = "I'm in the outer function"
        
        def inner_function():
            print(outer_var)  # 可以访问外部函数的变量
        
        inner_function()  # 输出: I'm in the outer function
    
    outer_function()
    

    嵌套函数的特点

  • 内部函数可以读取外部函数中定义的变量
  • 默认情况下,内部函数不能修改外部函数中定义的变量(创建同名局部变量)
  • 可以使用 nonlocal 关键字允许内部函数修改外部函数中定义的变量
  • def counter_function():
        count = 0
        
        def increment():
            # 尝试修改外部函数的变量
            count = count + 1  # UnboundLocalError
            return count
        
        return increment
    
    try:
        counter = counter_function()
        print(counter())
    except UnboundLocalError as e:
        print(f"错误: {e}")
        # 输出: 错误: local variable 'count' referenced before assignment
    

    nonlocal 关键字

    nonlocal 关键字用于在嵌套函数中声明一个变量来自于外部函数的作用域,允许内部函数修改外部函数中定义的变量。

    def counter_function():
        count = 0
        
        def increment():
            nonlocal count  # 声明 count 是从外部函数作用域获取的
            count = count + 1
            return count
        
        return increment
    
    counter = counter_function()
    print(counter())  # 输出: 1
    print(counter())  # 输出: 2
    print(counter())  # 输出: 3
    

    使用 nonlocal 的注意事项

    1. nonlocal 只能引用外部函数的变量,不能引用全局变量
    2. 被引用的变量必须存在于某个外部函数的作用域中
    3. nonlocal 声明必须在变量使用前出现
    def outer():
        x = "outer"
        
        def inner1():
            nonlocal x
            x = "modified by inner1"
        
        def inner2():
            x = "local to inner2"  # 创建局部变量,不影响外部
        
        print(f"开始时 x = {x}")  # 输出: 开始时 x = outer
        inner1()
        print(f"调用 inner1 后 x = {x}")  # 输出: 调用 inner1 后 x = modified by inner1
        inner2()
        print(f"调用 inner2 后 x = {x}")  # 输出: 调用 inner2 后 x = modified by inner1
    
    outer()
    

    LEGB 规则

    Python 在解析变量名时遵循 LEGB 规则,按以下顺序查找变量:

    1. Local(局部)- 函数内部
    2. Enclosing(嵌套)- 外部函数中
    3. Global(全局)- 模块级别
    4. Built-in(内置)- Python的内置命名空间

    这个规则决定了在访问变量时 Python 如何确定使用哪个变量。

    x = "全局变量"  # Global
    
    def outer():
        x = "外部函数变量"  # Enclosing
        
        def inner():
            x = "局部变量"  # Local
            print(f"inner: {x}")
        
        inner()  # 输出: inner: 局部变量
        print(f"outer: {x}")
    
    outer()  # 输出: outer: 外部函数变量
    print(f"global: {x}")  # 输出: global: 全局变量
    

    下面的示例,展示了 LEGB 规则:

    # Built-in
    # len 是内置函数
    
    # Global
    x = "global x"
    
    def outer():
        # Enclosing
        x = "enclosing x"
        
        def inner():
            # Local
            # x = "local x"  # 如果取消注释这行,会打印 "local x"
            print(f"x: {x}")
        
        inner()
    
    # 根据LEGB规则
    # 如果inner函数中有局部变量x,使用local x
    # 否则,如果outer函数中有变量x,使用enclosing x
    # 否则,如果全局作用域有变量x,使用global x
    # 否则,查找内置作用域
    outer()  # 输出: x: enclosing x
    

    作用域冲突与优先级

    当不同作用域中存在同名变量时,Python 按照 LEGB 规则确定优先级:

    1. 局部变量优先于外部函数变量
    2. 外部函数变量优先于全局变量
    3. 全局变量优先于内置名称

    局部作用域与全局作用域的冲突

    value = 100  # 全局变量
    
    def test_scope():
        value = 200  # 局部变量
        print(f"函数内部 value: {value}")  # 使用局部变量
    
    test_scope()  # 输出: 函数内部 value: 200
    print(f"函数外部 value: {value}")  # 输出: 函数外部 value: 100
    

    使用 global 可以改变这一默认行为:

    value = 100  # 全局变量
    
    def test_global():
        global value  # 声明使用全局变量
        print(f"修改前 value: {value}")  # 输出: 修改前 value: 100
        value = 200  # 修改全局变量
        print(f"修改后 value: {value}")  # 输出: 修改后 value: 200
    
    test_global()
    print(f"函数调用后 value: {value}")  # 输出: 函数调用后 value: 200
    

    局部作用域与外部函数作用域的冲突

    def outer_func():
        value = 100  # 外部函数变量
        
        def inner_func():
            value = 200  # 局部变量
            print(f"内部函数 value: {value}")  # 使用局部变量
        
        inner_func()  # 输出: 内部函数 value: 200
        print(f"外部函数 value: {value}")  # 输出: 外部函数 value: 100
    
    outer_func()
    

    使用 nonlocal 可以改变这一默认行为:

    def outer_func():
        value = 100  # 外部函数变量
        
        def inner_func():
            nonlocal value  # 声明使用外部函数变量
            print(f"修改前 value: {value}")  # 输出: 修改前 value: 100
            value = 200  # 修改外部函数变量
            print(f"修改后 value: {value}")  # 输出: 修改后 value: 200
        
        inner_func()
        print(f"外部函数 value: {value}")  # 输出: 外部函数 value: 200
    
    outer_func()
    

    多层嵌套函数中的作用域

    def level1():
        x = 1
        
        def level2():
            x = 2
            
            def level3():
                nonlocal x  # 这会引用 level2 中的 x
                x = 3
                print(f"level3 x: {x}")
            
            level3()  # 输出: level3 x: 3
            print(f"level2 x: {x}")  # 输出: level2 x: 3(被 level3 修改)
        
        level2()
        print(f"level1 x: {x}")  # 输出: level1 x: 1(不受影响)
    
    level1()
    

    内置函数冲突

    当我们创建与 Python 内置函数同名的变量时,会覆盖内置函数。这是 LEGB 规则中"B"(Built-in)的体现:内置名称的优先级最低。

    # 正常使用内置函数 len
    print(len([1, 2, 3]))  # 输出: 3
    
    # 创建与内置函数同名的变量
    len = "这不再是一个函数"
    
    try:
        print(len([1, 2, 3]))  # TypeError: 'str' object is not callable
    except TypeError as e:
        print(f"错误: {e}")
        # 输出: 错误: 'str' object is not callable
    
    # 恢复内置函数
    del len
    print(len([1, 2, 3]))  # 输出: 3
    

    其他常见的被覆盖的内置函数示例:

    # 覆盖 max 函数
    max = 100
    try:
        largest = max(1, 2, 3)  # TypeError
    except TypeError as e:
        print(f"错误: {e}")
        # 输出: 错误: 'int' object is not callable
    
    # 覆盖 list 函数
    list = [1, 2, 3]
    try:
        new_list = list("abc")  # TypeError
    except TypeError as e:
        print(f"错误: {e}")
        # 输出: 错误: 'list' object is not callable
    

    警告:不要使用与内置函数同名的变量名,这会导致内置函数无法使用,并可能引发难以调试的错误。

    最佳实践

    1. 避免使用全局变量

    2. 全局变量使代码难以理解和维护
    3. 尽量通过参数传递和返回值来共享数据
    4. 减少使用 globalnonlocal 关键字

    5. 过度使用会使代码难以追踪和理解
    6. 考虑使用类来管理状态,而不是依赖嵌套函数和变量
    7. 避免使用与内置函数同名的变量

    8. 避免使用 listdictsummax 等作为变量名
    9. 可以使用类似 list_dict_ 的命名方式,或更具描述性的名称
    10. 利用闭包特性

    11. 嵌套函数可以记住并访问其外部作用域中的变量,这称为闭包
    12. 闭包可以用于创建拥有私有状态的函数
    def create_counter(start=0):
        """创建一个计数器函数"""
        
        def counter():
            nonlocal start
            start += 1
            return start
        
        return counter
    
    # 使用闭包创建两个独立的计数器
    counter1 = create_counter(10)
    counter2 = create_counter(100)
    
    print(counter1())  # 11
    print(counter1())  # 12
    print(counter2())  # 101
    print(counter1())  # 13
    
    1. 在复杂情况下使用类替代嵌套函数
    class Counter:
        def __init__(self, start=0):
            self.value = start
        
        def increment(self):
            self.value += 1
            return self.value
        
        def reset(self):
            self.value = 0
            return self
    
    # 使用类创建计数器
    counter1 = Counter(10)
    counter2 = Counter(100)
    
    print(counter1.increment())  # 11
    print(counter1.increment())  # 12
    print(counter2.increment())  # 101
    

    总结

    1. 局部变量在函数内部定义,只在函数内部可见
    2. 全局变量在模块级别定义,可以在整个模块中访问
    3. global 关键字允许在函数内部修改全局变量
    4. 嵌套函数可以访问外部函数中定义的变量
    5. nonlocal 关键字允许在嵌套函数中修改外部函数中定义的变量
    6. LEGB 规则定义了 Python 查找变量的顺序:Local → Enclosing → Global → Built-in
    7. 作用域冲突时,优先级按 LEGB 顺序递减,可以使用 globalnonlocal 改变默认行为
    8. 定义与内置函数同名的变量会覆盖内置函数,应当避免

    作者:想知道哇

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python函数中的变量作用域详解

    发表回复