Python进阶系列之-迭代器与生成器

Python进阶系列之-迭代器与生成器:榨干内存的“懒加载”魔法

写在前面:在实际开发中,如果我们要处理一个包含1000万条数据的日志文件,或者生成一个1亿个元素的列表,你的第一反应是不是直接写个列表存起来?小心!这会直接把电脑内存撑爆,程序卡死!为了解决这个问题,Python提供了一套“懒加载”机制——迭代器与生成器。今天我们就来揭开它们神秘的面纱,看看Python是如何做到“用时才取,用完即焚”的。


一、前置知识:什么是迭代(Iteration)?

在讲迭代器之前,我们先理清几个容易混淆的概念。
在Python中,凡是可以通过 for ... in ... 遍历的对象,统称为可迭代对象
比如我们常用的列表、字典、集合、字符串,甚至文件对象。

💡 如何判断一个对象是否可迭代?
可以使用 isinstance(obj, Iterable) 来判断。

from collections.abc import Iterable
print(isinstance([1, 2, 3], Iterable))   # True
print(isinstance(123, Iterable))         # False

但这里有个坑:可迭代对象不一定是迭代器!

  • 列表、字典等:是可迭代对象,但不是迭代器。它们把所有数据都存在内存里,可以通过索引或键随机访问。
  • 迭代器:不仅可迭代,而且是一种惰性计算的对象。它内部不存所有数据,只记录当前遍历的位置,只能往前走,不能后退。

二、迭代器:顺藤摸瓜的“单行道”

2.1 什么是迭代器?

迭代器是一个可以记住遍历位置的对象。它从集合的第一个元素开始访问,直到所有的元素被访问完结束。它只能往前,不能后退。

2.2 iter() 与 next()

把一个可迭代对象变成迭代器,需要用 iter() 函数;从迭代器里取下一个值,需要用 next() 函数。

# 1. 定义一个普通的列表(可迭代对象)
my_list = [10, 20, 30]
print(type(my_list))  # <class 'list'>
# 2. 将列表转换为迭代器
my_iter = iter(my_list)
print(type(my_iter))  # <class 'list_iterator'>
# 3. 使用 next() 逐个获取值
print(next(my_iter))  # 10
print(next(my_iter))  # 20
print(next(my_iter))  # 30
# 4. 取完后再取,会抛出 StopIteration 异常
# print(next(my_iter))  # 报错: StopIteration

在这里插入图片描述

大白话理解:列表就像是一桌满汉全席,全摆在桌子上随你吃;迭代器就像是一个自动传菜机,每次只给你端上来一盘,吃完一盘按下按钮才给你下一盘,直到后厨没菜了(抛出异常)。

2.3 for 循环的底层真相

你有没有想过,for 循环是怎么遍历各种数据结构的?其实它的底层就是调用了迭代器协议!

# 底层伪代码逻辑演示:
# for i in [1, 2, 3]:
#     pass
# 等价于以下代码:
my_list = [1, 2, 3]
my_iter = iter(my_list)  # 1. 创建迭代器
while True:
    try:
        item = next(my_iter)  # 2. 循环获取下一个值
        print(item)
    except StopIteration:     # 3. 捕获异常,结束循环
        break

三、生成器:一种特殊的迭代器

我们刚才把列表转成了迭代器,但它毕竟还是把列表的数据全装进内存了,只是换了个遍历方式。如果我们想要100万个数字,总不能先创建一个100万的列表再转吧?
这时候生成器登场了!

3.1 什么是生成器?

生成器是Python中一种一边循环一边计算的机制。它本质上就是一个用户自己写的迭代器。它不需要把所有数据一次性算出来放到内存里,而是在你每次调用 next() 的时候,才算出下一个值。

3.2 创建生成器的两种方式

方式1:生成器推导式(小括号)

把列表推导式的 [] 换成 (),就得到了一个生成器。

# 列表推导式:直接生成所有数据,占用内存大
my_list = [i for i in range(1, 6)]
print(my_list)  # [1, 2, 3, 4, 5]
# 生成器推导式:生成的是一个对象,几乎不占内存
my_generator = (i for i in range(1, 6))
print(my_generator)  # <generator object <genexpr> at 0x...>
# 获取数据:可以用 next(),也可以用 for 循环
print(next(my_generator))  # 1
print(next(my_generator))  # 2

在这里插入图片描述

方式2:yield 关键字(重点!)

如果生成逻辑比较复杂,推导式写不下,就可以写一个函数,只要函数里出现了 yield 关键字,这个函数就不再是普通函数了,而是一个生成器函数

# 需求: 获取 1 ~ 5 的生成器
def get_generator():
    for i in range(1, 6):
        yield i  # yield会记录每次生成的数据,然后暂停并返回
# 调用函数,此时函数体内的代码并不会执行,而是返回一个生成器对象!
my_gen = get_generator()
print(next(my_gen))  # 1 (程序执行到 yield 1 暂停)
print(next(my_gen))  # 2 (从上次暂停的地方继续执行,到 yield 2 暂停)
# 剩余的可以用 for 循环遍历
for i in my_gen:
    print(i)  # 3, 4, 5

在这里插入图片描述

🚗 大白话理解 yield
普通函数就像是一气呵成跑完的马拉松,跑到终点 return 交出成绩单。
yield 的生成器函数就像是走走停停的快递车。遇到 yield,它就停下来把包裹(值)递给你,然后挂起休眠。等你再喊一声 next(),它立刻苏醒,从上次停下的地方继续往下跑,直到下一个 yield 或函数结束。


四、实战演练:用生成器生成批次数据

在AI模型训练中,数据往往是几十万条。如果一次性把数据全读进内存,机器根本扛不住。标准做法是:按批次读取数据喂给模型。生成器天生就是干这个的!
需求:读取一个有5000行歌词的 jaychou_lyrics.txt 文件,要求按 8条/批次 生成数据。

import os

# 创建测试数据
test_lines = [
    "我用功读书,只为有一天能飞得更高\n",
    "你是我心中最美的云彩\n",
    "安静的夜,我一个人在写歌\n",
    "雨下整夜,我的爱溢出就像雨水\n",
    "你走之后,酒暖回忆思念瘦\n",
    "我用尽所有力气,只为靠近你\n",
    "你的眼神,是我唯一的信仰\n",
    "时间在走,我还在原地等你\n",
    "爱像风,吹过就不再回头\n",
    "你是我生命中最美的意外\n",
    "如果爱有声音,那一定是你的心跳\n",
    "我愿化作星辰,守护你的每个夜晚\n"
]

# 写入文件
with open('jaychou_lyrics.txt', 'w', encoding='utf-8') as f:
    f.writelines(test_lines)

print("✅ 已成功创建测试文件:jaychou_lyrics.txt")
print("请重新运行你的 dataset_loader 代码。")

import math


def dataset_loader(file_path, batch_size):
    """
    数据生成器:按照 batch_size 条 分批读取数据
    """
    # 1. 读取文件所有行(文件不大时可以这样读,大文件可用 readline 逐行读)
    with open(file_path, 'r', encoding='utf-8') as f:
        data_lines = f.readlines()

    # 2. 计算总行数和总批次
    line_count = len(data_lines)
    # math.ceil: 向上取整,例如 10条数据,每批3条,需要 4批
    batch_count = math.ceil(line_count / batch_size)

    # 3. 循环生成每一批次的数据
    for i in range(batch_count):
        # 切片:[0:8], [8:16], [16:24] ...
        batch_data = data_lines[i * batch_size: i * batch_size + batch_size]

        # 核心:用 yield 把每批次数据抛出,暂停等待下一次调用
        yield batch_data


# 测试调用
if __name__ == '__main__':
    # 创建生成器对象
    my_generator = dataset_loader('./jaychou_lyrics.txt', 8)

    # 每次调用 next(),只加载8条数据到内存中处理!
    first_batch = next(my_generator)
    print(f"第一批数据: {first_batch}")

    # 或者用 for 循环遍历所有批次(内存中永远只有一期的数据)
    # for batch in my_generator:
    #     print(f"处理批次: {len(batch)}条")

在这里插入图片描述

为什么这个设计很牛?
因为当文件有 5000 万行时,data_lines 虽然把所有行读进了内存,但如果是用文件指针逐行读取配合 yield,内存占用将永远只有 batch_size 那么大!这就是生成器“用时间换空间”的极致体现。


五、生成器进阶:send() 方法不仅能取,还能存!

除了 next() 唤醒生成器,Python 还提供了一个 send() 方法,它不仅能让生成器继续往下走,还能在暂停的地方给生成器传一个值

def eat_dumplings():
    print("准备吃饺子...")
    while True:
        # yield 返回饺子数量,同时接收外部 send() 传进来的新味道
        flavor = yield "一个饺子"
        print(f"吃到了 {flavor} 馅的饺子")
bob = eat_dumplings()
# 1. 第一次必须用 next() 或 send(None) 启动生成器,让它走到第一个 yield 处
next(bob)  # 打印: 准备吃饺子... 并停在 yield 处
# 2. 使用 send() 传参,并获取这次的返回值
result = bob.send("猪肉大葱")
print(result)  # 打印: 吃到了 猪肉大葱 馅的饺子 -> 返回: 一个饺子
result = bob.send("韭菜鸡蛋")
print(result)  # 打印: 吃到了 韭菜鸡蛋 馅的饺子 -> 返回: 一个饺子

在这里插入图片描述

应用场景:在协程中,send() 是实现双向数据传输的核心机制。这也是为什么说生成器是Python协程的雏形。


六、总结与避坑指南

1. 对比一览表

特性列表/字典等迭代器生成器
内存占用大(一次性全装下)小(只记录位置)极小(边算边出)
创建方式[], {}iter(可迭代对象)()推导式, yield函数
访问方式索引、切片、循环next(), 循环next(), send(), 循环
可重复遍历可以(每次从头开始)不可以(单向道)不可以(单向道)

2. 小白避坑指南

  1. 生成器是一次性的! 遍历完一次后,生成器就空了,不能像列表那样二次 for 循环。如果还要用,只能重新创建一个生成器对象。
  2. 首次启动生成器:使用 send() 传参前,必须先用 next()send(None) 把生成器运行到第一个 yield 处,否则会报错 TypeError: can't send non-None value to a just-started generator
  3. 不要混淆 return 和 yield:函数里写了 yield 就不能写 return 返回值(只能写 return 表示结束)。因为生成器函数本身返回的已经是生成器对象了。

结语:迭代器和生成器是Python高阶语法的精华。理解了“懒加载”思想,你以后处理大数据集、写爬虫、做AI数据加载时,就能游刃有余,写出既优雅又省内存的代码!如果这篇博客对你有帮助,记得点赞+收藏哦!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Python-AI Xenon

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值