Python(三十五) 迭代器与生成器完整教程
目录
- 开篇:从"点名册"理解迭代
- 可迭代对象与迭代器协议
- next() 与 iter():手动操控迭代
- 生成器函数:用 yield 制造数据
- 生成器表达式:一行代码的生成器
- itertools 常用工具库
- 综合实战案例
- 常见面试题与易错点
一、开篇:从"点名册"理解迭代
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
新手易踩坑:
- 坑 1:
gen.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 到 1000000 中所有偶数的平方和(注意不要用列表推导式,会撑爆内存)。
- 用生成器表达式配合
''.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
- 用
itertools.product()生成一副扑克牌(4 种花色 × 13 个点数)。 - 用
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
(现成的迭代工具)
核心心法三句话:
- 可迭代对象是"花名册",迭代器是"正在点名的人"
- yield 是"游戏存档点",暂停后下次从这继续
- 生成器表达式是"内存环保的列表推导式",用圆括号代替方括号