Python闭包详解:一文读懂,告别闭包烦恼!

1. Python 闭包基础

闭包是 Python 中一个非常重要的概念,特别在函数式编程中,它能够为我们提供非常灵活的功能。理解闭包的定义、特征及其应用技巧,有助于编写高效、可维护的代码。


1.1. 什么是闭包?

闭包(Closure)是指一个函数可以“记住”并访问其定义时的作用域中的变量,即使在函数外部调用该函数时,依然能够访问到这些变量。

闭包通常是在以下情况下产生的:

  1. 一个函数内部定义了另一个函数。
  2. 内部函数引用了外部函数的变量。
  3. 外部函数返回了内部函数。

闭包的关键在于“函数记住了”它外部函数的局部变量,即使外部函数已经执行完毕。

示例:
def outer_function(x):
    def inner_function(y):
        return x + y
    return inner_function

closure = outer_function(5)  # outer_function 返回了 inner_function
print(closure(3))  # 输出: 8 (因为 5 + 3)

在这个例子中,inner_function 是一个闭包,它能够访问外部函数 outer_function 中的变量 x,即使 outer_function 已经执行完毕。


1.2. 闭包的特征

闭包有以下几个特征:

  1. 外部函数的局部变量:闭包会记住外部函数的局部变量,这使得它能够在外部函数执行完成后仍然访问这些变量。

  2. 可以作为返回值:闭包通常是通过返回一个内部函数来实现的。返回的内部函数包含对外部函数局部变量的引用。

  3. 可扩展性:由于闭包保存了外部函数的变量状态,它能够在后续调用时依赖这些状态,从而提供更灵活的功能。

  4. 自由变量:在闭包中,内嵌函数引用了外部函数的变量,这些被引用的变量被称为自由变量。

示例:
def outer_function(a):
    b = 5
    def inner_function(c):
        return a + b + c
    return inner_function

closure = outer_function(3)  # outer_function 返回了 inner_function
print(closure(2))  # 输出: 10 (因为 3 + 5 + 2)

在这个例子中,ab 是外部函数 outer_function 的局部变量,闭包 inner_function 记住了这些变量,即使外部函数执行完毕,inner_function 依然能够访问 ab

图解

image.png

展示了闭包的完整工作流程:

  1. 外部函数环境(粉色)
  2. outer_function 创建了环境,包含变量 a=3b=5
  3. 这个环境会被闭包保持
  4. 内部函数(蓝色)
  5. inner_function 在外部函数内定义
  6. 通过闭包机制捕获外部变量 ab
  7. 定义了自己的参数 c
  8. 闭包形成
  9. outer_function 执行完返回 inner_function
  10. closure 变量引用了这个返回的函数
  11. 所有被捕获的变量(ab)继续存活
  12. 闭包执行过程
  13. 调用 closure(2)
  14. 参数 c 被设为 2
  15. 访问已保存的 a=3b=5
  16. 计算 3 + 5 + 2 = 10
  17. 关键特性展示
  18. 变量捕获:虚线表示对外部变量的捕获
  19. 状态保持:即使外部函数结束,变量状态依然保持
  20. 数据封装:外部无法直接访问 ab

1.3. 使用闭包的思路和技巧

  1. 延迟计算: 闭包可以延迟计算结果,常用于需要延迟执行的情形,或者需要保留某些状态的场景。

    示例:实现一个简单的计数器
    def counter():
        count = 0
        def increment():
            nonlocal count
            count += 1
            return count
        return increment
    
    count1 = counter()
    print(count1())  # 输出: 1
    print(count1())  # 输出: 2
    

    在这个例子中,闭包 increment 延迟了计数器的执行,每次调用时都能够访问 count,实现了一个简单的计数器。

  2. 函数工厂: 闭包可以用于生成不同的函数。通过将不同的参数传递给外部函数,可以生成带有不同状态的函数。

    示例:创建带有不同加法器的工厂函数
    def make_adder(x):
        def adder(y):
            return x + y
        return adder
    
    add5 = make_adder(5)
    add10 = make_adder(10)
    
    print(add5(3))  # 输出: 8
    print(add10(3))  # 输出: 13
    

    在这个例子中,make_adder 是一个工厂函数,它返回一个闭包 adder,这个闭包记住了 x 的值,可以在后续调用中使用。

  3. 封装数据和隐藏实现细节: 闭包可以帮助我们封装数据并隐藏实现细节,通过创建可配置的函数,提高代码的模块化和可维护性。


1.4. 闭包的注意事项以及可能导致的 Bug

虽然闭包非常强大,但在使用时也需要注意以下几点,以避免潜在的问题:

  1. 作用域问题

  2. 在闭包中,如果使用 nonlocalglobal 关键字来修改外部作用域的变量,可能会引发意外的修改,导致不可预测的结果。
  3. 示例:作用域问题
    def outer_function():
        count = 0
        def inner_function():
            nonlocal count
            count += 1
            return count
        return inner_function
    
    count1 = outer_function()
    print(count1())  # 输出: 1
    print(count1())  # 输出: 2
    

    使用 nonlocal 修饰符时,要小心修改外部函数的变量,避免导致错误的修改。

  4. 闭包的生命周期

  5. 闭包会持有对外部函数变量的引用,可能导致外部函数的变量无法被垃圾回收,尤其是在长时间的函数调用或闭包传递中,可能引发内存泄漏。
  6. 示例:内存泄漏
    def outer_function():
        data = [1, 2, 3]
        def inner_function():
            return data
        return inner_function
    
    closure = outer_function()
    # `data` 会一直被引用,可能导致内存泄漏
    
  7. 调试困难

  8. 由于闭包将外部函数的变量封装到内部函数中,调试时可能不容易察觉。理解每个闭包中的变量作用域及其生命周期非常重要。
  9. 不小心捕获外部变量

  10. 在闭包中,可能无意中捕获外部变量的值,从而导致不必要的副作用,特别是在循环或高阶函数的情况下。
  11. 示例:不小心捕获外部变量
    def make_closures():
        return [lambda x: i + x for i in range(3)]
    
    closures = make_closures()
    print(closures )  # 输出: 5
    print(closures )  # 输出: 5
    print(closures )  # 输出: 5
    

    由于 lambda 表达式捕获了 i 的引用,而不是其值,所有的闭包都使用了 i 最后的值(即 2),导致所有调用结果都相同。

    解决方法:使用默认参数来捕获当前值。

    def make_closures():
        return [lambda x, i=i: i + x for i in range(3)]
    
    closures = make_closures()
    print(closures )  # 输出: 5
    print(closures )  # 输出: 6
    print(closures )  # 输出: 7
    

2. Python闭包典型案例

闭包是 Python 中一种非常有用的编程技巧,它能够让函数记住并访问其外部作用域中的变量,甚至在外部函数已经返回之后。闭包在多个场景中都有广泛应用,下面是一些典型的闭包应用案例。


2.1. 函数工厂(Factory Function)

闭包可以作为工厂函数的一个重要工具,用于生成带有特定状态或行为的函数。通过使用闭包,可以根据不同的输入创建不同的函数。

示例:生成加法器

假设我们需要生成多个加法器函数,每个加法器加一个特定的数字。通过闭包,我们可以根据需要生成这些加法器。

def make_adder(x):
    # 返回一个闭包,闭包记住了外部函数的参数 `x`
    def adder(y):
        return x + y
    return adder

# 使用闭包生成不同的加法器
add5 = make_adder(5)
add10 = make_adder(10)

print(add5(3))   # 输出: 8
print(add10(3))  # 输出: 13

在这个例子中,make_adder 是一个工厂函数,它生成一个闭包 adder,每个 adder 都记住了其 x 的值,可以返回相应的加法结果。


2.2. 计数器

闭包可以用来创建有状态的函数,像计数器这样的功能就是一个典型的应用。通过闭包,计数器可以保留其内部的计数状态,每次调用时更新计数值。

示例:计数器
def counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

# 创建计数器
counter1 = counter()
counter2 = counter()

print(counter1())  # 输出: 1
print(counter1())  # 输出: 2
print(counter2())  # 输出: 1

在这个例子中,counter 函数生成一个闭包 increment,它保留了 count 变量的状态,每次调用时都会增加计数值。通过 nonlocal 关键字,闭包能够修改外部函数的局部变量 count


2.3. 延迟执行

闭包非常适合用于延迟执行的场景。通过闭包,可以在未来某个时刻调用函数并执行某些操作,而不需要立即执行。

示例:延迟打印
def delayed_print(message):
    def printer():
        print(message)
    return printer

delayed = delayed_print("Hello, World!")
delayed()  # 输出: Hello, World!

这里,delayed_print 函数返回一个闭包 printer,该闭包记住了 message 变量,直到 delayed() 被调用时,闭包才会打印信息。


2.4. 缓存和记忆化(Memoization)

闭包可以用于缓存计算结果,从而避免重复计算,特别是在递归问题中非常有效。通过记忆化,可以显著提升递归算法的效率。

示例:斐波那契数列的记忆化
def fibonacci(n, memo={}):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)
    return memo[n]

print(fibonacci(50))  # 输出: 12586269025

在这个例子中,fibonacci 使用了闭包中的 memo 字典来缓存已经计算过的 Fibonacci 数字,避免了重复计算,提高了性能。


2.5. 装饰器

闭包是实现装饰器(Decorator)的关键技术,装饰器允许我们在不修改原函数的情况下动态地增加函数的功能。装饰器的实现本质上是通过闭包实现的。

示例:简单的装饰器
def decorator(func):
    def wrapper():
        print("Function is about to be called")
        func()
        print("Function has been called")
    return wrapper

@decorator
def say_hello():
    print("Hello!")

say_hello()

输出:

Function is about to be called
Hello!
Function has been called

在这个例子中,decorator 是一个闭包,它返回了一个 wrapper 函数,wrapper 在执行原函数之前和之后都添加了一些额外的操作。


总结

闭包是 Python 中一种非常强大的技术,它允许函数在执行时“记住”其外部作用域的变量。典型的闭包应用场景包括:

  1. 函数工厂:生成具有不同状态或行为的函数。
  2. 计数器:创建具有内存状态的函数。
  3. 延迟执行:延迟某些操作的执行。
  4. 缓存与记忆化:缓存计算结果,避免重复计算,提升效率。
  5. 装饰器:通过闭包为函数动态增加功能。

闭包不仅是函数式编程的核心概念,而且在 Python 中的使用非常广泛,能够帮助我们写出更灵活、简洁的代码。

3. 深入理解Python闭包

要深入了解 Python 中闭包的底层原理,我们需要探讨几个方面:函数的定义、作用域的查找规则、自由变量的存储、闭包的内存管理机制以及 Python 如何利用这些机制实现闭包。

1. 函数对象的创建与执行

在 Python 中,函数本质上是对象。当你定义一个函数时,Python 会为其创建一个函数对象。这个函数对象不仅包含代码(即函数体),还包含它的作用域信息。

例如:

def outer(x):
    def inner(y):
        return x + y
    return inner

当执行 outer(10) 时,Python 会创建一个 outer 函数对象,并将 x = 10 作为参数传入。同时,inner 函数也会被定义,并绑定在 outer 函数的局部作用域中。注意到 inner 函数内部引用了外部函数 outer 的局部变量 x

2. 作用域与闭包

在 Python 中,每个函数都有自己的作用域(local scope)。当函数内部引用变量时,Python 会首先查找该变量是否在当前作用域内。如果找不到,Python 会按照作用域链(LEGB:Local → Enclosing → Global → Built-in)依次向外层作用域查找。

对于闭包来说,inner 函数引用了 x 变量,这意味着 xinner 函数的自由变量(free variable)。然而,inner 函数并没有在自己的作用域中定义 x,它是来自 outer 函数的局部变量。Python 将 x 的值“记住”并与 inner 函数绑定在一起,从而形成了闭包。

3. 自由变量的存储与 __closure__ 属性

Python 为了实现闭包,会将自由变量(即 inner 函数内部引用但未定义的变量)保存在函数对象的 __closure__ 属性中。__closure__ 是一个元组,其中包含了所有自由变量的引用,这些变量存储在 cell 对象中。

回到我们之前的例子:

def outer(x):
    def inner(y):
        return x + y
    return inner

closure = outer(10)

在执行 outer(10) 时,Python 会创建一个 inner 函数,并将 x = 10 这个自由变量与 inner 绑定。然后,outer 返回的是 inner,此时 inner 就成为了一个闭包,保留了对 x 的引用。

可以通过 __closure__ 属性查看闭包中保存的自由变量:

print(closure.__closure__)  # 输出:(<cell at 0x7fc8b7b62d70: int object at 0x7fc8b7987ac0>,)
print(closure.__closure__[0].cell_contents)  # 输出:10

此时,closure.__closure__ 是一个元组,里面的每一个元素是一个 cell 对象,cell 对象包含了变量 x 的值。

4. 延迟绑定(Late Binding)与闭包的行为

在 Python 中,闭包有一个延迟绑定的特性。即,闭包内部的变量不是在函数定义时立即绑定的,而是在函数调用时才会进行绑定。

延迟绑定的含义:

  • 由于闭包保存了对外部作用域变量的引用(而非该变量的值),因此,闭包中的自由变量是按需计算的
  • 这意味着闭包能够记住外部函数调用时的参数,而不仅仅是保存当时的值。
  • 为了更好地理解延迟绑定的影响,我们来看一个常见的例子:

    def outer():
        return [lambda x: i * x for i in range(5)]
    
    funcs = outer()
    print([f(2) for f in funcs])  # 输出:[8, 8, 8, 8, 8]
    

    为什么结果是 [8, 8, 8, 8, 8] 而不是 [0, 2, 4, 6, 8]

    在上面的代码中,outer() 返回一个包含 5 个 lambda 函数的列表。每个 lambda 函数都会使用 i 这个外部变量。然而,lambda 函数并不会立即计算 i * x 的值,而是创建了一个闭包,保存了对 i 变量的引用。

    当我们执行 [f(2) for f in funcs] 时,所有的 lambda 函数都引用了同一个变量 i。因此,当 outer() 返回的所有函数被调用时,i 的最终值是 4(因为 range(5) 结束时 i 的值为 4)。所以所有的 lambda 函数都会计算 i * 2,结果是 8

    5. 闭包的内存管理

    Python 通过 引用计数垃圾回收机制 来管理闭包的内存。当函数被调用时,Python 会为其创建一个栈帧。闭包会将对外部变量的引用保存在 cell 对象中,而不是直接保存变量的值。这样,即使外部函数执行结束,闭包内部仍然能够访问这些外部变量。

    当闭包不再被引用时,cell 对象和闭包所引用的外部变量会被自动垃圾回收。

    6. 闭包与装饰器

    闭包常常用于实现装饰器。装饰器本质上是一个函数,它接受一个函数并返回一个新的函数,这个新函数通常会调用原始函数,同时在原始函数调用前后增加一些额外的行为。装饰器的实现依赖于闭包来保存对原始函数的引用。

    例如:

    def decorator(func):
        def wrapper(*args, **kwargs):
            print("Before calling the function.")
            result = func(*args, **kwargs)
            print("After calling the function.")
            return result
        return wrapper
    
    @decorator
    def say_hello(name):
        print(f"Hello, {name}")
    
    say_hello("Alice")
    

    在这个例子中,wrapper 函数是一个闭包,它通过 decorator 保存了对 say_hello 函数的引用。闭包使得装饰器能够在不修改原函数的代码的情况下,为其添加额外的功能。

    总结

  • 闭包的定义:闭包是一个函数,它“记住”了定义时的外部变量,这些变量被称为自由变量。
  • 自由变量与 __closure__:Python 使用 __closure__ 属性保存自由变量的引用。
  • 延迟绑定:闭包对外部变量的引用是延迟绑定的,即在使用时才计算变量的值。
  • 内存管理:闭包通过引用计数和垃圾回收机制管理内存,当闭包不再被引用时,内存会被释放。
  • 应用:闭包常用于实现装饰器、回调函数等。
  • 闭包在 Python 中是非常强大的编程工具,了解其底层原理有助于更好地利用其特性,编写更简洁、优雅的代码。

    作者:AI Agent首席体验官

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python闭包详解:一文读懂,告别闭包烦恼!

    发表回复