当前位置: 首页 > ds >正文

Python 闭包(Closure)实战总结

文章目录

    • 一、什么是闭包?一句话说清
    • 二、闭包的作用和使用场景
      • 1. 数据封装与状态保持(替代简单的类)
      • 2. 实现装饰器
      • 3. 函数工厂
    • 三、`nonlocal` 关键字的重要性
    • 四、一个常见的“坑”:循环中的闭包
    • 总结

在日常的 Python 开发中,我们经常会谈到装饰器、回调函数、函数式编程等概念,而它们背后都离不开一个核心的特性—— 闭包(Closure)。起初,闭包可能听起来有些抽象,但一旦理解了它的本质,你会发现它是一个非常强大且优雅的工具,能让你的代码更加简洁和高效。

这篇文章是我在工作中对闭包的一些理解和应用总结,希望能帮助你彻底搞懂它。

一、什么是闭包?一句话说清

抛开复杂的定义,我们可以这样理解:

闭包就是一个“记住”了自己创建时所在环境的函数。

即便创建它的外部函数已经执行完毕,闭包依然可以访问和操作那些外部函数中的变量。

要构成一个闭包,必须满足以下三个条件:

  1. 函数嵌套:存在一个外部函数,内部定义了另一个函数。
  2. 内部函数引用外部变量:内部函数必须引用了外部函数作用域中的变量(非全局变量)。
  3. 外部函数返回内部函数:外部函数最终返回的是内部函数对象,而不是调用它。

让我们来看一个最经典的例子:

def outer_func(x):# x 是外部函数的局部变量,也叫“自由变量”def inner_func(y):# inner_func 引用了外部的变量 xreturn x + y# 返回内部函数对象return inner_func# 调用外部函数,得到一个闭包
add_5 = outer_func(5)
add_10 = outer_func(10)# 执行闭包
print(f"add_5(1) 的结果是: {add_5(1)}")  # 输出: 6
print(f"add_10(2) 的结果是: {add_10(2)}") # 输出: 12

在这个例子中,add_5add_10 就是闭包。当 outer_func(5) 执行完毕后,变量 x=5 并没有消失,而是被“绑定”到了 add_5 这个函数上。

我们可以通过 __closure__ 这个特殊属性来验证,它会告诉你闭包捕获了哪些自由变量:

print(add_5.__closure__)
# (<cell at 0x...: int object at 0x...>,)
print(add_5.__closure__[0].cell_contents)
# 5

二、闭包的作用和使用场景

理解了概念,那么闭包在实际工作中有什么用呢?

1. 数据封装与状态保持(替代简单的类)

闭包提供了一种轻量级的方式来封装数据和行为,避免使用全局变量,同时又不像定义一个完整的类那么“重”。

想象一下,你需要一个计数器:

def make_counter():count = 0  # 这个状态被封装在闭包中def counter():nonlocal count # 声明 count 不是局部变量count += 1return countreturn countercounter1 = make_counter()
print(counter1())  # 1
print(counter1())  # 2counter2 = make_counter() # 创建一个新的、独立状态的计数器
print(counter2())  # 1

count 变量被安全地隐藏在 counter 函数的作用域内,外部无法直接访问,只能通过调用 counter1() 来修改。这实现了一种优雅的状态保持。

2. 实现装饰器

这是闭包最广为人知的应用。Python 中的装饰器本质上就是一个接受函数并返回一个新函数的闭包。

import timedef timer_decorator(func):# 这是一个典型的闭包结构def wrapper(*args, **kwargs):start_time = time.time()result = func(*args, **kwargs)end_time = time.time()print(f"函数 '{func.__name__}' 执行耗时: {end_time - start_time:.4f} 秒")return resultreturn wrapper@timer_decorator
def some_task(n):time.sleep(n)print("任务完成!")some_task(2)

@timer_decorator 只是 some_task = timer_decorator(some_task) 的语法糖。wrapper 函数就是一个闭包,它“记住”了 func(也就是原始的 some_task 函数),并在其基础上添加了计时功能。

3. 函数工厂

闭包非常适合用来创建一系列功能相似但配置不同的函数。

def make_multiplier(n):"""创建一个乘以n的函数"""def multiplier(x):return x * nreturn multiplier# 函数工厂 "生产" 出不同的函数
times_3 = make_multiplier(3)
times_5 = make_multiplier(5)print(times_3(10))  # 30
print(times_5(10))  # 50

三、nonlocal 关键字的重要性

make_counter 的例子中,我们用到了 nonlocal。这是一个关键点。

如果我们在闭包内部尝试修改一个捕获的变量,Python 默认会认为我们正在创建一个新的局部变量。

def make_counter_wrong():count = 0def counter():# 如果没有 nonlocal,下面这行会报错 UnboundLocalError# 因为 Python 认为你在给一个不存在的局部变量 count 赋值count += 1return countreturn counter

nonlocal 关键字就是用来告诉 Python:“嘿,我不是要创建新变量,我是要修改外层(但非全局)作用域里的那个 count 变量。”

  • global:修改全局作用域的变量。
  • nonlocal:修改外层非全局作用域的变量。

四、一个常见的“坑”:循环中的闭包

这是一个经典的面试题,也是实际开发中可能遇到的问题。

funcs = []
for i in range(3):def create_func():return i * ifuncs.append(create_func)# 你期望的输出可能是 0, 1, 4
# 但实际输出是...
for f in funcs:print(f())# 输出:
# 4
# 4
# 4

为什么会这样?

因为闭包捕获的是变量 i 本身,而不是它在循环中某一刻的值。当循环结束后,i 的值最终变成了 2。所以,后续调用所有闭包时,它们访问到的 i 都是 2,结果自然都是 2*2=4。这被称为**“延迟绑定”或“后期绑定”**。

如何修复?

思路是让闭包在创建时就“固定”住当时的值。最简单的方法是利用函数默认参数:

funcs_fixed = []
for i in range(3):# 利用默认参数在定义时就绑定 i 的值def create_func_fixed(val=i):return val * valfuncs_fixed.append(create_func_fixed)for f in funcs_fixed:print(f())# 输出:
# 0
# 1
# 4

总结

闭包是 Python 中一个强大而基础的概念。它不仅是理解装饰器等高级功能的基石,本身也是编写干净、模块化代码的利器。

  • 核心:函数和其创建时环境的结合体。
  • 优点:数据封装、状态保持、避免污染全局命名空间。
  • 应用:实现装饰器、函数工厂、轻量级状态机。
  • 注意:修改捕获变量时使用 nonlocal,警惕循环中的延迟绑定问题。

掌握了闭包,你对 Python 函数式编程的理解会更上一层楼。

http://www.xdnf.cn/news/14975.html

相关文章:

  • 【PyCharm 2025.1.2配置debug】
  • 分类树查询性能优化:从 2 秒到 0.1 秒的技术蜕变之路
  • 低代码实战训练营教学大纲 (10天)
  • [特殊字符] 电子机械制动(EMB)产业全景分析:从技术演进到千亿市场爆发
  • 网络编程学习路线图
  • Python 爬虫实战 | 国家医保
  • OpenBayes 教程上新丨医疗VLM新突破!HealthGPT对复杂MRI模态理解准确率达99.7%,单一模型可处理多类生成任务
  • 一天两道力扣(1)
  • 高效打字辅助工具,解决符号输入难题
  • 使用pdf box去水印
  • Part 0:射影几何,变换与估计-第三章:3D射影几何与变换
  • 分享|大数据分析师职业技术证书报考指南
  • 推荐系统中如果有一个上古精排模型,后续如何优化?
  • 遇到该问题:kex_exchange_identification: read: Connection reset`的解决办法
  • github在线图床
  • PostgreSQL中的rank()窗口函数:实用指南与示例
  • 浏览器原生控件上传PDF导致hash值不同
  • 制作一款打飞机游戏76:分数显示
  • 微软广告推出新的精细资产级别审核
  • 无代码自动化测试工具介绍
  • SpringBoot控制反转
  • CentOS 6操作系统安装
  • 05.SpringBoot拦截器的使用详解
  • 玄机——某学校系统中挖矿病毒应急排查
  • 人脸活体识别3:C/C++实现人脸眨眼 张嘴 点头 摇头识别(可实时检测)
  • lerobot 工程笔记(一)——使用smolvla控制so101
  • 【AI落地应用实战】AIGC赋能职场PPT汇报:从效率工具到辅助优化
  • Docker Compose 基础——AI教你学Docker
  • 链表的实现
  • Flink-1.19.0源码详解5-JobGraph生成-前篇