目 录CONTENT

文章目录

Python(三十五) 迭代器与生成器完整教程

Python(三十五) 迭代器与生成器完整教程

目录

  1. 开篇:从"点名册"理解迭代
  2. 可迭代对象与迭代器协议
  3. next() 与 iter():手动操控迭代
  4. 生成器函数:用 yield 制造数据
  5. 生成器表达式:一行代码的生成器
  6. itertools 常用工具库
  7. 综合实战案例
  8. 常见面试题与易错点

一、开篇:从"点名册"理解迭代

1.1 什么是迭代?

生活中:老师拿着一本点名册,从第一个学生开始,依次念出每个人的名字。这个"一个接一个处理"的过程就叫迭代(Iteration)

Python 中

# 对列表进行迭代:逐个取出元素
students = ["张三", "李四", "王五", "赵六"]
for name in students:
    print(f"点到:{name}")

# 输出:
# 点到:张三
# 点到:李四
# 点到:王五
# 点到:赵六

for 循环的背后,Python 悄悄做了三件事:

1. 调用 iter(students)  →  获取一个"点名器"(迭代器)
2. 反复调用 next(点名器) →  一个一个喊出名字
3. 名字喊完了 →  StopIteration  →  for 循环自动退出

1.2 本教程学习路线

可迭代对象(能放进 for 循环的东西)
    │
    └── 迭代器(真正干活的人,有 __next__() 方法)
            │
            ├── 手动创建迭代器(iter() + next())
            │
            ├── 生成器函数(用 yield 代替 return)
            │
            ├── 生成器表达式(一行代码的生成器)
            │
            └── itertools(瑞士军刀级迭代工具箱)

二、可迭代对象与迭代器协议

2.1 可迭代对象(Iterable)

定义:实现了 __iter__() 方法的对象,就是可迭代对象。能被 for 循环遍历的都是可迭代对象。

通俗理解:可迭代对象就是"一本花名册",你能从里面一个一个找人。

# 以下都是可迭代对象
list_data = [1, 2, 3]           # 列表
tuple_data = (1, 2, 3)          # 元组
dict_data = {"a": 1, "b": 2}    # 字典(遍历的是键)
set_data = {1, 2, 3}            # 集合
str_data = "hello"              # 字符串
range_data = range(5)           # range 对象
file_obj = open("test.txt")     # 文件对象

# 它们都能放进 for 循环
for item in list_data:
    print(item)

# 如何判断一个对象是否可迭代?
from collections.abc import Iterable

print(isinstance([1, 2, 3], Iterable))   # True
print(isinstance("hello", Iterable))     # True
print(isinstance(123, Iterable))         # False(整数不可迭代!)
print(isinstance(3.14, Iterable))        # False(浮点数不可迭代!)

新手易踩坑:

  • :把一个数字放进 for 循环 → for i in 100: ❌ 报错 TypeError: 'int' object is not iterable
  • 解释:整数不是"花名册",不能一个一个找人。想循环 100 次请用 for i in range(100):

2.2 迭代器(Iterator)

定义:同时实现了 __iter__()__next__() 方法的对象,就是迭代器。

通俗理解:迭代器就是"正在点名的那个人",他知道现在点到谁了,也知道下一个该点谁。

# 所有迭代器都是可迭代对象,但反过来不成立!
from collections.abc import Iterator

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

# 通过 iter() 可以把可迭代对象变成迭代器
my_iter = iter([1, 2, 3])
print(isinstance(my_iter, Iterator))     # True

2.3 迭代器协议(核心!)

迭代器协议就是两条规则:

规则 方法 作用
1 __iter__() 返回迭代器对象自身
2 __next__() 返回下一个元素;没有元素了则抛出 StopIteration
可迭代对象(Iterable)           迭代器(Iterator)
┌─────────────────┐           ┌─────────────────┐
│ __iter__()       │──iter()──→│ __iter__()       │
│ 返回一个迭代器    │           │ 返回自身          │
└─────────────────┘           │ __next__()       │
                              │ 返回下一个元素     │
                              └─────────────────┘

2.4 手写一个迭代器(彻底搞懂原理)

class CountDown:
    """倒计时迭代器:从 start 倒数到 1"""

    def __init__(self, start):
        self.current = start  # 当前数字

    def __iter__(self):
        # 迭代器的 __iter__ 返回自身
        return self

    def __next__(self):
        # 每次调用返回下一个数字
        if self.current <= 0:
            raise StopIteration   # 没数字了,举手报告"结束!"
        result = self.current
        self.current -= 1          # 指针前进一步
        return result


# === 使用倒计时迭代器 ===
countdown = CountDown(5)

# 方式1:手动调用 next()
print(next(countdown))  # 5
print(next(countdown))  # 4
print(next(countdown))  # 3
print(next(countdown))  # 2
print(next(countdown))  # 1
# print(next(countdown))  # StopIteration!倒计时结束

# 方式2:放进 for 循环(for 自动处理 StopIteration)
for num in CountDown(3):
    print(f"倒计时:{num}")
# 输出:
# 倒计时:3
# 倒计时:2
# 倒计时:1

# 方式3:转成列表
print(list(CountDown(3)))  # [3, 2, 1]

2.5 迭代器的三个核心特性

# 【特性1:一次性消费】迭代器只能遍历一次,遍历完就空了
my_iter = iter([1, 2, 3])
print(list(my_iter))  # [1, 2, 3]
print(list(my_iter))  # [] —— 已经空了!不能倒回去

# 对比:列表可以反复遍历
my_list = [1, 2, 3]
print(list(my_list))  # [1, 2, 3]
print(list(my_list))  # [1, 2, 3] —— 每次都是新的

# 【特性2:惰性求值】迭代器不会提前把所有元素都准备好
# 而是在每次 next() 时才"现算现给"
big_range = range(1000000000)  # 10亿个数!但内存占用极小
# 对比:list(range(1000000000)) 会直接占用约 8GB 内存!

# 【特性3:不可随机访问】迭代器不能像列表一样用索引
my_iter = iter([1, 2, 3])
# print(my_iter[1])  # TypeError! 迭代器不支持索引
print(next(my_iter))  # 只能这样一个一个取

2.6 可迭代对象 vs 迭代器 对比

特性 可迭代对象(如 list) 迭代器(如 iter(list))
反复遍历 可以 不能(一次性)
内存占用 大(存全部元素) 小(只存当前状态)
随机访问 支持 obj[i] 不支持
求长度 len() 支持 不支持
能否放 for 循环

记忆口诀:可迭代对象是"菜谱"(随时翻看),迭代器是"正在炒菜的人"(只能往前,炒完就没了)。

课堂小练习 1

自己写一个 FibonacciIterator 类,实现斐波那契数列迭代器:前两个数是 1, 1,之后每个数都是前两个数之和。要求设定一个最大值,超出最大值时停止迭代。

# 期望效果:
fib = FibonacciIterator(20)   # 不超过 20 的斐波那契数
print(list(fib))              # [1, 1, 2, 3, 5, 8, 13]
点击查看参考答案
class FibonacciIterator:
    def __init__(self, max_value):
        self.a = 1          # 第一个数
        self.b = 1          # 第二个数
        self.max_value = max_value
        self.count = 0      # 已生成的数量

    def __iter__(self):
        return self

    def __next__(self):
        if self.count == 0:
            self.count += 1
            if self.a > self.max_value:
                raise StopIteration
            return self.a
        if self.count == 1:
            self.count += 1
            if self.b > self.max_value:
                raise StopIteration
            return self.b
        # 计算下一个数
        next_val = self.a + self.b
        if next_val > self.max_value:
            raise StopIteration
        self.a = self.b
        self.b = next_val
        return next_val

fib = FibonacciIterator(20)
print(list(fib))  # [1, 1, 2, 3, 5, 8, 13]

三、next() 与 iter():手动操控迭代

3.1 iter():获取迭代器

# iter() 的两种用法

# 用法1:把一个可迭代对象转换成迭代器
nums = [10, 20, 30]
it = iter(nums)
print(type(it))  # <class 'list_iterator'>

# 用法2:iter(callable, sentinel) —— 重复调用直到遇到"哨兵值"
import random

# 反复调用 random.randint(1, 10),直到结果为 7 就停止
dice_roller = iter(lambda: random.randint(1, 10), 7)
for roll in dice_roller:
    print(f"掷出:{roll}")
print("掷出 7,停止!")
# 可能的输出:
# 掷出:3
# 掷出:9
# 掷出:1
# 掷出 7,停止!

3.2 next():获取下一个元素

# next() 的两种写法

my_iter = iter([10, 20, 30])

# 写法1:可能抛出 StopIteration
print(next(my_iter))  # 10
print(next(my_iter))  # 20
print(next(my_iter))  # 30
# print(next(my_iter))  # StopIteration!

# 写法2:带默认值,安全!(推荐)
my_iter2 = iter([10, 20, 30])
print(next(my_iter2, "没了"))  # 10
print(next(my_iter2, "没了"))  # 20
print(next(my_iter2, "没了"))  # 30
print(next(my_iter2, "没了"))  # "没了" —— 不抛异常!

3.3 for 循环背后的真相

# 这个 for 循环:
for item in [1, 2, 3]:
    print(item)

# 等价于下面的"手动版":
my_iter = iter([1, 2, 3])   # 步骤1:获取迭代器
while True:
    try:
        item = next(my_iter) # 步骤2:反复获取下一个
        print(item)
    except StopIteration:   # 步骤3:元素取完了,退出循环
        break

这就解释了为什么迭代器是一次性的:for 循环已经把迭代器"走到底"了,不能再回头。

3.4 文件对象也是迭代器

# 文件对象本身就是迭代器!可以逐行读取而不一次性加载整个文件
# with open("large_file.txt", "r", encoding="utf-8") as f:
#     for line in f:          # 每次只读一行,内存友好
#         print(line.strip())

# 等价于:
# with open("large_file.txt", "r", encoding="utf-8") as f:
#     while True:
#         try:
#             line = next(f)
#             print(line.strip())
#         except StopIteration:
#             break

课堂小练习 2

iter(callable, sentinel) 实现一个"读取用户输入直到输入为空"的功能:反复读取用户输入,当用户直接敲回车(空字符串)时停止。

点击查看参考答案
# iter(input, "") 意思是反复调用 input(),直到返回值等于 ""(空字符串)
print("请输入内容(直接按回车结束):")
for line in iter(input, ""):
    print(f"你输入了:{line}")
print("输入结束!")

# 运行效果:
# 请输入内容(直接按回车结束):
# 你好
# 你输入了:你好
# Python
# 你输入了:Python
# (直接回车)
# 输入结束!

四、生成器函数:用 yield 制造数据

4.1 什么是生成器?

生活类比:普通函数(return)像食堂阿姨,一次性把所有菜都打好放在托盘上递给你;生成器(yield)像回转寿司,现做现上,吃一碟来一碟,不会把明天的寿司都提前做出来。

  • 普通函数return 一次性返回所有结果,函数结束
  • 生成器函数yield 返回一个结果后暂停,下次调用时从暂停处继续

4.2 第一个生成器函数

def count_up_to(n):
    """从 1 数到 n 的生成器"""
    i = 1
    while i <= n:
        yield i       # 返回 i,然后暂停在这里!
        i += 1        # 下次 next() 时从这里继续


# === 使用生成器 ===
counter = count_up_to(5)

print(type(counter))   # <class 'generator'>
print(next(counter))   # 1
print(next(counter))   # 2
print(next(counter))   # 3

# 放进 for 循环
for num in count_up_to(3):
    print(f"计数:{num}")
# 计数:1
# 计数:2
# 计数:3

# 转成列表
print(list(count_up_to(4)))  # [1, 2, 3, 4]

4.3 yield 的执行流程(图解)

def simple_gen():
    print(">>> 开始")
    yield 1
    print(">>> 我又活过来了")
    yield 2
    print(">>> 最后一次")
    yield 3
    print(">>> 结束了")


gen = simple_gen()

print("第一次 next():")
result1 = next(gen)
print(f"得到:{result1}\n")

print("第二次 next():")
result2 = next(gen)
print(f"得到:{result2}\n")

print("第三次 next():")
result3 = next(gen)
print(f"得到:{result3}\n")

# 输出:
# 第一次 next():
# >>> 开始
# 得到:1
#
# 第二次 next():
# >>> 我又活过来了
# 得到:2
#
# 第三次 next():
# >>> 最后一次
# 得到:3

关键理解yield 就像一个"游戏存档点"。每次 yield 之后函数暂停,下次 next() 时从上次暂停的地方"读档继续"。

4.4 生成器的实用场景

场景一:处理大文件(逐行读取,不撑爆内存)

def read_large_file(filepath):
    """逐行读取大文件,每次只返回一行"""
    with open(filepath, "r", encoding="utf-8") as f:
        for line in f:
            yield line.strip()

# 使用:不论文件多大,内存中始终只存一行
# for line in read_large_file("huge_log.txt"):
#     if "ERROR" in line:
#         print(line)

场景二:无限序列生成

def infinite_counter(start=0):
    """无限计数器:从 start 开始永远数下去"""
    while True:
        yield start
        start += 1

# 注意:不能 list() 这个生成器(无限!),要配合条件使用
counter = infinite_counter(100)
for i in counter:          # 在循环内部控制停止条件
    print(i)
    if i >= 105:
        break
# 输出:100, 101, 102, 103, 104, 105

场景三:斐波那契数列(简洁优雅)

def fibonacci(max_value=None):
    """斐波那契数列生成器(可选最大值限制)"""
    a, b = 0, 1
    while max_value is None or a <= max_value:
        yield a
        a, b = b, a + b

# 取前 10 个
fib = fibonacci(50)
print(list(fib))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

场景四:数据管道(串联处理)

def read_numbers():
    """模拟从数据库或文件中逐条读取数字"""
    for i in range(1, 11):
        print(f"  读取:{i}")
        yield i

def filter_even(numbers):
    """过滤出偶数"""
    for num in numbers:
        if num % 2 == 0:
            print(f"  过滤通过:{num}")
            yield num

def square(numbers):
    """计算平方"""
    for num in numbers:
        result = num ** 2
        print(f"  平方计算:{num}^2 = {result}")
        yield result

# 三个生成器串成流水线,每个阶段只处理一个元素
pipeline = square(filter_even(read_numbers()))
for result in pipeline:
    print(f"最终输出:{result}\n")

# 输出关键:不是先读完所有数据再过滤再平方,而是:
# 读取1→1不是偶数→跳过→读取2→2是偶数→2平方=4→最终输出4→读取3→...

4.5 yield from:委托给子生成器

# 不用 yield from 的写法(啰嗦)
def combined_old():
    for item in [1, 2, 3]:
        yield item
    for item in "abc":
        yield item

# 用 yield from 的写法(简洁)
def combined_new():
    yield from [1, 2, 3]      # 委托给列表
    yield from "abc"          # 委托给字符串

print(list(combined_new()))   # [1, 2, 3, 'a', 'b', 'c']


# yield from 的真实威力:递归遍历嵌套结构
def flatten(nested_list):
    """展平任意嵌套的列表"""
    for item in nested_list:
        if isinstance(item, list):
            yield from flatten(item)  # 递归委托!
        else:
            yield item

nested = [1, [2, 3, [4, 5]], 6, [7, 8]]
print(list(flatten(nested)))  # [1, 2, 3, 4, 5, 6, 7, 8]

4.6 return vs yield 对比

特性 return yield
返回后函数状态 函数结束,局部变量销毁 函数暂停,局部变量保留
返回值类型 任意类型 生成器对象
可调用次数 一次(return 后函数结束) 多次(每次 yield 后可以继续)
内存占用 需要构建完整结果 惰性求值,内存极省
适用场景 小数据量,需要完整结果 大数据量,逐项处理

记忆口诀return 是"杀鸡取卵"(一次性),yield 是"养鸡下蛋"(持续产出)。

4.7 send() 方法:向生成器发送数据(进阶)

def echo_generator():
    """既产出数据,也接收数据的高级生成器"""
    print("生成器启动!")
    while True:
        received = yield               # 先 yield 空值,等待外部 send
        if received is None:
            continue
        print(f"生成器收到了:{received}")
        yield f"已处理:{received}"    # 再 yield 回应


gen = echo_generator()
next(gen)                    # 启动生成器,执行到第一个 yield

# send() 发送数据给生成器,并让它执行到下一个 yield
response = gen.send("你好")
print(f"外部收到了:{response}")

response = gen.send("Python")
print(f"外部收到了:{response}")

gen.close()  # 关闭生成器

# 输出:
# 生成器启动!
# 生成器收到了:你好
# 外部收到了:已处理:你好
# 生成器收到了:Python
# 外部收到了:已处理:Python

新手易踩坑:

  • 坑 1gen.send() 第一次调用前必须先 next(gen) 或用 gen.send(None) 启动
  • 坑 2:生成器函数里 yield 在赋值号右边 → received = yield 看起来很奇怪,记住就行
  • 坑 3:生成器用完了再 next()StopIteration,和迭代器一样是一次性的

课堂小练习 3

用生成器函数实现一个"素数生成器":每次 next() 返回下一个素数,从 2 开始,没有最大值限制。

点击查看参考答案
def prime_generator():
    """无限素数生成器"""
    primes = []           # 已发现的素数列表
    num = 2               # 从 2 开始检查
    while True:
        # 检查 num 是否能被任何已知素数整除
        is_prime = True
        for p in primes:
            if p * p > num:    # 优化:只需检查到 sqrt(num)
                break
            if num % p == 0:
                is_prime = False
                break
        if is_prime:
            primes.append(num)
            yield num
        num += 1

# 取前 10 个素数
gen = prime_generator()
for _ in range(10):
    print(next(gen), end=" ")
# 输出:2 3 5 7 11 13 17 19 23 29

五、生成器表达式:一行代码的生成器

5.1 语法:像列表推导式,但用圆括号

# 列表推导式:用方括号,返回完整列表(一次性生成所有元素)
squares_list = [x**2 for x in range(10)]
print(squares_list)
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(type(squares_list))  # <class 'list'>

# 生成器表达式:用圆括号,返回生成器(惰性求值)
squares_gen = (x**2 for x in range(10))
print(squares_gen)
# <generator object <genexpr> at 0x...>
print(type(squares_gen))   # <class 'generator'>

# 逐个取值
print(next(squares_gen))  # 0
print(next(squares_gen))  # 1
print(next(squares_gen))  # 4

5.2 内存对比(震撼!)

import sys

n = 1000000  # 100万

# 列表推导式:一次性生成 100 万个元素存在内存中
big_list = [x**2 for x in range(n)]
print(f"列表占用内存:{sys.getsizeof(big_list) / 1024 / 1024:.1f} MB")

# 生成器表达式:只创建一个生成器对象,元素还没生成
big_gen = (x**2 for x in range(n))
print(f"生成器占用内存:{sys.getsizeof(big_gen)} 字节")

结论:生成器表达式几乎不占内存,因为它只存"计算规则",不存结果。

5.3 什么时候用生成器表达式?

# 场景1:传给 sum()、max()、min() 等聚合函数
total = sum(x**2 for x in range(1000000))  # 不创建中间列表,直接累加

# 场景2:传给 ''.join()
result = ''.join(str(i) for i in range(10))  # 0123456789

# 场景3:传给 all() 或 any()
has_even = any(x % 2 == 0 for x in range(1, 11))  # True

# 场景4:传给 list()、tuple()、set() 等容器
unique_chars = set(c.lower() for c in "Hello World")  # {'h', 'e', 'l', 'o', ' ', 'w', 'r', 'd'}

5.4 列表推导式 vs 生成器表达式

特性 [x for x in ...] (x for x in ...)
返回值 列表 生成器
内存占用 大(存所有元素) 极小(只存规则)
可反复遍历 可以 不可以(一次性)
可索引访问 可以 [i] 不可以
求长度 len() 支持 不支持
适用场景 需要反复使用结果 只用一次 / 聚合计算

记忆口诀:需要反复用 → 列表推导式;只用一次或聚合 → 生成器表达式。

5.5 四者的层级关系

可迭代对象 (Iterable)
    │
    └── 生成器 (Generator)  →  自动实现了迭代器协议
            │
            ├── 生成器函数 (def + yield)
            │
            └── 生成器表达式 (x for x in ...)

关系总结:
- 所有生成器都是迭代器
- 所有迭代器都是可迭代对象
- 列表是可迭代对象,但不是迭代器

课堂小练习 4

  1. 用生成器表达式计算 1 到 1000000 中所有偶数的平方和(注意不要用列表推导式,会撑爆内存)。
  2. 用生成器表达式配合 ''.join() 生成 "0-1-2-3-4-5-6-7-8-9"
点击查看参考答案
# 练习1:生成器表达式直接传 sum()
result = sum(x**2 for x in range(1, 1000001) if x % 2 == 0)
print(f"偶数的平方和:{result}")
# 不会创建 100 万个元素的中间列表!

# 练习2:生成器表达式传给 join,但数字需要先转字符串
dash_separated = '-'.join(str(i) for i in range(10))
print(dash_separated)  # 0-1-2-3-4-5-6-7-8-9

六、itertools 常用工具库

itertools 是 Python 内置的"迭代器工具箱",提供了大量高效、内存友好的迭代工具。

import itertools

6.1 无限迭代器

# count(start, step):从 start 开始,每次加 step
counter = itertools.count(10, 2)  # 10, 12, 14, 16, ...
for i, val in enumerate(counter):
    print(val, end=" ")
    if i >= 5:
        break
# 输出:10 12 14 16 18 20


# cycle(iterable):无限循环遍历
colors = itertools.cycle(["红", "黄", "绿"])
for i, color in enumerate(colors):
    print(color, end=" ")
    if i >= 7:
        break
# 输出:红 黄 绿 红 黄 绿 红 黄


# repeat(elem, times):重复某个元素
for x in itertools.repeat("加油", 3):
    print(x)
# 输出:加油 加油 加油

6.2 排列组合

# product():笛卡尔积(所有可能的组合)
print("=== 笛卡尔积 ===")
suits = ["♠", "♥", "♦", "♣"]
ranks = ["A", "K", "Q"]
for card in itertools.product(ranks, suits):
    print(f"{card[0]}{card[1]}", end=" ")
# A♠ A♥ A♦ A♣ K♠ K♥ K♦ K♣ Q♠ Q♥ Q♦ Q♣


# permutations():排列(顺序有关)
print("\n\n=== 排列(3选2)===")
for p in itertools.permutations("ABC", 2):
    print("".join(p), end=" ")
# AB AC BA BC CA CB


# combinations():组合(顺序无关)
print("\n\n=== 组合(3选2)===")
for c in itertools.combinations("ABC", 2):
    print("".join(c), end=" ")
# AB AC BC


# combinations_with_replacement():可重复组合
print("\n\n=== 可重复组合(3选2)===")
for c in itertools.combinations_with_replacement("ABC", 2):
    print("".join(c), end=" ")
# AA AB AC BB BC CC

6.3 合并与拆分迭代器

# chain():把多个可迭代对象串起来
result = itertools.chain([1, 2, 3], "abc", [True, False])
print(list(result))
# [1, 2, 3, 'a', 'b', 'c', True, False]


# chain.from_iterable():扁平化一层嵌套
nested = [[1, 2], [3, 4], [5, 6]]
flat = itertools.chain.from_iterable(nested)
print(list(flat))
# [1, 2, 3, 4, 5, 6]


# zip_longest():不等长合并,短的用填充值补齐
names = ["小明", "小红", "小刚"]
scores = [85, 90]  # 只有两个成绩
for name, score in itertools.zip_longest(names, scores, fillvalue=0):
    print(f"{name}: {score}")
# 小明: 85
# 小红: 90
# 小刚: 0

6.4 过滤与分组

# compress():按掩码过滤
data = ["a", "b", "c", "d", "e"]
selectors = [True, False, True, False, True]
result = itertools.compress(data, selectors)
print(list(result))
# ['a', 'c', 'e']


# dropwhile():跳过开头的元素直到条件为 False
nums = [1, 3, 5, 2, 4, 6]
result = itertools.dropwhile(lambda x: x < 5, nums)
print(list(result))
# [5, 2, 4, 6]  —— 跳过 1, 3(都小于5),遇到 5 后不再跳过


# takewhile():取开头的元素直到条件为 False
result = itertools.takewhile(lambda x: x < 5, nums)
print(list(result))
# [1, 3]  —— 取 1, 3(都小于5),遇到 5 后停止


# filterfalse():保留条件为 False 的元素(和 filter() 相反)
result = itertools.filterfalse(lambda x: x % 2 == 0, range(10))
print(list(result))
# [1, 3, 5, 7, 9]  —— 过滤掉偶数


# groupby():按相邻相同值分组(注意:需要先排序!)
data = [1, 1, 2, 3, 3, 3, 1, 1]  # 注意最后的 1,1 和前面的 1,1 不在同一组!
for key, group in itertools.groupby(data):
    print(f"{key} → {list(group)}")
# 1 → [1, 1]
# 2 → [2]
# 3 → [3, 3, 3]
# 1 → [1, 1]    ← 和前面的 1 分开了!因为不相邻

# 正确做法:先排序
data_sorted = sorted(data)
for key, group in itertools.groupby(data_sorted):
    print(f"{key} → {list(group)}")
# 1 → [1, 1, 1, 1]
# 2 → [2]
# 3 → [3, 3, 3]

6.5 生成新序列

# accumulate():累积计算
nums = [1, 2, 3, 4, 5]
print(list(itertools.accumulate(nums)))
# [1, 3, 6, 10, 15]  ← 累积求和

print(list(itertools.accumulate(nums, lambda x, y: x * y)))
# [1, 2, 6, 24, 120]  ← 累积求积(阶乘!)


# pairwise():相邻成对(Python 3.10+)
print(list(itertools.pairwise("ABCDE")))
# [('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'E')]


# starmap():把参数元组解包后传给函数
pairs = [(2, 3), (4, 5), (6, 7)]
result = itertools.starmap(lambda x, y: x * y, pairs)
print(list(result))
# [6, 20, 42]  ← 等价于 2*3, 4*5, 6*7


# islice():对迭代器切片(因为迭代器不支持 [start:stop:step])
counter = itertools.count()  # 无限迭代器
slice_result = itertools.islice(counter, 10, 20)  # 取第 10 到第 19 个
print(list(slice_result))
# [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]


# tee():把迭代器分身成多个(独立消费)
original = iter([1, 2, 3])
copy1, copy2 = itertools.tee(original, 2)  # 分成两份
print(list(copy1))  # [1, 2, 3]
print(list(copy2))  # [1, 2, 3]  ← 两者独立!但原始迭代器被消耗了

6.6 itertools 速查表

函数 作用 示例输入 示例输出
count(5, 2) 无限计数 - 5, 7, 9, 11, ...
cycle("AB") 无限循环 "AB" A, B, A, B, ...
repeat("X", 3) 重复 "X", 3 X, X, X
chain("AB", "CD") 串联 "AB", "CD" A, B, C, D
compress("ABC", [1,0,1]) 掩码过滤 - A, C
dropwhile(<3, [1,4,2]) 跳过开头 - 4, 2
takewhile(<3, [1,4,2]) 取开头 - 1
accumulate([1,2,3]) 累积 [1,2,3] 1, 3, 6
product("AB", "12") 笛卡尔积 - (A,1),(A,2),(B,1),(B,2)
permutations("AB", 2) 排列 "AB", 2 (A,B),(B,A)
combinations("AB", 2) 组合 "AB", 2 (A,B)
groupby("AAB") 相邻分组 "AAB" (A,[A,A]),(B,[B])

课堂小练习 5

  1. itertools.product() 生成一副扑克牌(4 种花色 × 13 个点数)。
  2. itertools.groupby() 统计字符串 "aaabbbccaaa" 中每个连续字符组的长度。
点击查看参考答案
import itertools

# 练习1:生成扑克牌
suits = ["♠", "♥", "♦", "♣"]
ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
deck = [f"{r}{s}" for r, s in itertools.product(ranks, suits)]
print(f"扑克牌共 {len(deck)} 张:")
print(deck[:13])  # 只打印前 13 张(A 的所有花色)

# 练习2:统计连续字符组
s = "aaabbbccaaa"
for char, group in itertools.groupby(s):
    print(f"'{char}' 连续出现了 {len(list(group))} 次")
# 输出:
# 'a' 连续出现了 3 次
# 'b' 连续出现了 3 次
# 'c' 连续出现了 2 次
# 'a' 连续出现了 3 次

七、综合实战案例

案例 1:日志文件逐行分析器

import itertools

def analyze_log(filepath, keyword):
    """分析日志文件,找出包含关键字的行及其上下文"""
    with open(filepath, "r", encoding="utf-8") as f:
        # 给每行加上行号
        numbered_lines = enumerate(f, start=1)

        # 过滤出包含关键字的行
        matched = ((num, line.strip()) for num, line in numbered_lines
                   if keyword in line)

        # 只取前 10 条匹配
        for num, line in itertools.islice(matched, 10):
            print(f"[行 {num}] {line}")

# 模拟:analyze_log("server.log", "ERROR")

案例 2:滑动窗口(求移动平均线)

import itertools

def sliding_window(iterable, n):
    """滑动窗口:每次取连续 n 个元素"""
    # 创建 n 个独立迭代器
    iterators = itertools.tee(iterable, n)
    # 每个迭代器依次多走一步
    for i, it in enumerate(iterators):
        for _ in range(i):
            next(it, None)
    # 打包
    return zip(*iterators)

# 股价数据
prices = [10, 12, 11, 13, 15, 14, 16, 18, 17, 20]

# 计算 3 日移动平均线
for day, window in enumerate(sliding_window(prices, 3), start=3):
    avg = sum(window) / 3
    print(f"第 {day} 日 3日均线:{avg:.1f}")

# 输出:
# 第 3 日 3日均线:11.0    ← (10+12+11)/3
# 第 4 日 3日均线:12.0    ← (12+11+13)/3
# 第 5 日 3日均线:13.0    ← (11+13+15)/3
# ...

案例 3:分页读取大量数据

import itertools

def paginate(iterable, page_size):
    """将迭代器分页,每页 page_size 条"""
    iterator = iter(iterable)
    while True:
        page = list(itertools.islice(iterator, page_size))
        if not page:
            break
        yield page

# 模拟 100 条数据
data = range(1, 101)

for page_num, page in enumerate(paginate(data, 15), start=1):
    print(f"=== 第 {page_num} 页 ({len(page)} 条) ===")
    print(page)
    if page_num >= 3:
        break  # 演示只取前 3 页

# 输出:
# === 第 1 页 (15 条) ===
# [1, 2, 3, ..., 15]
# === 第 2 页 (15 条) ===
# [16, 17, ..., 30]
# === 第 3 页 (15 条) ===
# [31, 32, ..., 45]

八、常见面试题与易错点

8.1 易错点汇总

错误 1:迭代器和可迭代对象搞混

my_list = [1, 2, 3]
next(my_list)  # TypeError: 'list' object is not an iterator

# 修正:
my_iter = iter(my_list)
next(my_iter)  # 1

错误 2:迭代器只能遍历一次

gen = (x for x in range(3))
print(sum(gen))  # 3  (0+1+2)
print(sum(gen))  # 0  ← 迭代器已经空了!

错误 3:生成器表达式外面需要加括号的场景

# 作为唯一参数时,可以省略外层括号
sum(x**2 for x in range(10))  # OK,单参数

# 作为非唯一参数时,必须加括号
# sum(x**2 for x in range(10), 100)  # 语法错误!
sum((x**2 for x in range(10)), 100)  # OK

错误 4:groupby 需要先排序

# 错误:直接 groupby 没有排序的数据
data = [("A", 1), ("B", 2), ("A", 3)]
for key, group in itertools.groupby(data, key=lambda x: x[0]):
    print(f"{key}: {list(group)}")
# A: [('A', 1)]      ← "A" 被分成了两组!
# B: [('B', 2)]
# A: [('A', 3)]

# 修正:先按 key 排序
data_sorted = sorted(data, key=lambda x: x[0])
for key, group in itertools.groupby(data_sorted, key=lambda x: x[0]):
    print(f"{key}: {list(group)}")
# A: [('A', 1), ('A', 3)]  ← 正确!
# B: [('B', 2)]

错误 5:在生成器中进行类型检查的时机

def my_gen():
    yield 1
    yield 2
    yield 3

gen = my_gen()
# 生成器被 for 循环(或 list())消费后才停止
# 不能在还没消费时就判断 "是否为空"

8.2 经典面试题

题 1:以下代码输出什么?

def foo():
    for i in range(3):
        yield i

gen = foo()
print(next(gen))
print(next(gen))
print(list(gen))
答案
0
1
[2]

解析:前两次 next() 取走了 0 和 1,剩下的 list(gen) 只能拿到 [2]。


题 2:如何判断一个对象是生成器还是迭代器?

答案
from collections.abc import Generator, Iterator

gen = (x for x in range(3))
print(isinstance(gen, Generator))   # True  ← 生成器表达式是 Generator
print(isinstance(gen, Iterator))    # True  ← Generator 是 Iterator 的子类

it = iter([1, 2, 3])
print(isinstance(it, Generator))    # False ← 普通迭代器不是 Generator
print(isinstance(it, Iterator))     # True

题 3:用生成器实现 range(start, stop, step)

答案
def my_range(start, stop=None, step=1):
    if stop is None:
        start, stop = 0, start
    if step <= 0:
        raise ValueError("step 必须为正数")
    current = start
    while current < stop:
        yield current
        current += step

print(list(my_range(5)))        # [0, 1, 2, 3, 4]
print(list(my_range(2, 6)))     # [2, 3, 4, 5]
print(list(my_range(1, 10, 2))) # [1, 3, 5, 7, 9]

总结

迭代器与生成器知识地图:

                        可迭代对象 (Iterable)
                        (有 __iter__ 方法)
                               │
                    iter() 转换 ↓
                               │
                        迭代器 (Iterator)
                  (有 __iter__ + __next__ 方法)
                     ┌─────────┴─────────┐
                     ↓                   ↓
              普通迭代器               生成器 (Generator)
         (手写 __next__)      (自动实现迭代器协议)
                                    ┌─────────┴─────────┐
                                    ↓                   ↓
                             生成器函数              生成器表达式
                           (def + yield)          (x for x in ...)
                                                         │
                                                         ↓
                                                   itertools
                                              (现成的迭代工具)

核心心法三句话:

  1. 可迭代对象是"花名册",迭代器是"正在点名的人"
  2. yield 是"游戏存档点",暂停后下次从这继续
  3. 生成器表达式是"内存环保的列表推导式",用圆括号代替方括号
0
博主关闭了当前页面的评论