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 3squares = [x * x for x in range(5)]print(x) # NameError: name 'x' is not defined4.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 fsi是outer()的局部变量- 每轮循环
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 |
| 无推导式 | 完全用 def、for、append 代替 |
3、总结
- 外层仅有一个作用域,
for循环每次新建def - 默认参数
_i=i把当前循环值 冻结进闭包 - 最终得到 3 个独立闭包,行为与原式完全一致