Python闭包与自由变量详解:nonlocal关键字、lambda陷阱及偏函数应用

季宁
季宁
发布时间:2026-10-15| 6人浏览过
原创

本文深入解析Python中闭包与自由变量的工作机制,重点探讨不可变对象在闭包中引发的UnboundLocalError及TypeError错误,并详解nonlocal关键字的用法。同时,剖析lambda表达式在列表解析中的自由参数陷阱,对比map与默认参数解决方案,最后介绍闭包在装饰器、惰性求值及functools.partial偏函数中的实际应用场景。

1.定义

当在函数内部定义另一个函数,且内部函数引用了外部函数的变量(遵循LEGB查找规则),最终返回内部函数对象时,这种能够访问其定义时所在作用域的函数及其引用的变量被称为闭包。其中被引用的非全局变量被称为自由变量。该自由变量会保存在外部函数对象的只读属性 __closure__ 中,并与内层函数建立绑定关系,从而防止自由变量在内存中被垃圾回收。示例如下:

# 计算函数被调用的次数
def counter(FIRST=0):
   -----------------__closure__---------------
   |cnt = [FIRST]                            |  # 之所以选列表是因为作用域问题,详见后文
   |										 |
   |def add_one():                           |
   |    cnt[0] += 1                          |
   |    return cnt[0]                        |
    ------------------------------------------
    return add_one

# 每当外部函数被调用时,都将重新定义内部的函数,而变量 cnt 的值也可能不同
num5 = counter(5)
num10 = counter(10)

print(num5())  # 6
print(num5())  # 7
print(num10())  # 11
print(num10())  # 12

# 如果这个函数仅仅是嵌套函数,那么它的 __closure__ 应该是 None
print(num5.__closure__)  # (<cell at 0x0163FE30: list object at 0x01514A80>,)
print(num5.__closure__[0].cell_contents)  # 7
print(num10.__closure__[0].cell_contents)  # 12

# 或者通过 __code__.co_freevars 查看函数中是否有自由变量,如果有自由变量,即为闭包
print(num10.__code__.co_freevars)  # ('cnt',)

2.nonlocal 关键字

上述代码中的 cnt 变量使用了列表,这是一种可变对象。但如果处理的是不可变对象,例如整数(int)或元组(tuple),情况会有何不同?

def counter(FIRST=0):
    cnt = FIRST  # number
    
    def add_one():
        cnt += 1
        return cnt
    return add_one

num5 = counter(5)
print(num5.__closure__)
print(num5.__code__.co_freevars)
print(num5())
----------------------------------------------------------------------------
def counter(FIRST=0):
    cnt = (FIRST,)  # tuple
    
    def add_one():
        cnt[0] += 1
        return cnt[0]
    return add_one

num5 = counter(5)
print(num5.__closure__)
print(num5.__code__.co_freevars)
print(num5())

以上实例的输出结果如下:

None
()
Traceback (most recent call last):
  File "test.py", line, in <module>
    print(num5())
  File "test.py", line, in add_one
    cnt += 1
UnboundLocalError: local variable 'cnt' referenced before assignment
----------------------------------------------------------------------------
(<cell at 0x0180FE10: tuple object at 0x0173A750>,)
('cnt',)
Traceback (most recent call last):
  File "test.py", line, in <module>
    print(num5())
  File "test.py", line, in add_one
    cnt[0] += 1
TypeError: 'tuple' object does not support item assignment

从输出可以看出,此时 cnt 不再是自由变量,而是被解释器视为局部变量,并引发了 UnboundLocalError(未绑定局部变量错误)。为何它不再是自由变量?而列表为何能正常工作?

这是因为 Python 不要求在使用前显式声明变量。Python 解释器在编译阶段确定:只要函数体内对某个变量执行了赋值操作,该变量即被判定为局部变量。 Python 的模块代码在运行前不会经过预编译,但模块内的函数体代码在运行前会进行预编译。因此,无论变量绑定发生在作用域的哪个位置,编译器都能提前知晓。

语句 cnt += 1 等价于 cnt = cnt + 1,这属于赋值操作。因此,Python 解释器在编译阶段认定 cnt 为函数内的局部变量。但在执行时,先计算 cnt + 1,由于此前已认定 cnt 为局部变量,而在局部作用域中找不到 cnt 的初始值,解释器也不会向外层作用域查找,从而报错。因此,此时 cnt 已不再是自由变量。

那么对于 tuple 类型的 cnt 呢?执行 cnt[0] = cnt[0] + 1 时,虽然涉及赋值,但赋值目标 cnt[0] 是对 cnt 的索引访问,cnt 本身是从外层作用域引用的。 因此,输出结果显示:cnt 确实仍是自由变量,但由于它是不可变对象,执行赋值操作时会抛出 TypeError 错误。至此,你应该明白为何列表可行而元组会报错的原因了。

或者,你可以使用 nonlocal 关键字。该关键字用法与 global 类似,允许你对非全局作用域内的变量进行赋值。它能使一个被赋值的局部变量变为自由变量,且当 nonlocal 声明的变量发生变化时,__closure__ 中存储的值也会同步更新:

def counter(FIRST=0):
    cnt = FIRST  # number
    
    def add_one():
        nonlocal cnt
        cnt += 1
        return cnt
    return add_one

num5 = counter(5)
print(num5.__closure__)
print(num5.__code__.co_freevars)
print(num5())
(<cell at 0x01BFFE30: int object at 0x53E064D0>,)
('cnt',)
6

nonlocal 和 global

def scope_test():
    spam = "test spam"
    
    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    do_nonlocal()
    print("After nonlocal assignment:", spam)  # nonlocal spam 

    do_global()
    print("After global assignment:", spam)  # nonlocal spam
    
scope_test()
print("In global scope:", spam)  # global spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

3.注意事项

lambda 表达式的自由参数存在陷阱,特别是在与列表解析或 for 循环结合使用时。lambda para_list : expression 等价于 def (para_list): return expression。

#---CASE1
fs = [lambda j:i*j for i in range(3)]
print([f(2) for f in fs])

#---CASE2
fs = map(lambda i:(lambda j: i*j), range(3))
print([f(2) for f in fs])

#---CASE3
fs = [(lambda i:lambda j:i*j)(i) for i in range(3)]
print([f(2) for f in fs])
[4, 4, 4]
[0, 2, 4]
[0, 2, 4]

首先,CASE1 和 CASE3 显然在每次循环时都将一个 lambda 函数添加到列表中。不同之处在于,CASE1 添加的 lambda 函数中的 i 并未在定义时接收 for 循环中 i 的值,它仅在调用时才去外层作用域查找 i 的值。此时找到的 i 是 for 循环结束后的最终值。CASE3 则在定义和添加时立即为 i 赋好了初值。CASE2 则因为 map 每次迭代时都将可迭代对象的元素传给了 i,所以 CASE2 中每个 lambda 函数的 i 拥有各自的值。

对于这种 lambda 自由参数的问题,如果不是故意利用闭包特性,建议将其转换为默认参数:

fs = [lambda x: x+i for i in range(3)]
print([f(2) for f in fs])

fs = [lambda x, i=i: x+i for i in range(3)]
print([f(2) for f in fs])
[4, 4, 4]
[2, 3, 4]

另外,列表解析内部的作用域是一个全新的作用域,这与普通的 for 循环有所不同:

#---CASE4
fs = [lambda j:i*j for i in range(3)]
print([f(2) for f in fs])

i = 4
print([f(2) for f in fs])

#---CASE5
fs = []

for i in range(3):
    fs.append(lambda j:i*j)
print([f(2) for f in fs])

i = 4
print([f(2) for f in fs])
[10, 10, 10]
[10, 10, 10]
[10, 10, 10]
[8, 8, 8]

4.使用场景

  • 装饰器

  • 惰性求值,常见于数据库访问场景,可参考 Django 的 queryset 实现。

  • 需要对某个函数的参数提前赋值的情况;当然也可以使用 functools.partial 的偏函数:functools.partial(func, *args, **kw),它返回一个 partial 函数对象。

# y = a*x + b, a 和 b 可能只出现一次, x 会出现多次
def line(a, b, x):
    return a*x + b

print(line(3, 4, 5))
print(line(3, 4, 6))
print(line(7, 4, 5))
print(line(7, 4, 6))

# 2.使用闭包
def line(a, b):
    def value(x):
        return a*x + b
    return value

# y = 3x + 4
line1 = line(3, 4)
print(line1(5))
print(line1(6))
print(line1(7))

# y = 9x + 7
line2 = line(9, 7)
print(line2(5))
print(line2(6))
print(line2(7))

# 3.使用 functools.partial 偏函数
from functools import partial

line3 = partial(line, 3)
print(line3)  # functools.partial(<function line at 0x011237C8>, 3)
print(line3(4, 5))

line4 = partial(line, 3, 4)
print(line4(5))
print(line4(6))
print(line4(7))

line5 = partial(line, 9, 7)
print(line5(5))
print(line5(6))
print(line5(7))

简单总结 functools.partial 的作用:它能将函数的某些参数固定(即设置默认值),并返回一个新函数,调用这个新函数会更加简便。

总结

到此这篇关于Python中闭包和自由变量的文章就介绍到这了,更多相关Python闭包和自由变量内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

相关标签:

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

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 算法库。