Python闭包:深入理解与应用

Python基础:闭包


文章目录

  • Python基础:闭包
    • 一、知识点详解
      • 1.1 闭包是什么?
      • 1.2 核心概念
        • 1. 词法作用域
        • 2. 自由变量
      • 1.3 闭包的结构
      • 1.4 闭包三要素
      • 1.5 闭包的关键特性
      • 1.6 闭包的典型应用场景
      • 1.7 注意事项
    • 二、说明示例
      • 场景1 : 状态保持
      • 场景2 : 函数工厂
    • 三、知识点总结
    • 四、扩展知识
      • 4.1 易混淆知识(`闭包` VS `闭包函数`)
      • 4.2 常见误区
      • 4.3 循环变量引用问题
    • 五、知识点考察题


一、知识点详解

1.1 闭包是什么?

闭包是函数式编程里的关键概念,它由函数和其词法环境共同构成。
简单来讲,闭包是一个(内层)函数与其相关外部变量的组合

通俗理解
闭包就是一个能「记住」自己出生环境的函数。

核心特性
即便外部函数执行完毕,内部函数依旧能够访问和修改外部函数的变量。

1.2 核心概念

1. 词法作用域

专业解释

变量的可见性由代码的 静态结构 决定,并非运行时的调用链。函数在定义时就确定了其作用域链。

简单理解
变量能被使用的范围(词法作用域),就像你写代码时给它 “划了一个圈”:
圈画在哪里(代码结构),变量就在哪里生效,永远不变。
和之后 “怎么跑程序”(比如函数被调用的顺序、位置)无关。
这样理解,就像 “出生地决定籍贯” 一样,一旦确定,终身不变~

2. 自由变量

本质定义
在函数中被使用,但既不是该函数的参数,也不是其局部变量的变量。
闭包函数通过 __closure__ 属性存储这些变量的引用。

类比理解

  • 自由变量 → 图书馆书架上的书籍
  • 闭包函数 → 办理了借书手续的读者
  • 在图书馆(外层函数)开放时,读者(闭包函数)可以在馆内借阅书籍(自由变量)。当图书馆闭馆(外层函数结束),读者凭借之前办理的借书证(变量引用),仍能继续合法使用所借的书籍。

1.3 闭包的结构

闭包的典型结构为 嵌套函数,也就是在一个函数内部定义另一个函数,并且内部函数引用了外部函数的变量。
示例如下:

def outer():
    outer_var = "数据"

    def inner():
        print(outer_var)

    return inner


closure = outer()
closure()  # 输出:数据 

解析

外层函数 outer()

  • 定义局部变量 outer_var = "数据",这是一个 自由变量(即被内层函数引用但未在内层函数中定义的变量)。
  • 嵌套定义内层函数 inner(),该函数引用了外层变量 outer_var
  • 返回内层函数 inner(注意返回的是函数对象的 引用,不能加括号)。

闭包的形成

  • closure = outer() 执行后,outer() 结束,其作用域理论上应销毁。
  • 但由于 inner 引用了 outer_var,Python 会通过闭包机制将 outer_var 绑定到 inner__closure__ 属性中,使其持久化。

闭包的调用

  • closure() 执行时,仍能访问 outer_var,输出 "数据"
  • 这是因为闭包保留了外层函数的环境(即 outer_var 的值)。

1.4 闭包三要素

  1. 函数嵌套:必须存在一个外层函数,其内部嵌套定义另一个函数(内层函数)。
  2. 内层函数引用外层变量:内层函数必须引用外层函数的局部变量或参数(自由变量)。
  3. 外层函数返回内层函数:外层函数需将内层函数作为返回值返回(不带括号)。

1.5 闭包的关键特性

  1. 自由变量的捕获
    闭包通过 __closure__ 属性存储自由变量(如 outer_var),每个变量以 cell 对象形式保存,可通过 closure.__closure__[0].cell_contents 查看。

    print(closure.__closure__)
    # 输出:
    print(closure.__closure__[0].cell_contents)
    # 输出:数据
    
  2. 作用域与生命周期
    外层函数的局部变量通常会随函数结束而销毁,但闭包会延长其生命周期。
    内层函数的作用域链包含外层函数的命名空间,形成 词法作用域(静态作用域),与调用位置无关,即函数定义时的作用域决定变量查找规则,而非调用时的环境。

  3. 修改自由变量
    若需在闭包中修改外层变量,需使用 nonlocal 声明,否则会被视为创建新局部变量。

    def test():
        x = 1
    
        def inner():
            nonlocal x
            x = 2
            print(x)
    
        return inner
    
    
    test()()  # 输出:2
    

1.6 闭包的典型应用场景

  • 状态保持:例如计数器、缓存机制,可避免全局变量污染。
  • 函数工厂:能够动态生成定制化函数(如 multiplier(factor))。
  • 装饰器:基于闭包实现功能增强(如日志记录、权限校验)。
  • 回调函数:封装上下文信息供异步调用。

1.7 注意事项

  • 内存占用问题:闭包长期持有变量引用可能导致内存无法释放,需手动解除(如 closure = None)。
    def outer():
        data = [1, 2, 3]  # 外部变量
        def inner():
            print(data)
        return inner
    
    closure = outer()  # 创建闭包
    closure()  # 输出: [1, 2, 3]
    
    # 手动解除引用
    closure = None  # 此时闭包和外部变量可以被垃圾回收
    
  • 循环变量引用问题
    在循环中创建闭包时,若闭包直接引用循环变量(如 for i 中的 i),所有闭包会共享最终值。
    解决方法是传递循环变量为默认参数。(详情见下文扩展知识模块)

二、说明示例

场景1 : 状态保持

def counter():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment


counter_func = counter()  # 返回函数increment
print(counter_func())  # 1
print(counter_func())  # 2

场景2 : 函数工厂

def tool_factory(name):
    def tool(action):
        print(f"{name}执行:{action}")

    return tool


hammer = tool_factory("锤子")
screwdriver = tool_factory("螺丝刀")

hammer("敲钉子")  # 锤子执行:敲钉子
screwdriver("拧螺丝")  # 螺丝刀执行:拧螺丝

三、知识点总结

  1. 闭包概念
    由函数和其词法环境构成,是函数与其相关外部变量的组合,能记住出生环境,内部函数可访问修改外部函数变量。

  2. 核心概念
    词法作用域:变量可见性由代码静态结构决定,函数定义时确定作用域链。
    自由变量:函数中使用的非参数和局部变量,闭包函数通过__closure__属性存储引用。

  3. 闭包结构
    典型为嵌套函数,内部函数引用外部函数变量,外部函数返回内部函数引用。

  4. 闭包三要素
    函数嵌套、内层函数引用外层变量、外层函数返回内层函数。

  5. 闭包特性
    自由变量捕获:通过__closure__属性存储自由变量,以cell对象形式保存。
    作用域与生命周期:延长外层函数局部变量生命周期,内层函数作用域链包含外层命名空间。
    修改自由变量:需用nonlocal声明,否则视为创建新局部变量。


四、扩展知识

4.1 易混淆知识(闭包 VS 闭包函数)

在闭包的概念中,闭包函数特指“内层函数”,但它必须满足一个关键条件:
内层函数引用了外层函数作用域中的变量,并且外层函数将这个内层函数作为返回值输出。
而这个被返回的内层函数 + 其使用的外部变量的组合才被称为闭包。

例如

def outer(x):
    # 自由变量x将被内层函数捕获,成为闭包的一部分
    def inner(y):
        # 此处inner是闭包函数
        # 它引用了外层作用域中的变量x,满足闭包形成条件
        return x + y
    # 将闭包函数(inner)作为返回值,此时 闭包 尚未完全形成
    return inner  

# 创建 闭包 的时刻:inner函数与 x=10 的组合
# 此时闭包 = inner函数 + 被捕获的x=10
closure_func = outer(10)  

# 验证闭包中的捕获变量
print(closure_func.__closure__[0].cell_contents)  # 输出:10

# 调用闭包函数(closure function)
# 闭包函数执行时会同时使用:
# 1. 被捕获的外部变量(x=10)
# 2. 当前传入的参数(y=5)
print(closure_func(5))  # 输出15

在以上示例中

  • 闭包函数
    指的是被返回的内层函数(inner()
  • 闭包
    指整个闭包函数和其捕获的外部变量的组合( inner + 外部变量 x 的组合

4.2 常见误区

外层函数返回的是“闭包”而非内层函数本身?

  • 不是。外层函数返回的就是内层函数,但此时这个内层函数已经成为闭包,因为它携带了外部变量的引用。
    换句话说:当内层函数满足“引用外层变量 + 被外层返回”这两个条件时,它就被称为闭包函数

4.3 循环变量引用问题

在Python中,闭包直接引用循环变量会导致所有闭包共享循环变量的最终值,这是一个常见的易错点。

问题代码示例

def create_closures():
    closures = []
    for i in range(3):
        def closure():
            return i * i  # 直接引用循环变量i
        closures.append(closure)
    return closures

# 获取闭包列表
cl1, cl2, cl3 = create_closures()

# 调用闭包
print(cl1(), cl2(), cl3())  # 输出: 4 4 4(不是预期的0 1 4)

问题原因
所有闭包共享同一个变量i的引用,而i在循环结束后值为 2(range(3)生成0,1,2),因此所有闭包返回2*2=4

解决方法:传递循环变量为默认参数
通过将循环变量作为默认参数绑定到闭包中,可以捕获循环变量的当前值:

方法1:使用嵌套函数

def create_closures_fixed():
    closures = []
    for i in range(3):
        def make_closure(j):  # 嵌套函数接收当前i值
            def closure():
                return j * j  # 绑定到嵌套函数的局部变量i
            return closure
        closures.append(make_closure(i))  # 立即传递当前i值
    return closures

# 测试
cl1, cl2, cl3 = create_closures_fixed()
print(cl1(), cl2(), cl3())  # 输出: 0 1 4

方法2:直接使用默认参数

def create_closures_simple():
    closures = []
    for i in range(3):
        def closure(j=i):  # 将i作为默认参数绑定
            return j * j
        closures.append(closure)
    return closures

# 测试
cl1, cl2, cl3 = create_closures_simple()
print(cl1(), cl2(), cl3())  # 输出: 0 1 4

关键点

  • 默认参数在函数定义时求值,因此每个闭包会保存当前i的副本。
  • 这种方法避免了直接引用循环变量,确保每个闭包独立持有自己的值。

对比说明

方法 输出结果 原理
直接引用循环变量 4, 4, 4 所有闭包共享同一个i的引用,最终值为2。
嵌套函数或默认参数 0, 1, 4 每个闭包捕获循环变量的当前值,形成独立的绑定。

说明:若需修改闭包外的变量,需使用nonlocal声明(Python 3+)或使用可变对象(如列表等可直接修改操作)。


五、知识点考察题

funcs = [lambda x, y=i: x + y for i in range(3)]

(多选题)以下操作会报错的是( )

  • A. print(funcs)
  • B. print(funcs[0](1))
  • C. print(funcs(1)[1])
  • D. print(funcs[3](1))

答案
会报错的选项:C、D




关注「安于欣」获取更多Python技巧


你可能感兴趣的:(Python学习笔记,python,开发语言)