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

Python 闭包详解

在 Python 编程中,闭包(Closure) 是一个经常出现但又容易被忽视的重要概念。理解闭包不仅能帮助我们写出更优雅的代码,还能深入掌握 Python 的作用域规则。本篇文章将通过“银行存取款”的实例,带你彻底搞懂闭包。


一、什么是闭包?

简单来说就是:函数内部再定义一个函数,并且内部函数引用了外部函数的局部变量,那么这个内部函数就是闭包。

闭包具备三个核心特性:

  1. 函数嵌套:在一个函数里定义另一个函数。
  2. 状态保持:内部函数引用外部函数的变量,并且即使外部函数执行结束,这些变量依旧可以被内部函数访问和修改。
  3. 外部函数返回内部函数:外部函数将内部函数作为返回值

换句话说,闭包让一个函数可以“记住”它所在的环境。


二、为什么需要闭包?

在实际开发中,闭包可以用于:

  • 隐藏数据:像面向对象编程里的私有变量一样,保护一些数据不被外部直接修改。
  • 保持状态:闭包可以让函数拥有“记忆”,保留上一次的执行结果。
  • 简化代码:减少全局变量的使用,让逻辑更局部、更可组合。

三、银行存取款的例子

我们通过银行账户的场景来对比 不使用闭包使用闭包 的不同。

1. 不使用闭包的函数实现

balance = 100  # 全局变量,初始余额def deposit(amount):global balancebalance += amountreturn f"存入 {amount} 元后,余额为 {balance} 元"def withdraw(amount):global balanceif amount <= balance:balance -= amountreturn f"取出 {amount} 元后,余额为 {balance} 元"else:return "余额不足"print(deposit(50))   # 存入 50 元后,余额为 150 元
print(withdraw(70))  # 取出 70 元后,余额为 80 元

这种写法能运行,但存在两个问题:

  1. 依赖全局变量:所有操作都要修改全局的 balance,不利于维护。
  2. 无法创建多个账户:多个账户之间会相互干扰。如果需要两个账户,变量 balance 就会冲突。

2. 使用闭包实现

现在让我们使用闭包来创建一个更优雅的解决方案:

def bank_account(initial_balance=0):balance = initial_balance  # 外部函数的局部变量def deposit(amount):nonlocal balance  # 修改外部函数变量balance += amountreturn f"存入 {amount} 元后,余额为 {balance} 元"def withdraw(amount):nonlocal balanceif amount <= balance:balance -= amountreturn f"取出 {amount} 元后,余额为 {balance} 元"else:return "余额不足"return deposit, withdraw# 使用
deposit, withdraw = bank_account(100)
print(deposit(50))   # 存入 50 元后,余额为 150 元
print(withdraw(30))  # 取出 30 元后,余额为 120 元
print(withdraw(200)) # 余额不足

对比不使用闭包的版本

  • 不依赖全局变量,balance 被安全地封装在函数作用域里,外部无法直接改;每次调用 bank_account() 都得到独立“账户”。
  • 每次调用 bank_account() 都会生成一个新的账户,互不干扰。
  • 缺点:如果操作种类变多,返回多个函数的组织会变复杂。

nonlocal

  • 作用:在嵌套函数中,声明某个变量来自最近一层的外部函数作用域,从而允许赋值修改该变量。

  • 为什么需要:Python 认为在函数体内对变量赋值就会创建局部变量;若你想修改外层的同名变量,必须显式 nonlocal

  • 对比 global

    • global 作用于模块级变量
    • nonlocal 作用于外部函数的局部变量(最近的一层)。
  • 示例

    def counter():n = 0def inc():# n += 1  # 若取消注释,这里会 UnboundLocalErrornonlocal nn += 1return nreturn inc
    

四、闭包的工作原理

理解工作原理能帮助我们准确预判行为与性能:

  1. 自由变量与“细胞对象(cell)”
    当外部变量被内部函数引用时,编译器会把这些变量标记为自由变量并放进“cell”里。内部函数返回后,这些 cell 仍与内部函数对象一起存活。

  2. 晚绑定(by reference)
    闭包捕获的是变量的“引用”,而不是值(即“晚绑定”)。变量后续变化会被闭包“看到”。这就是经典“循环变量陷阱”的根源(见后文)。

  3. 对象存活时间
    只要闭包(内部函数对象)还被引用,它就持有这些 cell 的引用;相应外部对象也不会被回收。

    • 这意味着:闭包可能延长被捕获对象的生命周期。若捕获了大对象,就可能造成不必要的内存占用
  4. 可观察性
    你可以用以下方式窥视闭包内部:

    def outer():x = 10def inner():  # 闭包return xreturn innerf = outer()
    print(f.__code__.co_freevars)          # ('x',)
    print(f.__closure__[0].cell_contents)  # 10
    

内存提示:如果闭包不再需要,及时释放其引用(例如从列表中移除或让变量指向 None),避免无意间长期持有大对象。必要时可只在闭包里保存对象的弱引用weakref)来减少持有强引用的风险(仅适用于可弱引用的对象)。


五、更优雅的实现:类式接口

当然,这个问题用类实现会更自然:

class BankAccount:def __init__(self, balance=0):self.balance = balancedef deposit(self, amount):self.balance += amountreturn f"存入 {amount} 元后,余额为 {self.balance} 元"def withdraw(self, amount):if amount <= self.balance:self.balance -= amountreturn f"取出 {amount} 元后,余额为 {self.balance} 元"else:return "余额不足"account = BankAccount(100)
print(account.deposit(50))   # 150
print(account.withdraw(70))  # 80

类的优势在于:结构清晰,可扩展性强,更符合面向对象编程思想。当行为变多、需要多态/继承/类型检查/序列化时,类的可维护性更好。


六、闭包 vs 类

闭包和类本质上都能封装数据和提供操作接口,但各有优缺点:

维度闭包
轻量性✅ 代码更简洁,快速封装小状态❌ 定义冗长,但结构更完整
状态保持✅ 内部函数天然保持状态✅ 实例属性保持状态,逻辑更直观
封装性✅ 数据隐藏性更强(外部无法直接访问自由变量)✅ 提供私有属性、描述符、@property 等更丰富的封装手段
可读性简短直观,但操作多时容易分散方法命名清晰,能力边界明确,适合协作
可扩展性❌ 操作一多就管理困难,不适合复杂逻辑✅ 天然适配扩展、继承与组合,适合大型系统
适用场景小规模状态保持、回调函数、装饰器、数据隐藏复杂业务建模、需要多态/继承/类型检查/序列化

对比图

七、闭包的常见应用场景

  1. 计数器
def counter():count = 0def inc():nonlocal countcount += 1return countreturn incc = counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3
  1. 函数工厂(延迟计算)
def power(exp):def calc(base):return base ** expreturn calcsquare = power(2)
cube = power(3)print(square(5))  # 25
print(cube(2))    # 8
  1. 回调函数

回调常常要“带点上下文”,闭包能把上下文打包进去:

def make_logger(prefix):def on_event(event):print(f"{prefix}: {event}")return on_eventdef emit(events, cb):for e in events:cb(e)handler = make_logger("INFO")
emit(["connected", "received"], handler)

在异步/事件驱动/GUI/网络库中,携带上下文的回调是闭包的天然舞台。

  1. 数据隐藏(实现私有变量类似的功能)

就像前面的 银行账户 例子一样,balance 对外不可见,但能通过闭包提供的接口安全访问。

  1. 装饰器(最常见的闭包应用之一)

装饰器的本质就是闭包:在不修改函数代码的前提下,增加额外功能

import timedef timer(func):def wrapper(*args, **kwargs):start = time.time()result = func(*args, **kwargs)end = time.time()print(f"{func.__name__} 执行耗时 {end - start:.4f} 秒")return resultreturn wrapper@timer
def slow_function():time.sleep(1)print("函数执行完成")slow_function()

这里 timer 返回的 wrapper 函数就是一个闭包:

  • 它引用了外部函数 func
  • 即使 timer 执行结束,wrapper 依然能访问并调用 func

八、闭包的常见陷阱

  1. 循环变量陷阱
funcs = []
for i in range(3):def f():return ifuncs.append(f)print([f() for f in funcs])  # [2, 2, 2] 而不是 [0, 1, 2]

原因:内部函数引用的是同一个 i,循环结束时 i=2
解决方法:用默认参数绑定变量:

funcs = []
for i in range(3):def f(x=i):return xfuncs.append(f)print([f() for f in funcs])  # [0, 1, 2]
  1. 无意持有大对象导致的内存占用
    闭包存活期间,其捕获的对象也跟着存活。避免把巨大的列表/模型/连接直接塞进闭包;若确需使用:

    • 缩小捕获范围(只捕获必要字段,而非整对象)。
    • 任务完成后确保不再持有闭包引用(例如清空存放闭包的容器)。
    • 可选:在闭包中保存弱引用weakref.ref)而非强引用(适用时)。
  2. 过度使用闭包:闭包适合小功能,当行为多、跨模块协作复杂时,请转向类或更清晰的架构。


总结

  • 闭包要点:嵌套函数 + 引用外部变量;捕获的是变量引用而非值(晚绑定)。
  • 银行存款示例
    • 纯函数版:显式传状态,简单清晰;
    • 闭包版:闭包避免了全局变量问题,且支持多账户;保持状态、隐藏数据、简化代码。
    • 类版:类更适合复杂场景。
    • 不要用 deposit=True 这类布尔开关,若要统一入口,优先枚举/字符串;更推荐两个具名函数或类方法。
  • 工作原理:自由变量被装进 cell;闭包存活会延长其生命周期,可能导致内存占用
  • 应用场景:计数器、函数工厂、回调函数、装饰器、数据隐藏。
  • 陷阱:循环变量晚绑定、无意持有大对象、闭包滥用。
  • nonlocal:修改外层函数变量时必备;不要和 global 混淆。

闭包是 Python 的强大特性之一,熟练掌握它能让你写出更加优雅、灵活的代码。

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

相关文章:

  • rust语言 (1.88) egui (0.32.1) 学习笔记(逐行注释)(十三)菜单、右键菜单
  • JDK版本报错
  • Function + 枚举 + Map:轻量路由器的最佳实践
  • [GeographicLib] LocalCartesian用法
  • 时序数据库选型“下半场”:从性能竞赛到生态博弈,四大主流架构深度横评
  • Palantir Foundry 领先其他数据平台5到10年:一位使用者的深入观察
  • 门面设计模式
  • 第4章 SPSS简介与数据库构建
  • 网络协议---TCP
  • 最大连续1的个数Ⅲ-滑动窗口
  • 2025/8/24 DockerDesktop安装使用
  • 【网络运维】Shell 脚本编程:while 循环与 until 循环
  • 审核问题——应用未配置图标的前景图和后景图
  • JUC——AQS
  • 客流特征识别误报率↓76%!陌讯多模态时序融合算法在智慧零售的实战解析
  • 蓝凌EKP产品:从 XML 到 JSON ——表单存储的性能优化实践
  • [自用笔记]上传本地项目至github
  • 【嵌入式开发 Linux 常用命令系列 8 -- git checkout 解冲突详细介绍】
  • Qt工具栏中图标槽函数没有响应的问题分析
  • 十一、redis 入门 之 数据持久化
  • 基于FPGA的情绪感知系统设计方案:心理健康监测应用(一)
  • yggjs_rlayout框架v0.1.2使用教程 01快速开始
  • 基于RBF-GA的铝/镁异材FSLW工艺参数优化研究
  • Qt---架构文件.pro
  • 02-开发环境搭建与工具链
  • 鸿蒙中点击响应时延分析
  • 多核多线程应用程序开发可见性和乱序如何处理
  • css3之flex布局
  • Linux 学习笔记 - 集群管理篇
  • 音视频学习(五十五):H264中的profile和level