⌨️ 第四章 流程图与算法
📄 一句话概括

你每天写的 for x in ... 底下都站着一个迭代器:一个知道【下一个是什么】的对象,按需吐一个、吐完拉倒——正因为【按需】,它可以一次只生产一个数据,一亿个数字也吃不了一亿个的内存。

📝 本节和前面几节的关系

4-3 的 for 是【用法】,这一篇掀开它的【机器盖】。回收 3-8 的数组(可迭代的代表)与 3-13 的字典(键的迭代)——能被 for 遍历的东西,背后全是迭代协议。

一、可迭代对象与迭代器:两台机器

nums = [10, 20, 30]
it = iter(nums)        # 从可迭代对象开出迭代器
print(next(it))        # 10
print(next(it))        # 20
print(next(it))        # 30
print(next(it))        # StopIteration!——for 就是替你默默接住这个异常然后收工

for 循环的真相:for x in nums 等价于【开迭代器 → 不停 next → 接住 StopIteration 就结束】——你写了十年的 for,底层全是这一套。

二、生成器:自己造一台迭代器

生成器函数用 yield 代替 return——函数【暂停在 yield 处】,下次要值再从原地继续:

def countdown(n):
    while n > 0:
        yield n        # 吐一个、暂停;下次 next 从这行后面继续
        n -= 1

for x in countdown(3):
    print(x)           # 3 2 1

惰性求值的威力:数据【要一个才生产一个】:

def big():
    for i in range(100_000_000):   # 一亿个数
        yield i
# 内存里永远只有【当前这一个】——列表版则会真存一亿个数字

适用分界:要反复访问、要切片、要知道长度——用列表;只扫一遍(读大文件逐行、过滤流水线)——用生成器,内存账省到姥姥家。

三、动手 Demo:next 现场与内存对比

def even_up_to(n):
    for i in range(0, n + 1, 2):
        yield i

it = even_up_to(10)
print(next(it), next(it), next(it))   # 0 2 4
print(list(it))                        # [6, 8, 10] —— 从 6 接着吐到头

for x in even_up_to(8):                # 同一函数可反复开出新迭代器
    print(x, end=" ")
print()

看到什么算成功:0 2 4、[6, 8, 10]、0 2 4 6 8。

读结果:前三个 next 吐 0/2/4 后机器记住了位置——list(it) 从 6 接着吐,说明迭代器【只此一份、用过不回】;同一函数再次 for 又从 0 开始(新开了一台机器)。

五、30 秒实验

跑 it = iter("你好") 然后连续 next(it)——第一次得到字符【你】。字符串也是可迭代对象,逐【字符】吐(回收 3-9:Python 按字符不按字节)。再跑 next(it) 三次,看 StopIteration 长什么样。

六、常见误区

  1. 【列表和迭代器差不多,随便换】——差远了。列表【存着全部、可反复访问】;迭代器【边产边发、一次性】——迭代器走完就空了,list(it) 之后 it 已经是空机器。
  2. 【生成器是语法糖,没实际好处】——错。一亿行日志逐行处理,列表版直接把内存吃满(可能几十 GB),生成器版内存恒定几 MB——不是优化技巧,是能不能跑起来的分界。
  3. 【迭代器只能迭代列表这类容器】——错。文件对象、range、字典、甚至无限序列(itertools.count())都能迭代——只要实现迭代协议。
自测三问(点开对答案) 1. **可迭代对象与迭代器的关系?** 答:可迭代对象能开出迭代器(iter);迭代器持进度、每次 next 吐一个,吐完抛 StopIteration——for 底层就是这一套。 2. **生成器为什么省内存?** 答:yield 让函数暂停续跑,数据按需生产、内存里永远只有当前一个;列表则一次性把全部装进内存。 3. **迭代器遍历完再取值会怎样?** 答:抛 StopIteration(for 内部接住即收工);手动 next 就是你亲眼看到这个异常——迭代器一次性、用过不回。

相关笔记