展开菜单
首页 精品内容 本月促销 装机必备 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软件 更多
    讯飞智作 网页版
    讯飞智作 网页版

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw macOS版
    AionClaw macOS版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy macOS版
    Loomy macOS版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。

    AirBuddy 3 macOS版
    AirBuddy 3 macOS版

    AirBuddy 3 是面向 macOS 的蓝牙外设增强工具,可集中查看 AirPods、Beats 与苹果设备电量,提供快速连接、播放控制、电量提醒、桌面小组件和多台 Mac 外设接力等功能。

    AirServer macOS版
    AirServer macOS版

    AirServer macOS版可把 Mac 转换为无线投屏接收端,通过 AirPlay 或 Google Cast 接收手机、平板和电脑画面,并支持多设备同时连接、高清镜像及会话录制,适合家庭娱乐、课堂教学、会议演示和应用展示。

    AnyDesk macOS版
    AnyDesk macOS版

    AnyDesk macOS版是一款适用于远程办公、设备维护和客户支持的远程桌面工具,可连接并操作异地计算机,支持文件传输、无人值守访问、会话权限控制及跨平台协作。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。

    讯飞绘文 网页版
    讯飞绘文 网页版

    讯飞绘文网页版是科大讯飞推出的AI图文创作与内容运营工具,覆盖热点选题、文章生成、稿件校对、智能配图、排版发布和数据复盘等环节,适合自媒体创作者及内容团队在线使用。

    Maya(Autodesk Maya) Windows版
    Maya(Autodesk Maya) Windows版

    Autodesk Maya 是面向影视、电视、动画和游戏行业的专业三维制作软件,集多边形与 NURBS 建模、角色绑定、关键帧和动作捕捉动画、Bifrost 特效模拟、材质开发及 Arnold 渲染于一体,

    Aperty Windows版
    Aperty Windows版

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    立刻MV 网页版
    立刻MV 网页版

    立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

    蛙蛙写作 网页版
    蛙蛙写作 网页版

    蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

    超级简历WonderCV 网页版
    超级简历WonderCV 网页版

    超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

    音述AI 网页版
    音述AI 网页版

    音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

    AionClaw Windows版
    AionClaw Windows版

    AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

    Loomy Windows版
    Loomy Windows版

    Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。