36·模块八:装饰器与高级主题高级

闭包(Closures)及其应用

closuresscope

第36课:闭包(Closures)及其应用

学习目标

  • 理解闭包的本质:能够准确定义闭包,并描述其构成的三个核心要素。
  • 掌握闭包的创建与使用:能够编写代码来创建闭包,并解释其工作原理。
  • 应用闭包解决实际问题:学会使用闭包实现函数工厂、数据封装(模拟私有状态)等常见模式。
  • 理解闭包与装饰器的关系:认识到闭包是理解Python装饰器的重要基础。

核心概念

想象一下,你有一个制造多功能工具的"工厂"(外层函数)。这个工厂在制造工具(内层函数)时,会在工具里悄悄地预存一些特定的配置或材料(外部函数的变量)。即使工厂已经完成了制造(外层函数已执行完毕),被制造出来的工具依然能记住并使用这些预存的东西。这种"带记忆的函数工具",就是闭包

闭包的正式定义:当一个嵌套函数(内部函数)引用了其外部作用域(外部函数)中的变量,并且外部函数已经执行完毕,如果这个内部函数被返回并仍在使用,那么它与那些被引用的外部变量共同构成的整体,就叫做闭包。

构成闭包的三要素

  1. 有一个嵌套的函数(函数里面再定义一个函数)。
  2. 内部函数引用了外部函数的变量(这个变量不是全局变量,也不是内部函数的局部变量)。
  3. 外部函数将内部函数作为返回值返回

让我们通过一个简单的例子来"看到"闭包的这三个要素:

def outer_function(): # 外部函数
    message = "Hello" # 外部函数的局部变量

    def inner_function(): # 1. 嵌套函数
        # 2. 内部函数引用了外部函数的变量 `message`
        print(message)

    # 3. 返回内部函数本身(注意:没有括号,不是调用)
    return inner_function

# 调用外部函数,得到的是返回的内部函数
my_func = outer_function()
# 此时,outer_function 已经执行完毕,其局部变量 `message` 理应"消失"。
# 但当我们调用 my_func 时...
my_func() # 输出: Hello

这里,my_func(即 inner_function)和它引用的 message 变量一起,构成了一个闭包。inner_function "记住"了它被创建时的环境(message 的值),这就是闭包的魔力。

代码示例

示例1:闭包基础 - 计数器

一个经典的例子,用闭包实现一个可以计数的函数。

def make_counter():
    count = 0  # 被内部函数引用的外部变量

    def counter():  # 嵌套的内部函数
        nonlocal count  # 声明我们要修改外部函数的变量
        count += 1
        return count

    return counter  # 返回内部函数

# 创建两个独立的计数器
counter1 = make_counter()
counter2 = make_counter()

print(f"Counter1: {counter1()}, {counter1()}, {counter1()}") # 输出: Counter1: 1, 2, 3
print(f"Counter2: {counter2()}, {counter2()}")             # 输出: Counter2: 1, 2

# 每个计数器都独立维护自己的 `count` 状态,互不影响。
# `count` 变量对外部世界是隐藏的(封装),但通过闭包访问。

示例2:函数工厂

闭包可以用来生成一系列功能相似但配置不同的函数。

def make_multiplier(factor):
    """一个创建乘法函数的工厂"""
    def multiplier(number):
        # 引用了外部函数的变量 `factor`
        return number * factor
    return multiplier

# 使用工厂制造不同的"乘法器工具"
double = make_multiplier(2)  # 创建一个乘以2的函数
triple = make_multiplier(3)  # 创建一个乘以3的函数

print(double(5))  # 输出: 10
print(triple(5))  # 输出: 15
print(double(10)) # 输出: 20

doubletriple 是两个不同的闭包,它们各自"记住"了自己的 factor(2和3)。

示例3:模拟私有变量(数据封装)

闭包可以用来隐藏数据,只通过特定的函数接口来访问和修改它,这类似于面向对象编程中的私有属性。

def bank_account(initial_balance):
    balance = initial_balance  # 我们希望隐藏的"私有"数据

    def deposit(amount):
        nonlocal balance
        balance += amount
        print(f"Deposited ${amount}. New balance: ${balance}")

    def withdraw(amount):
        nonlocal balance
        if amount > balance:
            print("Insufficient funds!")
        else:
            balance -= amount
            print(f"Withdrew ${amount}. New balance: ${balance}")

    def get_balance():
        return balance

    # 返回一个包含操作函数的字典(或返回一个操作函数)
    return {
        'deposit': deposit,
        'withdraw': withdraw,
        'get_balance': get_balance
    }

# 创建账户,初始余额100
my_account = bank_account(100)

# 通过提供的接口操作账户
my_account['deposit'](50)    # 输出: Deposited $50. New balance: $150
my_account['withdraw'](30)   # 输出: Withdrew $30. New balance: $120
print(my_account['get_balance']()) # 输出: 120

# 我们无法直接访问 `balance` 变量
# print(balance) # 这会引发 NameError,变量被"封装"了

实践练习

练习1:基础闭包

创建一个闭包 greeting_generator,它接受一个问候语(如 "Hello")作为参数,并返回一个函数。返回的函数接受一个名字作为参数,并输出完整的问候,如 "Hello, Alice"。

# 你的代码
greet_hello = greeting_generator("Hello")
greet_hi = greeting_generator("Hi")

print(greet_hello("Alice")) # 预期输出: Hello, Alice
print(greet_hi("Bob"))     # 预期输出: Hi, Bob

练习2:闭包实现计数器(带初始值和步长)

编写一个函数 make_counter,它接受两个可选参数:start=0step=1。它返回一个闭包函数。每次调用该闭包函数,它都会返回当前的计数值,并按 step 增加。

# 你的代码
c1 = make_counter()          # 从0开始,步长1
c2 = make_counter(start=10)  # 从10开始,步长1
c3 = make_counter(step=5)    # 从0开始,步长5

print(c1(), c1(), c1()) # 预期输出: 0 1 2
print(c2(), c2())       # 预期输出: 10 11
print(c3(), c3(), c3()) # 预期输出: 0 5 10

练习3:高级应用 - 带缓存的函数

编写一个函数 cached_function,它接受一个计算函数(例如计算斐波那契数)作为参数。cached_function 返回一个闭包,该闭包具有记忆功能,会缓存之前计算过的结果,避免重复计算。

import time

def slow_fibonacci(n):
    """一个很慢的递归斐波那契函数,用于演示"""
    if n < 2:
        return n
    time.sleep(0.5) # 模拟耗时计算
    return slow_fibonacci(n-1) + slow_fibonacci(n-2)

# 使用你的 cached_function 包装它
fast_fib = cached_function(slow_fibonacci)

# 第一次计算 fib(5) 会很慢
start = time.time()
result1 = fast_fib(5)
print(f"fib(5) = {result1}, Time: {time.time() - start:.2f}s")

# 第二次计算 fib(5) 应该瞬间完成(从缓存中读取)
start = time.time()
result2 = fast_fib(5)
print(f"fib(5) = {result2}, Time: {time.time() - start:.2f}s")

# 计算新的值 fib(6) 会比较慢,但 fib(6) = fib(5) + fib(4),其中 fib(5) 已缓存
start = time.time()
result3 = fast_fib(6)
print(f"fib(6) = {result3}, Time: {time.time() - start:.2f}s")

常见错误

  1. 循环中的闭包陷阱:在循环内部创建闭包时,如果闭包引用的是循环变量,它们通常会共享变量的最终值,而不是创建时的值。

    # 错误示例
    functions = []
    for i in range(3):
        def func():
            return i  # 这里的 `i` 是外层作用域的 `i`
        functions.append(func)
    
    # 你以为输出 0, 1, 2,但实际上输出 2, 2, 2
    for f in functions:
        print(f())  # 输出: 2, 2, 2
    
    # 正确示例:使用默认参数来"捕获"当前值
    functions = []
    for i in range(3):
        def func(i=i):  # 将当前的 `i` 作为默认参数
            return i
        functions.append(func)
    
    for f in functions:
        print(f())  # 输出: 0, 1, 2
    
  2. 忘记 nonlocal 声明:当内部函数需要修改外部函数的不可变类型变量(如数字、字符串、元组)时,必须使用 nonlocal 关键字声明。对于可变类型(如列表、字典),可以直接修改,但重新赋值仍需 nonlocal

    def make_adder(x):
        def adder(y):
            # x = x + y  # 错误!UnboundLocalError: local variable 'x' referenced before assignment
            nonlocal x  # 需要声明
            x = x + y
            return x
        return adder
    
    add5 = make_adder(5)
    print(add5(3))  # 输出: 8
    
  3. 混淆返回函数与调用函数return func 返回的是函数对象本身,而 return func() 是调用函数并返回其结果。在创建闭包时,我们需要返回函数对象。

    def outer():
        def inner():
            print("I am inner")
        return inner    # 正确:返回函数对象
        # return inner() # 错误:这会立即执行inner函数,并返回其结果(这里为None)
    

小结

  • 闭包是携带状态的函数:它是一个内部函数,连同它"记住"的外部函数的变量,形成一个封闭的包裹。
  • 闭包的形成条件:1. 函数嵌套;2. 内部函数引用外部函数变量;3. 外部函数返回内部函数。
  • 核心机制:闭包延长了外部函数局部变量的生命周期,使其在函数调用结束后依然存在。
  • 主要应用
    • 数据封装/隐藏:创建私有变量,只通过特定接口访问。
    • 函数工厂:根据配置动态生成不同行为的函数。
    • 保持状态:如实现计数器、累积器等。
  • 与装饰器的关系:装饰器本质上就是接受一个函数作为参数并返回一个新函数的闭包。理解了闭包,就为深入学习装饰器打下了坚实的基础。

下一课,我们将基于对闭包的理解,正式进入正则表达式的奇妙世界,学习如何使用 re 模块进行强大的文本匹配和处理。

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「正则表达式基础与 re 模块」 以巩固所学知识。