Skip to content

作用域与 LEGB:函数如何找到名字 ​

核心概念 ​

作用域(scope)决定了名字(变量)在哪里可见、如何查找。Python 使用 LEGB 规则查找名字:

  • Local:本地作用域(函数内部)
  • Enclosing:外层函数作用域(嵌套函数)
  • Global:全局作用域(模块级别)
  • Built-in:内置作用域(Python 内置名字)

查找顺序:L → E → G → B,找到第一个匹配就停止。

本地作用域(Local) ​

函数内部定义的名字是本地的:

python
def func():
    x = 10  # 本地变量
    print(x)

func()  # 10
print(x)  # NameError: name 'x' is not defined

函数参数也是本地变量:

python
def greet(name):  # name 是本地变量
    message = f"Hello, {name}"
    print(message)

greet("Alice")  # Hello, Alice
print(name)  # NameError

全局作用域(Global) ​

模块顶层定义的名字是全局的:

python
x = 100  # 全局变量

def func():
    print(x)  # 读取全局变量

func()  # 100

全局变量的读与写 ​

读取全局变量:直接使用

python
counter = 0

def show_counter():
    print(counter)  # 读取全局 counter

show_counter()  # 0

修改全局变量:需要 global 关键字

python
counter = 0

def increment():
    counter = counter + 1  # 错误!

increment()  # UnboundLocalError

问题分析:

  • counter = ... 使 Python 认为 counter 是本地变量
  • 右侧 counter + 1 试图读取本地 counter,但它尚未赋值
  • 产生 UnboundLocalError

正确写法:

python
counter = 0

def increment():
    global counter  # 声明使用全局 counter
    counter = counter + 1

increment()
print(counter)  # 1

global 关键字告诉 Python:counter 指向全局作用域的变量,不创建本地变量。

何时使用全局变量 ​

常见场景:

  • 模块级常量(通常大写)
  • 配置参数
  • 简单脚本中的共享状态

最佳实践:

  • 尽量避免修改全局变量(使用参数和返回值)
  • 只读全局常量不需要 global 关键字
  • 复杂程序用类或闭包管理状态

外层作用域(Enclosing) ​

嵌套函数可以访问外层函数的变量:

python
def outer():
    x = "outer"
    
    def inner():
        print(x)  # 访问外层函数的 x
    
    inner()

outer()  # outer

多层嵌套 ​

python
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:

python
def outer():
    count = 0
    
    def inner():
        count = count + 1  # 错误!UnboundLocalError
        print(count)
    
    inner()

outer()  # UnboundLocalError

正确写法:

python
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: 2

nonlocal 告诉 Python:count 指向外层作用域的变量,不创建本地变量。

nonlocal vs global:

  • nonlocal:指向最近的外层函数作用域
  • global:指向模块级全局作用域

内置作用域(Built-in) ​

Python 内置名字(如 len, print, int)在内置作用域:

python
def func():
    print(len([1, 2, 3]))  # len 是内置函数

func()  # 3

可以覆盖内置名字(不推荐):

python
def func():
    len = 10  # 覆盖内置 len
    print(len)  # 10
    # print(len([1, 2, 3]))  # TypeError: 'int' object is not callable

func()

全局覆盖:

python
len = "not a function"
print(len([1, 2, 3]))  # TypeError

最佳实践:避免使用内置名字作为变量名。

LEGB 实战示例 ​

python
x = "global"

def outer():
    x = "enclosing"
    
    def inner():
        x = "local"
        print(x)
    
    inner()
    print(x)

outer()
print(x)

输出:

text
local
enclosing
global

解释:

  • inner() 中的 print(x):找到本地 x = "local"
  • outer() 中的 print(x):找到本地 x = "enclosing"
  • 全局的 print(x):找到全局 x = "global"

变量生命周期 ​

本地变量:函数调用时创建,返回时销毁

python
def func():
    x = 10
    print(x)

func()  # 10
func()  # 10(重新创建 x)

全局变量:模块加载时创建,程序结束时销毁

闭包变量:外层函数返回时不销毁(下一节详解)

常见陷阱 ​

陷阱 1:循环变量作用域 ​

python
for i in range(3):
    pass

print(i)  # 2(i 在循环外仍然可见)

Python 的 for 循环不创建新作用域,循环变量泄漏到外层。

陷阱 2:列表推导中的变量 ​

python
x = "global"
result = [x for x in range(3)]
print(x)  # Python 3: "global"(不泄漏)

Python 3 中列表推导有独立作用域,不影响外层变量。(Python 2 会泄漏)

陷阱 3:默认参数在定义时求值 ​

python
def func(x, items=[]):
    items.append(x)
    return items

print(func(1))  # [1]
print(func(2))  # [1, 2],不是 [2]!

默认值 [] 在函数定义时创建一次,所有调用共享。使用 None 作为默认值。

练习 ​

  1. 预测输出:不运行代码,预测输出,然后验证:

    python
    x = 10
    
    def func():
        x = 20
        print(f"Inside func: {x}")
    
    func()
    print(f"Outside func: {x}")
  2. 全局计数器:编写函数 increment_global() 和 reset_global(),分别递增和重置全局计数器 counter。

  3. nonlocal 实践:修复以下代码,使 increment() 能够修改外层的 count:

    python
    def make_counter():
        count = 0
        
        def increment():
            count = count + 1
            return count
        
        return increment
    
    counter = make_counter()
    print(counter())  # 应输出 1
    print(counter())  # 应输出 2
  4. LEGB 顺序:给定以下代码,解释每个 print(x) 输出什么以及为什么:

    python
    x = "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)
  5. 避免内置名字:以下代码有什么问题?如何修复?

    python
    def calculate_length(list):
        sum = 0
        for item in list:
            sum = sum + 1
        return sum

完成练习后,打开本章答案页核对。

下一步 ​

理解了作用域规则,下一节学习闭包:函数如何捕获外层变量,闭包的生命周期和实际应用。

回到第二章概览 · 返回Python 学习路线

用心记录每一步成长