Python闭包与Lambda作用域陷阱:迟绑定原理及解决方案

向北走
向北走
发布时间:2026-10-15| 4人浏览过
原创

本文深入解析Python中闭包与Lambda表达式常见的作用域陷阱。由于Python采用LEGB查找规则且闭包具有迟绑定特性,循环变量在内部函数调用时才被查询,导致所有函数返回相同结果。文章通过生成器、列表推导及filter迭代器三个典型案例,详细演示了问题成因,并提供了将闭包作用域转换为局部作用域的有效修复方案。

Python闭包与lambda的作用域

lambda写法

def fun():
    for i in range(3):
        yield lambda x : x * i

f0, f1, f2 = fun()
print(f0(1), f1(2), f2(3))

闭包的写法

def fun():
    result = []
    for i in range(3):
        def demo(x):
            return x * i
        result.append(demo)
    return result
f0, f1, f2 = fun()
print(f0(1), f1(2), f2(3))

上述两种写法的执行结果均为2、4、6,但按照直觉预期,结果应当是0、2、6。

问题原因:

根本原因在于Python的变量查找规则LEGB(Local、Enclosing、Global、Built-in)。在上述示例中,变量i位于闭包作用域(Enclosing scope)。Python的闭包机制采用迟绑定(Late Binding),这意味着内部函数中引用的变量i的值,并非在定义时确定,而是在内部函数实际被调用时才去查找并获取。

解决办法

将闭包作用域转换为局部作用域。

lambda写法

def fun():
    for i in range(3):
        yield lambda x, i = i: x * i

f0, f1, f2 = fun()
print(f0(1), f1(2), f2(3))

闭包写法

def fun():
    result = []
    for i in range(3):
        def demo(x, i=i):
            return x * i
        result.append(demo)
    return result
f0, f1, f2 = fun()
print(f0(1), f1(2), f2(3))

以上两种修改后的输出结果为0、2、6。

另一种情况:

def fun():
    for i in range(3):
        yield lambda x : x * i
f0, f1, f2 = fun()
print(f0(1), f1(2), f2(3))

输出结果依然为2、4、6。

问题原因

fun()方法返回的是一个生成器(或迭代器),它并未立即执行,而是在每次遍历调用时才执行具体逻辑。

在遍历结束后执行打印操作时,变量i使用的是最后一次循环迭代后的值。如果将lambda视为闭包方法,变量i的值依然来源于闭包作用域(无局部变量覆盖)。

python当中的坑(闭包与lambda)

先来看一个栗子

def create():
    return [lambda x:i*x for i in range(5)]
 
for i in create():
    print(i(2))

结果:

8

8

8

8

8

create函数的返回值是一个列表,列表中的每个元素都是一个函数,其功能是将输入参数x乘以倍数i。预期的结果应该是0、2、4、6、8,但实际输出了五个8,这确实出乎意料。

由于出现这个陷阱时经常使用了lambda,所以可能会误认为是lambda的问题,但lambda表示不愿背这个锅。问题的本质在于Python中的属性查找规则LEGB(Local、Enclosing、Global、Built-in)。在上面的例子中,i位于闭包作用域(Enclosing scope),而Python的闭包是迟绑定,这意味着闭包中用到的变量的值,是在内部函数被调用时查询得到的。

解决办法也很简单,那就是变闭包作用域为局部作用域。

def create():
    return [lambda x, i=i:i*x for i in range(5)]
 
for i in create():
    print(i(2))

换种写法:

def create():
    a = []
    for i in range(5):
        def demo(x, i=i):
            return x*i
        a.append(demo)
    return a
 
for i in create():
    print(i(2))

以上两种写法是一样的

结果:

0 2 4 6 8

再来一个问题相似的栗子

代码很简单:(声明:python3问题)

nums = range(2,20)
for i in nums:
    nums = filter(lambda x: x==i or x%i, nums)
print(list(nums))

结果:

[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

同样按照正常的逻辑结果应该为:

[2, 3, 5, 7, 11, 13, 17, 19]

问题产生的原因:

  • 在python3当中filter()函数返回的是一个迭代器,因此并没有做真正的执行,而是在每次调用的时候执行(python2中filter()返回的值列表,无此现象)
  • 在遍历后执行打印时,现在执行循环当中的函数,同上面一个栗子的问题,i这个变量使用的是最后调用时的值,与以上栗子不同的是以上栗子用的是内嵌作用域的值,而这个栗子用的是全局i的值

修改代码:

nums = range(2,20)
for i in nums:
    nums = filter(lambda x,i=i: x==i or x%i, nums)
print(list(nums))

结果:

[2, 3, 5, 7, 11, 13, 17, 19]

以上为个人经验,希望能给大家一个参考,也希望大家多多支持本站。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。