Python 作用域

Python作用域详解

📊 作用域层次结构

Built-in(内置)作用域 ← 最高层

Global(全局)作用域

Enclosing(闭包)作用域

Local(局部)作用域 ← 最底层

🔍 四种作用域详解

1. 局部作用域(Local Scope)

def calculate():
# 局部作用域开始
x = 10 # 局部变量
y = 20
result = x + y
return result
# 局部作用域结束

calculate()
# print(x) # 错误!x在局部作用域,外部无法访问

2. 闭包作用域/嵌套作用域(Enclosing Scope)

def outer():
# 外部函数作用域开始
x = "outer" # 闭包作用域变量

def inner():
# 内部函数作用域开始
y = "inner"
print(f"inner: x={x}, y={y}") # 可以访问外部变量x
# 内部函数作用域结束

inner()
# print(y) # 错误!y在内部函数作用域
# 外部函数作用域结束

outer()

3. 全局作用域(Global Scope)

# 全局作用域开始
GLOBAL_VAR = "I'm global" # 全局变量

def show_global():
print(f"In function: {GLOBAL_VAR}")

show_global()
print(f"Outside: {GLOBAL_VAR}") # 可以访问
# 全局作用域结束

4. 内置作用域(Built-in Scope)

# 内置作用域包含Python内置函数和异常
print(len([1, 2, 3])) # len是内置函数
print(type("hello")) # type是内置函数
print(max(1, 2, 3)) # max是内置函数

# 查看内置作用域
import builtins
print(dir(builtins)[:10]) # 显示前10个内置名称

🎯 LEGB规则

Python查找变量时遵循 LEGB规则(从内到外查找):

  • Local:局部作用域
  • Enclosing:闭包作用域
  • Global:全局作用域
  • Built-in:内置作用域
# LEGB规则示例
x = "global" # 全局变量

def outer():
x = "enclosing" # 闭包变量

def inner():
x = "local" # 局部变量
print(x) # 输出"local"(先找局部)

inner()

outer()
# LEGB查找顺序演示
x = "global"

def outer():
x = "enclosing"

def inner():
# 没有定义局部变量x
print(x) # 输出"enclosing"(向上查找)

inner()

outer()
# 一直找到内置作用域
def test():
# 没有定义len变量
print(len("hello")) # 找到内置函数len,输出5

test()

🔑 关键字:global和nonlocal

global关键字

# 在函数内部修改全局变量
counter = 0

def increment():
global counter # 声明使用全局变量
counter += 1
print(f"Counter: {counter}")

increment() # Counter: 1
increment() # Counter: 2
print(f"Global counter: {counter}") # Global counter: 2
# 不使用global的后果
count = 0

def increment_wrong():
# 这里创建了一个新的局部变量,而不是修改全局变量
count = count + 1 # 错误!UnboundLocalError
print(count)

# increment_wrong() # 会报错

nonlocal关键字

def outer():
count = 0

def inner():
nonlocal count # 声明使用外层函数的变量
count += 1
print(f"Inner: {count}")

inner()
print(f"Outer: {count}")

outer()
# 输出:
# Inner: 1
# Outer: 1
# 多层嵌套中的nonlocal
def outer():
x = "outer"

def middle():
x = "middle"

def inner():
nonlocal x # 修改middle的x,不是outer的x
x = "inner修改了middle的x"
print(f"Inner: {x}")

inner()
print(f"Middle: {x}") # 被inner修改了

middle()
print(f"Outer: {x}") # 仍然是"outer"

outer()

⚡ 作用域实战示例

示例1:闭包和状态保持

def make_counter():
count = 0 # 闭包作用域中的变量

def counter():
nonlocal count
count += 1
return count

return counter

# 创建两个独立的计数器
counter1 = make_counter()
counter2 = make_counter()

print(counter1()) # 1
print(counter1()) # 2
print(counter2()) # 1(独立的计数器)
print(counter1()) # 3

示例2:工厂函数

def power_factory(exponent):
"""创建计算幂的函数"""
def power(base):
return base ** exponent

return power

square = power_factory(2)
cube = power_factory(3)

print(square(5)) # 25 (5²)
print(cube(5)) # 125 (5³)
print(square(3)) # 9 (3²)

示例3:装饰器中的作用域

def logger(func):
"""记录函数调用的装饰器"""
call_count = 0 # 闭包作用域

def wrapper(*args, **kwargs):
nonlocal call_count
call_count += 1
print(f"调用 {func.__name__}{call_count}次")
return func(*args, **kwargs)

return wrapper

@logger
def greet(name):
print(f"Hello, {name}!")

greet("Alice")
greet("Bob")
greet("Charlie")

🧪 作用域测试与陷阱

陷阱1:列表推导式中的变量泄露(Python 3中已修复)

# Python 2中,列表推导式中的变量会泄露到外部作用域
# Python 3中已修复

x = "原始x"
numbers = [x for x in range(5)]
print(x) # Python 3中输出:"原始x",Python 2中输出:4
print(numbers) # [0, 1, 2, 3, 4]

陷阱2:默认参数的作用域

def add_item(item, items=[]):  # 默认值在函数定义时计算一次!
items.append(item)
return items

print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] - 保留了之前的值!
print(add_item("cherry")) # ['apple', 'banana', 'cherry']

# 正确做法:使用None作为默认值
def add_item_correct(item, items=None):
if items is None:
items = [] # 每次调用都创建新列表
items.append(item)
return items

print(add_item_correct("apple")) # ['apple']
print(add_item_correct("banana")) # ['banana'] - 新的列表

陷阱3:循环变量在闭包中的问题

# 常见问题:循环创建闭包
functions = []
for i in range(3):
def show():
print(i)
functions.append(show)

for f in functions:
f() # 全部输出2!因为i最后的值是2

# 解决方案1:使用默认参数
functions = []
for i in range(3):
def show(x=i): # 使用默认参数捕获当前值
print(x)
functions.append(show)

for f in functions:
f() # 输出0, 1, 2

# 解决方案2:使用闭包工厂
def make_show(i):
def show():
print(i)
return show

functions = [make_show(i) for i in range(3)]
for f in functions:
f() # 输出0, 1, 2

📊 作用域可视化工具

查看局部和全局变量

def example_function(a, b):
local_var = "局部变量"
print("局部变量:", locals()) # 查看局部命名空间
print("全局变量:", globals().keys()) # 查看全局命名空间

example_function(1, 2)

# 输出示例:
# 局部变量: {'a': 1, 'b': 2, 'local_var': '局部变量'}
# 全局变量: dict_keys(['__name__', '__doc__', ..., 'example_function'])

检查变量是否存在

def check_variables():
x = 10

# 检查变量是否在局部作用域
print('x' in locals()) # True
print('y' in locals()) # False

# 检查变量是否在全局作用域
print('x' in globals()) # False(局部变量)

# 使用内置作用域检查
print('len' in dir(__builtins__)) # True

check_variables()

🔧 高级作用域应用

动态作用域模拟(使用闭包)

class DynamicScope:
def __init__(self):
self.scopes = [{}]

def push_scope(self):
self.scopes.append({})

def pop_scope(self):
if len(self.scopes) > 1:
return self.scopes.pop()
return None

def set_var(self, name, value):
self.scopes[-1][name] = value

def get_var(self, name):
# 从内向外查找
for scope in reversed(self.scopes):
if name in scope:
return scope[name]
raise NameError(f"变量 '{name}' 未定义")

# 使用动态作用域
ds = DynamicScope()
ds.set_var("x", 10)
ds.push_scope() # 进入新作用域
ds.set_var("x", 20)
print(ds.get_var("x")) # 20(内层作用域)
ds.pop_scope() # 退出作用域
print(ds.get_var("x")) # 10(外层作用域)

上下文管理器中的作用域

import contextlib

@contextlib.contextmanager
def temporary_value(obj, attr, temp_value):
"""临时修改对象的属性"""
original = getattr(obj, attr, None)
setattr(obj, attr, temp_value)
try:
yield # 进入上下文,使用临时值
finally:
setattr(obj, attr, original) # 恢复原始值

class Config:
debug_mode = False

config = Config()
print(f"原始: {config.debug_mode}") # False

with temporary_value(config, 'debug_mode', True):
print(f"临时: {config.debug_mode}") # True

print(f"恢复后: {config.debug_mode}") # False

💡 最佳实践总结

  1. 最小化全局变量:优先使用局部变量和函数参数
  2. 明确变量作用域:使用有意义的命名区分不同作用域的变量
  3. 避免副作用:函数尽量不要修改外部作用域的变量
  4. 合理使用闭包:闭包适合用于创建有状态的行为
  5. 小心默认参数:使用None作为可变对象的默认值
  6. 了解LEGB规则:明确变量查找顺序,避免命名冲突
  7. 适时使用global/nonlocal:只在确实需要修改外部变量时使用
  8. 保持作用域清晰:避免过深的嵌套,保持代码可读性

掌握Python作用域机制对于编写高质量、可维护的代码至关重要。合理利用作用域可以创建更模块化、更安全的代码结构。