展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > 浅析Python闭包如何捕获自由变量

浅析Python闭包如何捕获自由变量

一、从一个计数器开始 很多朋友在学习作用域时,都遇到过下面这段让人困惑的代码: def make_counter(): count = 0 def counter(): count += 1 # 这里会报错 return count return counter c = make_counter()

本文目录

    一、从一个计数器开始

    很多朋友在学习作用域时,都遇到过下面这段让人困惑的代码:

    def make_counter():
        count = 0
        def counter():
            count += 1   # 这里会报错
            return count
        return counter
    
    c = make_counter()
    print(c())  # UnboundLocalError: local variable 'count' referenced before assignment
    

    这可不是什么bug——恰恰相反,这是Python作用域规则在“说话”。报错的根源就藏在count += 1这行看似简单的代码里,而理解它,几乎是理解闭包本质的必经之路。

    要彻底搞懂这段代码,我们得先理清Python的作用域规则,以及闭包到底是怎么工作的。

    二、LEGB 规则:名字查找的顺序

    在Python的世界里,每当需要查找一个变量名时,解释器都会遵循一个固定的顺序,我们称之为LEGB法则:

    • L(Local):当前函数内部定义的变量
    • E(Enclosing):外层嵌套函数中的变量
    • G(Global):模块级全局变量
    • B(Built-in):Python内置的名字,比如len、print

    用一段代码来验证这个顺序,会非常直观:

    x = "global"  # G 层
    
    def outer():
        x = "enclosing"  # E 层
    
        def inner():
            x = "local"  # L 层
            print(x)  # -> local(找到了就停)
    
        inner()
    
    outer()
    

    每一层都可以定义和上层同名的变量,它们彼此独立,互不干扰。Python之所以这样设计,是因为名字查找发生在运行时,而非编译时——解释器执行到哪一行,才去对应的作用域里寻找变量。

    LEGB规则可以这样可视化理解:

    浅析Python闭包如何捕获自由变量

    查找过程就像从圆心向外扩散,由内而外,找到即止。

    三、global关键字:打破 E 层

    现在回到开头那个报错的计数器。问题出在count += 1这行,它实际上等价于:

    count = count + 1
    

    Python解释器看到这行代码时,发现等号左边出现了count,就立刻判定count应该是当前作用域(L层)的局部变量。但麻烦来了:count实际上定义在make_counter()的作用域里(E层),并不在counter()的局部作用域里。Python拒绝在E层为L层创建同名变量,于是UnboundLocalError就出现了。

    一个直接的解决思路是,把count提升到全局作用域:

    count = 0
    
    def make_counter():
        global count  # 声明接下来访问全局的 count
        count += 1
        return count
    
    print(make_counter())  # 1
    print(make_counter())  # 2
    

    但global有个致命的缺陷:它让count变成了模块级的全局变量。这意味着,如果你创建多个make_counter()实例,它们将共享同一个count,计数器之间的状态隔离被完全破坏了。

    四、nonlocal关键字:访问 E 层变量

    nonlocal可以看作是global的近亲,但作用域完全不同。它允许在L层函数中修改E层(即嵌套外层)的变量:

    def make_counter():
        count = 0
    
        def counter():
            nonlocal count  # 声明:接下来对 count 的赋值操作,作用于外层的 count
            count += 1
            return count
    
        return counter
    
    c = make_counter()
    print(c())  # 1
    print(c())  # 2
    print(c())  # 3
    

    关键在于,nonlocal不会在L层创建新变量,也不涉及G层——它直接作用于最近一层外层函数的变量。

    这里简单对比一下global和nonlocal:

    关键字作用层行为
    global模块级 G 层读写全局变量,多个函数共享
    nonlocal嵌套外层 E 层读写外层函数变量,多个闭包独立

    五、闭包的完整执行流程

    理解了nonlocal,我们再完整地看一遍闭包的执行流程。还是最初报错的代码,但这次我们先不加任何关键字:

    def make_counter():
        count = 0  # <- E 层变量
    
        def counter():
            print("count 当前值:", count)  # 读 E 层变量 - 没问题
            return count  # 读 E 层变量 - 没问题
    
        return counter
    

    看到了吗?单纯的读操作(不加nonlocal)是不会报错的——Python允许读取外层变量。只有写操作(比如count = something或count += something)才会触发UnboundLocalError,因为等号左边让Python误以为你要创建一个新的L层变量。

    当Python看到nonlocal count这行声明时,在编译期(生成字节码的阶段)就已经把这件事记录下来了:

    import dis
    
    def make_counter():
        count = 0
    
        def counter():
            nonlocal count
            count += 1
            return count
    
        return counter
    
    # counter 函数的字节码
    dis.dis(counter := make_counter())
    

    关键字节码如下:

    5 2 LOAD_GLOBAL 0 (count)

    4 LOAD_CONST 1 (1)

    6 BINARY_OP 0 (+)

    8 STORE_FAST 0 (count)

    10 LOAD_FAST 0 (count)

    12 RETURN_VALUE

    注意LOAD_GLOBAL 0 (count)这一行,这就是nonlocal的实现方式。如果没有nonlocal声明,这行会变成LOAD_FAST(试图读取L层变量),紧接着的STORE_FAST就会因为变量未定义而触发UnboundLocalError。

    六、闭包变量的生命周期

    闭包有一个容易被忽略但至关重要的特性:闭包变量的生命周期和闭包函数本身一样长。

    def make_multiplier(factor):
        # factor 绑定在 make_multiplier 的局部作用域里
        def multiply(value):
            return value * factor
        return multiply
    
    doubler = make_multiplier(2)
    
    # make_multiplier() 已经执行完毕退出了
    # 但 doubler 仍然持有 factor=2
    print(doubler(5))   # 10
    print(doubler(100)) # 200
    

    factor原本是make_multiplier()的局部变量。按照常规,函数执行完毕,其局部变量就该被销毁了。但doubler还在引用它,所以Python的垃圾回收机制检测到factor仍有外部引用,就把它保留下来,并通过一个叫做cell的对象包装后,存入doubler.__closure__属性中。

    这就是为什么我们能通过doubler.__closure__[0].cell_contents读取到2——cell对象就是闭包变量在内存中的载体。

    >>> doubler.__closure__
    (,)
    >>> doubler.__closure__[0].cell_contents
    2
    

    再看一个复杂点的例子,验证多个闭包如何共享同一个外层变量:

    def processor(initial=0):
        total = initial
    
        def add(x):
            nonlocal total
            total += x
            return total
    
        def subtract(x):
            nonlocal total
            total -= x
            return total
    
        return add, subtract
    
    add, subtract = processor(100)
    
    print(add(30))     # 130,total = 100 + 30
    print(subtract(20)) # 110,total = 130 - 20
    print(add(10))     # 120,total = 110 + 10
    

    add和subtract这两个闭包函数,指向的是同一个cell对象。因此,修改total对两个函数都生效。这种“共享状态”的特性,在事件处理器、回调函数等场景中非常有用。

    七、闭包的典型应用场景

    场景一:函数工厂(最常见用法)

    根据不同的参数,动态生成具有特定功能的函数:

    def power_factory(exp):
        def power(base):
            return base ** exp
        return power
    
    square = power_factory(2)
    cube = power_factory(3)
    
    print(square(5))  # 25
    print(cube(5))    # 125
    

    exp这个参数被捕获在各自的闭包里,square和cube拥有独立的exp值,互不干扰。相比为每种指数都写一个专门的函数,函数工厂的方式显然更灵活、更优雅。

    场景二:带记忆的递归函数

    def memoized_fibonacci():
        cache = {}  # E 层变量
    
        def fib(n):
            if n in cache:
                return cache[n]
            if n <= 1:
                result = n
            else:
                result = fib(n-1) + fib(n-2)
            cache[n] = result
            return result
    
        return fib
    
    fib = memoized_fibonacci()
    print(fib(100))  # 354224848179261915075
    print(fib(200))  # 280571172992510140037611908417314019
    

    cache字典在闭包中持久化,每次递归调用都能访问同一个缓存。这巧妙地避免了普通递归中,子问题被重复计算的巨大开销。

    场景三:装饰器(闭包的直接应用)

    装饰器本质上就是闭包的一个经典应用:

    import functools
    import time
    
    def timing_decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            # fn 和 elapsed_time 都是闭包变量
            start = time.perf_counter()
            result = fn(*args, **kwargs)
            elapsed = time.perf_counter() - start
            print(f"{fn.__name__} 耗时 {elapsed:.4f}s")
            return result
        return wrapper
    

    wrapper函数捕获了两个自由变量:fn(被装饰的函数)和用于计时的变量。timing_decorator返回的wrapper闭包里,装着对被装饰函数的引用。当你调用被装饰后的函数时,实际执行的是这个wrapper。

    八、闭包的常见错误:迟绑定

    这是闭包概念里最隐蔽的一个坑。当闭包在循环中被创建时,所有闭包实例捕获的其实是同一个变量,而这个变量的值,是以闭包被调用时的值为准,而不是创建时的值:

    def create_multipliers():
        multipliers = []
        for i in range(5):
            multipliers.append(lambda x: x * i)  # i 是自由变量
        return multipliers
    
    fns = create_multipliers()
    
    # 全部返回 4*4=16,而不是 0*4, 1*4, 2*4, 3*4, 4*4
    print([fn(4) for fn in fns])  # [16, 16, 16, 16, 16]
    

    问题在于,循环结束时i = 4,而所有闭包引用的是同一个i。所以当它们被调用时,拿到的都是最终的4,结果自然都是4 * 4 = 16。

    解决方法:利用默认参数在闭包创建时立即捕获当前值:

    def create_multipliers_fixed():
        multipliers = []
        for i in range(5):
            multipliers.append(lambda x, i=i: x * i)  # i=i 把当前值绑定为默认值
        return multipliers
    
    fns = create_multipliers_fixed()
    print([fn(4) for fn in fns])  # [0, 4, 8, 12, 16]
    

    在lambda x, i=i: ...中,右边的i是自由变量,在定义时取值为当前的循环变量;左边的i是默认参数,被绑定到lambda的局部作用域。每次循环迭代,当前i的值都被“冻结”进默认参数,之后循环变量再怎么变化,也影响不到已经绑定好的值了。

    使用functools.partial也能达到同样的效果:

    import functools
    
    def create_multipliers_partial():
        multipliers = []
        for i in range(5):
            multipliers.append(functools.partial(lambda x, i: x * i, i=i))
        return multipliers
    

    九、__closure__与自由变量的深度解析

    我们可以通过__code__.co_freevars属性,直接查看一个函数捕获了哪些自由变量:

    def outer(x):
        def inner(y):
            # z 从更外层捕获
            def deeper(z):
                return x + y + z
            return deeper
        return inner
    
    # 查看各层函数的自由变量
    outer_fn = outer(10)
    inner_fn = outer_fn(20)
    deeper_fn = inner_fn(30)
    
    >>> outer_fn.__code__.co_freevars
    ('x',)
    >>> inner_fn.__code__.co_freevars
    ('x', 'y')
    >>> deeper_fn.__code__.co_freevars
    ('x', 'y', 'z')
    
    # __closure__ 的顺序和 co_freevars 一一对应
    >>> deeper_fn.__closure__
    (, , )
    >>> deeper_fn.__closure__[0].cell_contents, \
         deeper_fn.__closure__[1].cell_contents, \
         deeper_fn.__closure__[2].cell_contents
    (10, 20, 30)
    

    co_freevars是字节码层面的元信息,告诉解释器哪些名字是自由变量;而__closure__则是这些自由变量对应的cell对象序列,两者的顺序是完全一致的。

    十、知识点总结

    浅析Python闭包如何捕获自由变量

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Python
    相关文章 更多
    精品专题 更多
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    IntelliJ IDEA macOS版
    IntelliJ IDEA macOS版

    IntelliJ IDEA 是 JetBrains 推出的专业集成开发环境,重点服务于 Java、Kotlin 与 JVM 项目开发。软件集成代码补全、静态检查、重构、调试、测试、版本控制及构建工具,并可借助插件扩展语言和框架支持。

    PyCharm macOS版
    PyCharm macOS版

    JetBrains PyCharm 是面向 Python 开发者的集成开发环境,提供智能补全、代码检查、重构、调试、测试、版本控制及虚拟环境管理功能。Mac 版兼容 Intel 与 Apple 芯片,核心功能可免费使用。

    WebStorm macOS版
    WebStorm macOS版

    JetBrains WebStorm 是面向 JavaScript、TypeScript 和现代 Web 技术的专业集成开发环境,可智能理解项目结构,提供代码补全、重构、错误检查、运行调试、测试及 Git 工具。

    After Effects macOS版
    After Effects macOS版

    Adobe After Effects 是面向动态图形、影视特效与视频合成的专业创作软件。它提供关键帧动画、遮罩抠像、运动跟踪、三维空间、文字动画和表达式等工具,并可与 Premiere Pro、Photoshop、Illustrator

    Audition macOS版
    Audition macOS版

    Adobe Audition 是面向播客、影视后期、音乐制作及内容创作者的专业音频工作站,提供波形精修、多轨录制与混音、频谱编辑、降噪修复、响度匹配和效果处理工具,并可与 Adobe Premiere 协同完成视频声音制作。

    Bridge macOS版
    Bridge macOS版

    Adobe Bridge 是面向摄影师、设计师及内容团队的免费数字资产管理工具,可集中预览照片、视频和 Adobe 项目文件,并利用评级、标签、关键词、元数据、筛选与收藏集完成分类检索。它还支持批量重命名、格式导出、相机导入及 Camera

    Illustrator macOS版
    Illustrator macOS版

    Adobe Illustrator 是 Adobe 推出的专业矢量图形设计软件,适合在 Mac 上制作标志、图标、插画、包装、信息图及印刷版面。它以路径和锚点构建可无损缩放的图形,并提供钢笔、形状、文字、图像描摹、渐变、画板及生成式功能。

    InDesign macOS版
    InDesign macOS版

    Adobe InDesign 是面向印刷品与数字出版物的专业版面设计软件,可在 Mac 上制作书籍、杂志、宣传册、海报、报告及交互式文档。它提供精细的文字样式、网格、母版、长文档管理和印前输出能力。

    Lightroom Classic macOS版
    Lightroom Classic macOS版

    Adobe Lightroom Classic 是面向桌面摄影工作流的照片管理与后期处理软件,可将原始照片保存在本地硬盘,通过目录、关键词、评分和收藏夹高效整理图库,并提供 RAW 冲印、蒙版、镜头校正、批量同步及多格式导出等功能。

    Media Encoder macOS版
    Media Encoder macOS版

    Adobe Media Encoder 是 Adobe 推出的专业音视频编码与转码工具,可通过队列、预设和监视文件夹批量完成格式转换、代理文件创建及多平台成片输出,并与 Premiere Pro、After Effects 等软件紧密协作。

    Photoshop macOS版
    Photoshop macOS版

    Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理软件,提供图层、蒙版、选区、修复、调色、文字排版、智能对象及生成式编辑等能力。Mac 版支持 Intel 与 Apple Silicon 处理器。

    Premiere Pro macOS版
    Premiere Pro macOS版

    Adobe Premiere Pro 是面向专业视频制作的非线性剪辑软件,提供多轨时间线、调色、音频处理、字幕、代理工作流和多格式输出能力,并可与 After Effects、Audition、Photoshop 及 Frame.io 协同

    WINDOWS 更多
    IntelliJ IDEA Windows版
    IntelliJ IDEA Windows版

    JetBrains IntelliJ IDEA 是以 Java、Kotlin 和 JVM 项目为核心的专业集成开发环境,提供智能补全、代码检查、安全重构、调试、测试、版本控制及构建工具集成。统一发行版的核心功能可免费用于商业和非商业项目,

    PhpStorm Windows版
    PhpStorm Windows版

    PhpStorm 是 JetBrains 面向 PHP 与现代 Web 开发打造的专业集成开发环境,提供代码补全、静态检查、重构、调试、测试、数据库、版本控制及前端语言支持。Windows 用户可直接下载安装程序,

    PyCharm Windows版
    PyCharm Windows版

    PyCharm 是 JetBrains 推出的 Python 集成开发环境,提供智能代码补全、错误检查、重构、调试、测试、Git、终端及 Jupyter Notebook 支持。统一版安装后可体验 30 天 Pro 功能,

    WebStorm Windows版
    WebStorm Windows版

    WebStorm 是 JetBrains 面向 JavaScript、TypeScript 和现代 Web 技术打造的集成开发环境。它提供项目级代码分析、智能补全、安全重构、导航搜索,并集成调试、测试、终端、Git 与常用前端工具,

    Photoshop Windows版
    Photoshop Windows版

    Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理工具,可在 Windows 上完成照片修饰、选区抠图、图层合成、文字排版、绘画及生成式编辑。软件采用订阅授权,由 Creative Cloud 管理安装、更新与许

    InDesign Windows版
    InDesign Windows版

    Adobe InDesign 是面向印刷与数字出版的专业页面排版工具,可制作书籍、杂志、宣传册、海报、交互式 PDF 和电子书。它提供精细的文字样式、多页面管理、印前检查及多格式输出能力,并能与 Photoshop、Illustrator

    Illustrator Windows版
    Illustrator Windows版

    Adobe Illustrator 是用于绘制矢量图形、插画、图标、徽标和版式的专业设计工具。它以路径和锚点为核心,作品放大后仍能保持清晰,并提供画板、文字、渐变、图像描摹及精确变换等功能,适合品牌视觉、印刷物料、数字界面和商业插画创作。

    Bridge Windows版
    Bridge Windows版

    Adobe Bridge 是 Adobe 推出的创意资产管理工具,可集中预览、分类和查找照片、视频、PDF及多种设计文件,并支持元数据、关键词、评级、批量重命名和格式导出。它可与 Photoshop、Illustrator 等应用衔接,

    Lightroom Classic Windows版
    Lightroom Classic Windows版

    Adobe Lightroom Classic 是面向 Windows 桌面摄影工作流的照片管理与无损编辑工具,可通过目录集中整理大型图库,并完成 RAW 显影、局部蒙版、批量调整、打印和多格式导出。它强调本地文件管理与精细控制,

    Acrobat Pro Windows版
    Acrobat Pro Windows版

    Adobe Acrobat Pro 是面向专业 PDF 工作的综合解决方案,可在 Windows 中创建、编辑、转换、比较、整理和保护文档,并支持表单、电子签名、扫描件文字识别及敏感信息永久遮盖。订阅还包含网页、移动服务和云端协作能力,

    After Effects Windows版
    After Effects Windows版

    Adobe After Effects 是面向动态图形、动画与影视视觉特效制作的专业工具,可完成关键帧动画、图层合成、遮罩跟踪、抠像、文字动画和二维三维元素整合。Windows 版通过 Adobe Creative Cloud 安装及更新,

    Media Encoder Windows版
    Media Encoder Windows版

    Adobe Media Encoder 是 Adobe 面向视频与音频制作流程提供的专业编码和转码工具,可批量处理媒体文件、创建代理、套用输出预设并监控文件夹自动执行任务。它与 Premiere、After Effects、Audition