68_Python生成器与迭代器

Python生成器与迭代器:惰性求值与内存友好的编程艺术

文章目录

前言

处理大规模数据时,一次性将所有数据加载到内存往往不现实------你也许需要逐行处理一个10GB的日志文件,或者生成一个理论上是无限的数列。Python的**迭代器(Iterator)生成器(Generator)**提供了优雅的解决方案:按需生成数据,而非一次性存储所有数据。这种"惰性求值"策略是Python中内存管理的利器。

迭代器与生成器在面试中的地位 :它们是Python高级特性的"敲门砖"。理解迭代器协议(__iter__ / __next__)是理解for循环底层原理的前提;掌握生成器则是理解协程和异步编程的基础。面试官常问"生成器和列表的区别"、"如何实现一个自定义迭代器"等问题,本文会逐一给出清晰答案。

一、迭代器基础

1.1 可迭代对象 vs 迭代器

这是学习迭代器的第一道"认知门槛"。很多初学者混淆了"可迭代对象"和"迭代器"两个概念。核心区别 :可迭代对象(Iterable)是任何可以被 for...in 遍历的对象,它实现了 __iter__ 方法;迭代器(Iterator)则进一步实现了 __next__ 方法,可以惰性地逐个产出值。比喻 :可迭代对象像一个"书籍目录"------你知道里面有什么;迭代器像一个"书签"------你知道当前位置,并能翻到下一页。列表是可迭代对象但不是迭代器;iter(list) 返回的才是迭代器。

python 复制代码
# 可迭代对象(Iterable):可以被for...in遍历的对象
# 迭代器(Iterator):实现了__next__方法的对象

from collections.abc import Iterable, Iterator

my_list = [1, 2, 3]
print(isinstance(my_list, Iterable))  # True
print(isinstance(my_list, Iterator))  # False - 列表是可迭代对象,但不是迭代器

# 使用 iter() 将可迭代对象转为迭代器
list_iter = iter(my_list)
print(isinstance(list_iter, Iterator))  # True

# 手动迭代
print(next(list_iter))  # 1
print(next(list_iter))  # 2
print(next(list_iter))  # 3
# print(next(list_iter))  # 触发 StopIteration 异常

1.2 for 循环的幕后原理

python 复制代码
# for 循环本质上是这样工作的:
numbers = [10, 20, 30]

# 等价于以下的 while 循环
iterator = iter(numbers)
while True:
    try:
        item = next(iterator)
        print(item)
    except StopIteration:
        break

1.3 自定义迭代器

python 复制代码
class Countdown:
    """倒计时迭代器"""

    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

# 使用自定义迭代器
for num in Countdown(5):
    print(num, end=" ")  # 5 4 3 2 1 0

print()

# 也可以手动迭代
cd = Countdown(3)
print(next(cd))  # 3
print(next(cd))  # 2

二、生成器入门

生成器是Python中最强大的特性之一。它的本质是一个"可以暂停和恢复的函数"------每次 yield 产生一个值后,函数的状态(局部变量、指令指针等)被完整保存下来,等下次 next() 调用时再恢复。这种"保存现场、下次恢复"的机制,也是Python协程(coroutine)和 asyncio 的实现基础。

2.1 使用 yield 创建生成器

生成器是创建迭代器最简单的方式------只需要在函数中使用 yield 关键字。比起手动实现 __iter____next__ 方法,生成器函数大大减少了样板代码,同时让逻辑表达更加直观。

python 复制代码
def countdown_gen(start):
    """生成器版本的倒计时"""
    current = start
    while current >= 0:
        yield current
        current -= 1

# 调用生成器函数返回一个生成器对象
gen = countdown_gen(5)
print(type(gen))  # <class 'generator'>

# 迭代生成器
for num in gen:
    print(num, end=" ")  # 5 4 3 2 1 0

2.2 yield 的执行机制

python 复制代码
def demo_generator():
    print("生成器开始执行")
    yield 1
    print("恢复执行,yield 第2个值")
    yield 2
    print("恢复执行,yield 第3个值")
    yield 3
    print("生成器执行完毕")

gen = demo_generator()

print("调用 next(gen) 第1次:")
print("返回值:", next(gen))
print()
print("调用 next(gen) 第2次:")
print("返回值:", next(gen))
print()
print("调用 next(gen) 第3次:")
print("返回值:", next(gen))

输出:

复制代码
调用 next(gen) 第1次:
生成器开始执行
返回值: 1

调用 next(gen) 第2次:
恢复执行,yield 第2个值
返回值: 2

调用 next(gen) 第3次:
恢复执行,yield 第3个值
返回值: 3

yield 让函数在"交出值"的地方暂停 ,下次 next() 调用时从暂停处继续执行------这就是生成器的"惰性求值"本质。

2.3 yield from:委托子生成器

python 复制代码
def sub_generator_a():
    yield "A1"
    yield "A2"
    yield "A3"

def sub_generator_b():
    yield "B1"
    yield "B2"

def main_generator():
    yield "Start"
    # yield from 简化子生成器的迭代
    yield from sub_generator_a()
    yield "Middle"
    yield from sub_generator_b()
    yield "End"

for item in main_generator():
    print(item, end=" ")
# Start A1 A2 A3 Middle B1 B2 End

三、生成器表达式

生成器表达式是创建生成器的简洁语法,类似列表推导式但使用圆括号 。它们的外观区别仅在于括号类型,但底层行为截然不同------列表推导立即计算所有值并创建完整列表;生成器表达式则返回一个生成器对象,在你遍历它时才逐个计算值。实际使用建议:如果只需要遍历一次数据,用生成器表达式;如果需要多次访问数据或需要索引访问,用列表推导。

python 复制代码
# 列表推导:立即计算,一次性创建整个列表(占内存)
list_comp = [x ** 2 for x in range(1000000)]
print(f"列表推导占用内存: {list_comp.__sizeof__() / 1024 / 1024:.1f} MB")

# 生成器表达式:惰性计算,需要时才生成(极省内存)
gen_expr = (x ** 2 for x in range(1000000))
print(f"生成器表达式占用内存: {gen_expr.__sizeof__() / 1024:.1f} KB")

# 逐次取值
print(next(gen_expr))  # 0
print(next(gen_expr))  # 1
print(next(gen_expr))  # 4

性能对比:

python 复制代码
import sys

data = [1, 2, 3, 4, 5]

# 列表推导创建新列表
squares_list = [x * x for x in data]

# 生成器表达式不创建新列表,可以和函数直接配合
total = sum(x * x for x in data)  # 无需方括号,更高效
print(f"平方和: {total}")

# 内存占用对比
print(f"列表推导: {sys.getsizeof(squares_list)} bytes")
gen = (x * x for x in data)
print(f"生成器表达式: {sys.getsizeof(gen)} bytes")

四、itertools:生成器工具箱

itertools 模块是Python标准库中的"隐蔽宝藏",提供了大量高效的迭代器构建工具。这些工具都是用C语言实现的,性能极高。它们分为三类:无限迭代器 (count、cycle、repeat)、有限迭代器 (accumulate、chain、compress、groupby等)、组合迭代器 (product、permutations、combinations)。在实际开发中,itertools 可以极大简化涉及组合、排列、分组、过滤等操作的代码,是每个Python进阶开发者都应该掌握的模块。

python 复制代码
import itertools

# ─── 无限迭代器 ───
counter = itertools.count(start=10, step=2)
print([next(counter) for _ in range(5)])  # [10, 12, 14, 16, 18]

cycled = itertools.cycle("ABC")
print([next(cycled) for _ in range(7)])   # ['A', 'B', 'C', 'A', 'B', 'C', 'A']

repeated = itertools.repeat("Hi", 3)
print(list(repeated))  # ['Hi', 'Hi', 'Hi']

# ─── 合并迭代器 ───
a = [1, 2, 3]
b = ['a', 'b', 'c']

# chain: 串联多个迭代器
print(list(itertools.chain(a, b)))  # [1, 2, 3, 'a', 'b', 'c']

# zip_longest: 以最长的为准
print(list(itertools.zip_longest(a, b, fillvalue=None)))
# [(1, 'a'), (2, 'b'), (3, 'c')]

# ─── 组合生成 ───
items = ['A', 'B', 'C']

# 排列 (有顺序)
print(list(itertools.permutations(items, 2)))
# [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

# 组合 (无顺序)
print(list(itertools.combinations(items, 2)))
# [('A', 'B'), ('A', 'C'), ('B', 'C')]

# 笛卡尔积
colors = ['红', '蓝']
sizes = ['S', 'M', 'L']
print(list(itertools.product(colors, sizes)))
# [('红', 'S'), ('红', 'M'), ('红', 'L'), ('蓝', 'S'), ('蓝', 'M'), ('蓝', 'L')]

# ─── 过滤与分组 ───
data = [1, 2, 3, 4, 5, 6]

# filterfalse: 保留不满足条件的
print(list(itertools.filterfalse(lambda x: x % 2 == 0, data)))
# [1, 3, 5]

# takewhile / dropwhile
print(list(itertools.takewhile(lambda x: x < 4, data)))  # [1, 2, 3]
print(list(itertools.dropwhile(lambda x: x < 4, data)))  # [4, 5, 6]

# groupby: 按key分组(需要数据预先排序)
students = [
    ("北京", "张三"),
    ("北京", "李四"),
    ("上海", "王五"),
    ("上海", "赵六"),
]
for city, group in itertools.groupby(students, key=lambda x: x[0]):
    names = [name for _, name in group]
    print(f"{city}: {', '.join(names)}")

五、生成器的高级用法

5.1 双向通信:send() 方法

send() 是生成器的高级特性,它允许外部世界向生成器内部发送数据,实现双向通信。注意:在调用 send() 之前,必须先用 next()send(None) 将生成器推进到第一个 yield 表达式处。send() 方法将数据传递给 yield 表达式并返回,同时暂停在下一个 yield 处等待。这个机制是协程的底层实现基础。

python 复制代码
def interactive_generator():
    """可以与外部双向通信的生成器"""
    print("生成器启动")
    while True:
        received = yield
        print(f"收到: {received}")
        if received == "quit":
            break
    print("生成器关闭")
    return "Goodbye"

gen = interactive_generator()
next(gen)  # 启动生成器(必须先执行到第一个yield)
gen.send("Hello")
gen.send("World")
try:
    gen.send("quit")
except StopIteration as e:
    print(f"返回值: {e.value}")

5.2 数据管道

生成器的一个重要应用模式是数据管道 ------将多个生成器串联起来,每个生成器完成一步数据处理,形成"读取→清洗→转换→输出"的流式处理链。这种模式的核心优势是内存友好 ------数据像水流一样在管道中流动,每个时刻只有当前正在处理的一小部分数据在内存中。对于处理大数据集,数据管道模式比先全部读入再逐个处理的方式更加高效和可靠。

"""生成器1:读取文件行(模拟)"""

lines = ["apple,5,2.5", "banana,3,1.8", "orange,8,3.0",

"apple,2,2.5", "banana,6,1.8", "grape,4,4.0"]

for line in lines:

yield line.strip()

def parse_csv(lines_gen):

"""生成器2:解析CSV"""

for line in lines_gen:

parts = line.split(",")

yield {"name": parts0, "quantity": int(parts1), "price": float(parts2)}

def calculate_revenue(parsed_gen):

"""生成器3:计算收入"""

for item in parsed_gen:

item"revenue" = item"quantity" * item"price"

yield item

def aggregate_by_name(revenue_gen):

"""生成器4:按名称汇总"""

summary = {}

for item in revenue_gen:

name = item"name"

if name not in summary:

summaryname = 0

summaryname += item"revenue"

yield from summary.items()

管道串联执行

pipeline = aggregate_by_name(

calculate_revenue(

parse_csv(

read_lines("data.csv")

)

)

)

for name, total in pipeline:

print(f"{name}: ¥{total:.1f}")

复制代码
## 六、实战案例:大文件处理

处理大文件是生成器最经典的应用场景。当一个文件有几十GB时,使用 `read()` 一次性加载会导致内存溢出------你的程序不是"慢",而是直接"挂掉"。使用生成器配合分块读取,可以在几乎恒定的内存开销下处理任意大小的文件。下面的案例展示了如何用生成器管道处理一个大型日志文件并提取Top N的IP地址。

```python
import os
import re


def read_file_in_chunks(filepath, chunk_size=1024 * 1024):
    """以块的方式读取大文件,避免内存溢出"""
    with open(filepath, "r", encoding="utf-8") as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            yield chunk


def extract_ips_from_chunks(chunks_gen):
    """从文件块中提取IP地址"""
    ip_pattern = re.compile(r'\b(?:\d{1,3}\.){3}\d{1,3}\b')
    buffer = ""  # 处理跨chunk的行

    for chunk in chunks_gen:
        buffer += chunk
        lines = buffer.split("\n")
        # 最后一行可能不完整,保留到下次处理
        buffer = lines.pop()

        for line in lines:
            ips = ip_pattern.findall(line)
            for ip in ips:
                yield ip


def top_n(items_gen, n=10):
    """找出Top N的项(使用堆更高效,此处简化)"""
    from collections import Counter
    counter = Counter(items_gen)
    return counter.most_common(n)


# 生成测试文件
test_log_content = ""
for i in range(10000):
    ip = f"192.168.1.{i % 256}"
    test_log_content += f"{ip} - [2024-01-15] GET /page/{i}\n"

with open("big_log.txt", "w", encoding="utf-8") as f:
    f.write(test_log_content)

# 使用管道处理大文件
chunks = read_file_in_chunks("big_log.txt", chunk_size=4096)
ips = extract_ips_from_chunks(chunks)
top_ips = top_n(ips, 5)

print("访问最多的IP (Top 5):")
for ip, count in top_ips:
    print(f"  {ip}: {count}次")

七、迭代器协议检查与常见坑

迭代器有一个容易让人忽略的特点:生成器只能遍历一次。一旦生成器的所有值都被消费,再次遍历它将不会产生任何值。这在调试时常常造成困惑------你打印了一次生成器的内容(list(gen)),然后试图再次遍历它,发现它是空的。解决方案很简单:如果需要多次遍历,使用列表存储结果;或者每次都重新创建生成器。

python 复制代码
def fibonacci(n):
    """斐波那契数列生成器"""
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

# 注意:生成器只能遍历一次!
fib = fibonacci(5)
print("第一次遍历:")
print(list(fib))      # [0, 1, 1, 2, 3]
print("第二次遍历:")
print(list(fib))      # [] ------ 生成器已耗尽!

# 需要重复遍历时,应使用函数调用重新创建
print("重新创建生成器:")
print(list(fibonacci(5)))  # [0, 1, 1, 2, 3]

总结

生成器和迭代器是Python中处理序列数据的核心机制。它们不仅是内存管理的利器,更是Python异步编程和协程的基石------yield 就是协程的雏形,理解生成器的工作方式是掌握asyncio的前提。

特性 列表 生成器
内存占用 一次性存储所有元素 按需生成,极省内存
访问方式 可多次随机访问 只能遍历一次
计算时机 立即 惰性求值
适用场景 小数据,需多次访问 大数据流,一次处理
  • iter() / next() 实现迭代器协议
  • yield 轻松创建生成器,yield from 委托子生成器
  • 生成器表达式 (x for x in ...) 是列表推导的内存友好版
  • itertools 模块是处理迭代器的瑞士军刀

下一篇我们将探索时间日期处理 ,学习如何使用 datetime 模块优雅地处理所有与时间相关的操作。

✅ 亮点总结

  • 从迭代器协议(__iter__/__next__)底层原理讲起,揭示 for 循环的幕后机制
  • yield 关键字的核心用法全解:普通生成器、yield from 委托、send() 协程通信
  • 生成器表达式作为内存友好型列表推导,适合大数据流的链式处理
  • itertools 模块 7 大常用工具(count/cycle/chain/islice/groupby/combinations/product)实战演示

适用场景

  • 大文件流式处理:逐行读取海量日志文件而不占用过多内存
  • 无限序列生成:生成斐波那契数列、素数序列等理论上无限的数学序列
  • 数据管道构建:将多个生成器串联,实现"读取→清洗→转换→输出"的流式处理链

扩展方向

  • 学习 Python 3.3+ 的 yield from 高级用法,实现协程嵌套和双向通信
  • 探索 collections.abc 中的迭代器抽象基类,构建自定义容器类型
  • 结合 asyncio 的异步生成器(async for),实现异步流式数据处理