Skip to content

Python 命名空间和作用域

在 Python 中,命名空间和作用域是两个重要的概念,它们决定了变量的可见性和生命周期。本章节将详细介绍 Python 中的命名空间和作用域。

命名空间(Namespace)

基本概念

命名空间是一个映射,将名称(如变量名、函数名、类名等)映射到对应的值(如变量值、函数对象、类对象等)。

命名空间的类型

Python 中有三种主要的命名空间:

  1. 内置命名空间(Built-in Namespace):包含 Python 内置的函数和变量,如 print(), len(), int 等。
  2. 全局命名空间(Global Namespace):包含模块级别的变量、函数和类定义。
  3. 局部命名空间(Local Namespace):包含函数或类的方法内部定义的变量和参数。

命名空间的查找顺序

当引用一个名称时,Python 会按照以下顺序查找命名空间:

  1. 局部命名空间(当前函数或方法内部)
  2. 全局命名空间(当前模块)
  3. 内置命名空间

如果在所有命名空间中都找不到该名称,则会引发 NameError 异常。

示例

python
# 命名空间示例

# 全局命名空间变量
x = 10  # 全局变量

def func():
    # 局部命名空间变量
    y = 20  # 局部变量
    print(f"局部变量 y: {y}")
    print(f"全局变量 x: {x}")
    # 尝试访问内置函数
    print(f"内置函数 len: {len}")

# 调用函数
func()

# 尝试访问局部变量(会失败)
try:
    print(f"尝试访问局部变量 y: {y}")
except NameError as e:
    print(f"错误: {e}")

# 访问全局变量
print(f"全局变量 x: {x}")

# 访问内置函数
print(f"使用内置函数 len: {len([1, 2, 3])}")

输出:

局部变量 y: 20
全局变量 x: 10
内置函数 len: <built-in function len>
错误: name 'y' is not defined
全局变量 x: 10
使用内置函数 len: 3

作用域(Scope)

基本概念

作用域是指命名空间的可见范围,即变量在哪些代码区域中可以被访问。

作用域的类型

Python 中有四种主要的作用域:

  1. 局部作用域(Local Scope):函数或方法内部的作用域。
  2. 嵌套作用域(Enclosing Scope):嵌套函数的外层函数的作用域。
  3. 全局作用域(Global Scope):模块级别的作用域。
  4. 内置作用域(Built-in Scope):包含内置名称的作用域。

作用域的查找顺序

当引用一个名称时,Python 会按照以下顺序查找作用域:

  1. 局部作用域(L)
  2. 嵌套作用域(E)
  3. 全局作用域(G)
  4. 内置作用域(B)

这个顺序通常被称为 LEGB 规则。

示例 1:基本作用域

python
# 基本作用域

# 全局作用域变量
x = 10

def outer():
    # 嵌套作用域变量
    y = 20
    
    def inner():
        # 局部作用域变量
        z = 30
        print(f"局部变量 z: {z}")
        print(f"嵌套作用域变量 y: {y}")
        print(f"全局变量 x: {x}")
    
    inner()
    # 尝试访问局部变量(会失败)
    try:
        print(f"尝试访问 inner 中的局部变量 z: {z}")
    except NameError as e:
        print(f"错误: {e}")

# 调用函数
outer()

# 尝试访问嵌套作用域变量(会失败)
try:
    print(f"尝试访问 outer 中的变量 y: {y}")
except NameError as e:
    print(f"错误: {e}")

输出:

局部变量 z: 30
嵌套作用域变量 y: 20
全局变量 x: 10
错误: name 'z' is not defined
错误: name 'y' is not defined

示例 2:LEGB 规则

python
# LEGB 规则

# 内置作用域(Python 内置)
# len() 等内置函数

# 全局作用域
x = 100

def outer():
    # 嵌套作用域
    x = 200
    
    def inner():
        # 局部作用域
        x = 300
        print(f"局部作用域 x: {x}")  # 300
    
    inner()
    print(f"嵌套作用域 x: {x}")  # 200

# 调用函数
outer()
print(f"全局作用域 x: {x}")  # 100

输出:

局部作用域 x: 300
嵌套作用域 x: 200
全局作用域 x: 100

变量的作用域

全局变量

全局变量是在模块级别定义的变量,可以在模块的任何地方访问。

局部变量

局部变量是在函数或方法内部定义的变量,只能在定义它的函数或方法内部访问。

非局部变量

非局部变量是在嵌套函数的外层函数中定义的变量,在内层函数中可以通过 nonlocal 关键字访问和修改。

示例 1:全局变量和局部变量

python
# 全局变量和局部变量

x = 10  # 全局变量

def func():
    x = 20  # 局部变量(与全局变量同名)
    print(f"函数内部的 x: {x}")  # 20

# 调用函数
func()
print(f"函数外部的 x: {x}")  # 10

输出:

函数内部的 x: 20
函数外部的 x: 10

示例 2:使用 global 关键字

python
# 使用 global 关键字

x = 10  # 全局变量

def func():
    global x  # 声明 x 为全局变量
    x = 20  # 修改全局变量
    print(f"函数内部的 x: {x}")  # 20

# 调用函数
func()
print(f"函数外部的 x: {x}")  # 20(全局变量已被修改)

输出:

函数内部的 x: 20
函数外部的 x: 20

示例 3:使用 nonlocal 关键字

python
# 使用 nonlocal 关键字

def outer():
    x = 10  # 外层函数的变量
    
    def inner():
        nonlocal x  # 声明 x 为非局部变量
        x = 20  # 修改外层函数的变量
        print(f"内层函数的 x: {x}")  # 20
    
    inner()
    print(f"外层函数的 x: {x}")  # 20(外层函数的变量已被修改)

# 调用函数
outer()

输出:

内层函数的 x: 20
外层函数的 x: 20

作用域的实际应用

示例 1:闭包

闭包是指一个函数可以访问其定义时的作用域,即使该函数在其他作用域中被调用。

python
# 闭包示例

def make_counter():
    count = 0  # 外层函数的变量
    
    def counter():
        nonlocal count  # 声明为非局部变量
        count += 1
        return count
    
    return counter  # 返回内层函数

# 创建计数器
c1 = make_counter()
c2 = make_counter()

# 使用计数器
print(f"c1: {c1()}")  # 1
print(f"c1: {c1()}")  # 2
print(f"c1: {c1()}")  # 3

print(f"c2: {c2()}")  # 1(c2 有自己的 count 变量)
print(f"c2: {c2()}")  # 2
print(f"c1: {c1()}")  # 4

输出:

c1: 1
c1: 2
c1: 3
c2: 1
c2: 2
c1: 4

示例 2:装饰器

装饰器是一种特殊的闭包,用于修改函数的行为。

python
# 装饰器示例

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"调用函数: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"函数 {func.__name__} 返回: {result}")
        return result
    return wrapper

@logger
def add(a, b):
    return a + b

@logger
def multiply(a, b):
    return a * b

# 调用函数
print(f"add(1, 2) = {add(1, 2)}")
print(f"multiply(3, 4) = {multiply(3, 4)}")

输出:

调用函数: add
函数 add 返回: 3
add(1, 2) = 3
调用函数: multiply
函数 multiply 返回: 12
multiply(3, 4) = 12

作用域的陷阱

示例 1:变量遮蔽(Variable Shadowing)

当局部变量与全局变量同名时,局部变量会遮蔽全局变量。

python
# 变量遮蔽

x = 10  # 全局变量

def func():
    x = 20  # 局部变量,遮蔽了全局变量
    print(f"函数内部的 x: {x}")  # 20

# 调用函数
func()
print(f"函数外部的 x: {x}")  # 10(全局变量未被修改)

输出:

函数内部的 x: 20
函数外部的 x: 10

示例 2:延迟绑定(Late Binding)

在闭包中,内层函数会捕获外层函数的变量引用,而不是变量的值。

python
# 延迟绑定

def make_functions():
    functions = []
    for i in range(5):
        def func():
            return i
        functions.append(func)
    return functions

# 创建函数列表
funcs = make_functions()

# 调用函数
for i, func in enumerate(funcs):
    print(f"func[{i}]() = {func()}")  # 所有函数都返回 4

输出:

func[0]() = 4
func[1]() = 4
func[2]() = 4
func[3]() = 4
func[4]() = 4

解决方案:使用默认参数

python
# 解决方案:使用默认参数

def make_functions():
    functions = []
    for i in range(5):
        def func(i=i):  # 使用默认参数,绑定当前值
            return i
        functions.append(func)
    return functions

# 创建函数列表
funcs = make_functions()

# 调用函数
for i, func in enumerate(funcs):
    print(f"func[{i}]() = {func()}")  # 现在返回正确的值

输出:

func[0]() = 0
func[1]() = 1
func[2]() = 2
func[3]() = 3
func[4]() = 4

命名空间和作用域的高级话题

1. 模块的命名空间

每个模块都有自己的全局命名空间,不同模块之间的命名空间是隔离的。

示例:

创建 module1.py

python
# module1.py
x = 10
def func():
    print(f"module1.x = {x}")

创建 module2.py

python
# module2.py
x = 20
def func():
    print(f"module2.x = {x}")

创建 main.py

python
# main.py
import module1
import module2

print(f"module1.x = {module1.x}")
print(f"module2.x = {module2.x}")

module1.func()
module2.func()

运行 main.py

module1.x = 10
module2.x = 20
module1.x = 10
module2.x = 20

2. 类的命名空间

类也有自己的命名空间,类的属性和方法存储在类的命名空间中。

示例:

python
# 类的命名空间

class MyClass:
    x = 10  # 类属性
    
    def __init__(self, y):
        self.y = y  # 实例属性
    
    def method(self):
        z = 30  # 局部变量
        print(f"局部变量 z: {z}")
        print(f"实例属性 y: {self.y}")
        print(f"类属性 x: {self.x}")

# 创建实例
obj = MyClass(20)

# 访问类属性
print(f"类属性 MyClass.x: {MyClass.x}")
print(f"实例访问类属性 obj.x: {obj.x}")

# 访问实例属性
print(f"实例属性 obj.y: {obj.y}")

# 调用方法
obj.method()

# 修改类属性
MyClass.x = 100
print(f"修改后类属性 MyClass.x: {MyClass.x}")
print(f"实例访问修改后的类属性 obj.x: {obj.x}")

# 创建另一个实例
obj2 = MyClass(200)
print(f"新实例 obj2.x: {obj2.x}")  # 继承修改后的类属性
print(f"新实例 obj2.y: {obj2.y}")

输出:

类属性 MyClass.x: 10
实例访问类属性 obj.x: 10
实例属性 obj.y: 20
局部变量 z: 30
实例属性 y: 20
类属性 x: 10
修改后类属性 MyClass.x: 100
实例访问修改后的类属性 obj.x: 100
新实例 obj2.x: 100
新实例 obj2.y: 200

3. 作用域和导入

当导入模块时,导入的名称会添加到当前模块的全局命名空间中。

示例:

python
# 作用域和导入

# 导入模块
import math

# 导入特定名称
from math import sqrt

# 检查命名空间
print(f"math 在全局命名空间中: {'math' in globals()}")
print(f"sqrt 在全局命名空间中: {'sqrt' in globals()}")
print(f"pi 在全局命名空间中: {'pi' in globals()}")  # 不在,因为没有导入

# 使用导入的名称
print(f"math.pi: {math.pi}")
print(f"sqrt(4): {sqrt(4)}")

输出:

math 在全局命名空间中: True
sqrt 在全局命名空间中: True
pi 在全局命名空间中: False
math.pi: 3.1415926535897696
sqrt(4): 2.0

实际应用示例

示例 1:配置管理

python
# 配置管理

# 全局配置
config = {
    "debug": False,
    "timeout": 30,
    "max_connections": 100
}

def get_config(key, default=None):
    """获取配置值"""
    return config.get(key, default)

def set_config(key, value):
    """设置配置值"""
    config[key] = value
    print(f"配置 {key} 已设置为 {value}")

def process_data(data):
    """处理数据"""
    # 使用配置
    timeout = get_config("timeout")
    debug = get_config("debug")
    
    if debug:
        print(f"处理数据,超时设置为 {timeout} 秒")
    
    # 处理逻辑...
    result = len(data)
    
    if debug:
        print(f"处理完成,结果: {result}")
    
    return result

# 测试
print(f"初始配置 debug: {get_config('debug')}")
print(f"初始配置 timeout: {get_config('timeout')}")

# 处理数据
result = process_data([1, 2, 3, 4, 5])
print(f"处理结果: {result}")

# 修改配置
set_config("debug", True)
set_config("timeout", 60)

# 再次处理数据
result = process_data([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print(f"处理结果: {result}")

输出:

初始配置 debug: False
初始配置 timeout: 30
处理结果: 5
配置 debug 已设置为 True
配置 timeout 已设置为 60
处理数据,超时设置为 60 秒
处理完成,结果: 10
处理结果: 10

示例 2:缓存装饰器

python
# 缓存装饰器

def cache(func):
    """缓存函数结果的装饰器"""
    cache_dict = {}  # 闭包中的缓存字典
    
    def wrapper(*args, **kwargs):
        # 创建缓存键
        key = str(args) + str(kwargs)
        
        # 检查缓存
        if key in cache_dict:
            print(f"从缓存中获取结果")
            return cache_dict[key]
        
        # 计算结果
        result = func(*args, **kwargs)
        
        # 存储到缓存
        cache_dict[key] = result
        print(f"计算结果并缓存")
        
        return result
    
    return wrapper

@cache
def fibonacci(n):
    """计算斐波那契数列"""
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

@cache
def factorial(n):
    """计算阶乘"""
    if n <= 1:
        return 1
    return n * factorial(n-1)

# 测试
print(f"fibonacci(10) = {fibonacci(10)}")
print(f"fibonacci(10) = {fibonacci(10)}")  # 从缓存获取
print(f"fibonacci(15) = {fibonacci(15)}")

print(f"factorial(5) = {factorial(5)}")
print(f"factorial(5) = {factorial(5)}")  # 从缓存获取
print(f"factorial(10) = {factorial(10)}")

输出:

计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
fibonacci(10) = 55
从缓存中获取结果
fibonacci(10) = 55
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
fibonacci(15) = 610
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
factorial(5) = 120
从缓存中获取结果
factorial(5) = 120
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
计算结果并缓存
factorial(10) = 3628800

总结

Python 中的命名空间和作用域是理解 Python 代码执行机制的重要概念:

  1. 命名空间:是名称到值的映射,包括内置、全局和局部三种类型。
  2. 作用域:是命名空间的可见范围,包括局部、嵌套、全局和内置四种类型。
  3. 查找顺序:当引用名称时,Python 会按照 LEGB 规则查找命名空间。
  4. 变量类型:全局变量、局部变量和非局部变量,分别使用 globalnonlocal 关键字声明。
  5. 实际应用:闭包、装饰器、配置管理、缓存等。
  6. 常见陷阱:变量遮蔽、延迟绑定等。

理解命名空间和作用域的概念,对于编写清晰、可维护的 Python 代码非常重要。