第36课:闭包(Closures)及其应用
学习目标
- 理解闭包的本质:能够准确定义闭包,并描述其构成的三个核心要素。
- 掌握闭包的创建与使用:能够编写代码来创建闭包,并解释其工作原理。
- 应用闭包解决实际问题:学会使用闭包实现函数工厂、数据封装(模拟私有状态)等常见模式。
- 理解闭包与装饰器的关系:认识到闭包是理解Python装饰器的重要基础。
核心概念
想象一下,你有一个制造多功能工具的"工厂"(外层函数)。这个工厂在制造工具(内层函数)时,会在工具里悄悄地预存一些特定的配置或材料(外部函数的变量)。即使工厂已经完成了制造(外层函数已执行完毕),被制造出来的工具依然能记住并使用这些预存的东西。这种"带记忆的函数工具",就是闭包。
闭包的正式定义:当一个嵌套函数(内部函数)引用了其外部作用域(外部函数)中的变量,并且外部函数已经执行完毕,如果这个内部函数被返回并仍在使用,那么它与那些被引用的外部变量共同构成的整体,就叫做闭包。
构成闭包的三要素:
- 有一个嵌套的函数(函数里面再定义一个函数)。
- 内部函数引用了外部函数的变量(这个变量不是全局变量,也不是内部函数的局部变量)。
- 外部函数将内部函数作为返回值返回。
让我们通过一个简单的例子来"看到"闭包的这三个要素:
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
double 和 triple 是两个不同的闭包,它们各自"记住"了自己的 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=0 和 step=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")
常见错误
-
循环中的闭包陷阱:在循环内部创建闭包时,如果闭包引用的是循环变量,它们通常会共享变量的最终值,而不是创建时的值。
# 错误示例 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 -
忘记
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 -
混淆返回函数与调用函数:
return func返回的是函数对象本身,而return func()是调用函数并返回其结果。在创建闭包时,我们需要返回函数对象。def outer(): def inner(): print("I am inner") return inner # 正确:返回函数对象 # return inner() # 错误:这会立即执行inner函数,并返回其结果(这里为None)
小结
- 闭包是携带状态的函数:它是一个内部函数,连同它"记住"的外部函数的变量,形成一个封闭的包裹。
- 闭包的形成条件:1. 函数嵌套;2. 内部函数引用外部函数变量;3. 外部函数返回内部函数。
- 核心机制:闭包延长了外部函数局部变量的生命周期,使其在函数调用结束后依然存在。
- 主要应用:
- 数据封装/隐藏:创建私有变量,只通过特定接口访问。
- 函数工厂:根据配置动态生成不同行为的函数。
- 保持状态:如实现计数器、累积器等。
- 与装饰器的关系:装饰器本质上就是接受一个函数作为参数并返回一个新函数的闭包。理解了闭包,就为深入学习装饰器打下了坚实的基础。
下一课,我们将基于对闭包的理解,正式进入正则表达式的奇妙世界,学习如何使用 re 模块进行强大的文本匹配和处理。