skip to content
Logo Lostman_Wang的小站

Python函数-循环变量闭包陷阱详解

一、列表推导式的作用域

1、静态外层作用域(Lexical Enclosing Scope)

列表推导式在语法上位于某个“外层作用域”内部,这个外层作用域可能是:

  • 模块级别(global 作用域)
  • 函数内部(local 作用域)
  • 类的命名空间(class body)
  • 另一个推导式或生成器表达式内部(嵌套推导式)

“外层作用域”就是静态词法作用域,即包含该推导式的最小可执行代码块的作用域。

2、列表推导式自己的局部作用域

在 Python 3 中,列表推导式相当于隐式地生成了一个嵌套函数,其伪代码结构大致如下:

# 源代码
result = [expr(x) for x in iterable if cond(x)]
# 伪代码(简化)
def _listcomp(_outer_iterable):
_result = []
for x in _outer_iterable:
if cond(x):
_result.append(expr(x))
return _result
result = _listcomp(iterable)
  • 这个 _listcomp 就是推导式自己的局部作用域。
  • 循环变量 x、列表名 _result 等都只存在于该局部作用域中,推导式执行完后就被回收。

3、变量查找顺序(LEGB 规则)

列表推导式在执行时,如果内部引用了某个未在推导式内部绑定的变量名,Python 会按照 LEGB 规则依次查找:

  • Local:推导式内部(循环变量、推导式内部临时变量)
  • Enclosing:包含该推导式的函数作用域(如果有)
  • Global:模块级别
  • Built-in:内建命名空间
y = 10 # 全局变量
def foo():
z = 5 # 函数局部变量
result = [x + y + z for x in range(3)]
return result
print(foo()) # [15, 16, 17]
# x 是推导式局部变量
# y 在全局作用域找到
# z 在函数局部作用域找到

4、常见疑问与陷阱

4.1、循环变量不会泄漏

# Python 3
squares = [x * x for x in range(5)]
print(x) # NameError: name 'x' is not defined

4.2、类作用域的特例

  • 在类体中定义的推导式无法直接访问类作用域中的变量,因为类体本身并不创建闭包作用域:
class C:
attr = 100
lst = [attr for _ in range(3)] # NameError: name 'attr' is not defined
# 解决方式:使用 self.attr(在方法里)或把值作为参数传进去。

5、总结

  • 在 Python 3 中,列表推导式“所在的外层作用域”就是静态语法上包含它的那个作用域(函数、模块等);而推导式本身会额外创建一个局部作用域,循环变量不会泄漏,但仍能读取外层变量。

二、循环变量闭包拆解

1、原始匿名写法

fs = [lambda x: x + i for i in range(3)]
  • 列表推导式所在的外层作用域就是当前模块(或函数)
  • 因此只有一个变量 i 被所有 lambda 共享,形成 1 个 cell
  • 列表推导式本身不是函数,lambda 的闭包引用的是 推导式外部的 i,而不是推导式内部每次循环的局部 i

2、正确拆解(1 个外层、3 个内层)

def outer(): # 只有一个外层
fs = []
for i in range(3):
def inner(x): # 每次循环都创建新的闭包
return x + i
fs.append(inner)
return fs
  • iouter() 的局部变量
  • 每轮循环 inner 都捕获 当前值(Python 通过 cell 机制),所以 3 个闭包各持不同 cell

3、错误拆解(多层 outer)

def outer():
for i in range(3):
def inner(x):
return x + i
return inner # 只返回第一个就结束循环

这里把 for 写在 outer 内部,但 每次循环都 return,导致只生成 1 个闭包就退出,语义完全不符。

4、总结

  • 原式中的 lambda 闭包引用的是列表推导式外部作用域的 i
  • 正确拆解应保持 1 个外层函数 + 循环内新建 3 个闭包
  • 多层 outer 会导致提前返回,破坏了“3 个闭包”的数量和共享作用域。

三、“语法级”修复

1、原始匿名写法

fs = [lambda x, i=i: x + i for i in range(3)]
  • 外层作用域:当前模块(或当前函数)。
  • 列表推导式遍历range(3) → i 取值 0,1,2
  • 循环变量i 只有一个实例,但 每次 lambda 通过默认参数 i=i 把当前值冻结下来,于是得到 3 个 独立闭包,分别捕获 0,1,2

2、等价拆解:无匿名、无推导式

def outer():
"""外层作用域唯一;循环创建 3 个闭包,并用默认参数冻结 i"""
fs = [] # 结果列表
for i in range(3): # 手动循环
def inner(x, _i=i): # _i 为默认参数,冻结当前 i
return x + _i
fs.append(inner) # 存入列表
return fs # 返回 3 个闭包
# 使用
funcs = outer()
print([f(0) for f in funcs]) # [0, 1, 2]
步骤说明
外层只有一个作用域(outer 的局部变量 i
默认参数每轮循环把当前 i 值写入 inner 的默认参数,生成 3 个独立 cell
无推导式完全用 defforappend 代替

3、总结

  • 外层仅有一个作用域,for 循环每次新建 def
  • 默认参数 _i=i 把当前循环值 冻结进闭包
  • 最终得到 3 个独立闭包,行为与原式完全一致