Python 迭代器与生成器

本文面向已有前端开发基础、正在学习 Python 的开发者。

迭代器和生成器解决的是同一个问题:数据不一定要一次性全部准备好,可以在需要的时候一个一个取出来。前端里最接近的经验是 for...ofSymbol.iterator、生成器函数 function*yield

这几个概念可以先合在一起记:

  • 可迭代对象表示"可以被遍历的数据源"
  • 迭代器表示"真正负责一步一步取值的对象"
  • 生成器表示"用 yield 快速创建出来的迭代器"
    后面的 for 循环,本质上就是先从可迭代对象拿到迭代器,再不断从迭代器里取下一个值。

一、先把概念边界讲清楚

先记住一条主线:

python 复制代码
for item in obj
  -> 先调用 iter(obj) 拿到迭代器
  -> 再不断调用 next(迭代器)
  -> 遇到 StopIteration 后结束

所以这几个概念可以这样分:

概念 关注点 Python JavaScript
可迭代对象 能不能开始遍历 能被 iter() 接受 Symbol.iterator
迭代器 这次遍历走到哪里了 能被 next() 调用 next()
迭代协议 遍历接口怎么约定 iter() -> next() -> 结束时抛异常 Symbol.iterator -> next()
生成器 怎么快速创建迭代器 函数体里写 yield function* + yield

两门语言只是接口名字和结束方式不同:

css 复制代码
Python:
可迭代对象 -- iter() --> 迭代器 -- next() --> value / StopIteration

JS:
可迭代对象 -- Symbol.iterator() --> 迭代器 -- next() --> { value, done }

最关键的边界是:可迭代对象表示"能开始一次遍历",迭代器表示"这次遍历本身"。列表、字符串这类可迭代对象可以反复遍历,因为每次都能创建新的迭代器;已经创建出来的迭代器通常只能向前走,取过的值不会自动回到起点。

生成器不算新的遍历体系,它只是更省事的迭代器写法。手写迭代器要自己维护位置和结束条件;生成器用 yield 保存暂停点,每次 next() 都从上一次暂停的位置继续执行。

是不是有点懵😳

二、从 for 循环看迭代过程

JavaScript 里,一个对象只要实现了 Symbol.iterator,就可以被 for...of 消费。

js 复制代码
const names = ["张三", "李四", "王五"];

for (const name of names) {
  console.log(name);
}

如果拆开看,for...of 背后大概做了这些事:

js 复制代码
const iterator = names[Symbol.iterator]();

console.log(iterator.next()); // { value: '张三', done: false }
console.log(iterator.next()); // { value: '李四', done: false }
console.log(iterator.next()); // { value: '王五', done: false }
console.log(iterator.next()); // { value: undefined, done: true }

所以前端里有两层概念:

概念 判断方式 作用
iterable Symbol.iterator 可以交给 for...of
iterator next() 可以一步一步取值

Python 也有这两层,只是名字和结束方式不同:

JavaScript Python
obj[Symbol.iterator]() iter(obj)
iterator.next() next(iterator)
返回 { value, done } 返回本次值
done: true 表示结束 抛出 StopIteration 表示结束

把这个对照关系记住,后面的 Python 语法就会清楚很多。

三、可迭代对象 iterable

可迭代对象就是:能被 for 循环遍历的对象。

python 复制代码
names = ['张三', '李四', '王五']
cities = ('北京', '上海', '深圳')
msg = 'hello'

for name in names:
    print(name)

这些对象都能被 for 遍历,所以它们都是可迭代对象。

从协议角度看,可迭代对象要能被 iter() 接受:

python 复制代码
names = ['张三', '李四', '王五']
msg = 'hello'
age = 18

print(iter(names)) # list_iterator
print(iter(msg))   # str_iterator

# print(iter(age)) # TypeError: 'int' object is not iterable

也可以用 hasattr 粗略观察:

python 复制代码
names = ['张三', '李四', '王五']
msg = 'hello'
age = 18

print(hasattr(names, '__iter__')) # True
print(hasattr(msg, '__iter__'))   # True
print(hasattr(age, '__iter__'))   # False

这里的 __iter__ 是 Python 的魔法方法。平时开发一般不直接写 names.__iter__(),而是用内置函数 iter(names)

python 复制代码
obj.__iter__()
  -> 底层魔法方法

iter(obj)
  -> 日常使用方式
  -> 内部会调用 obj.__iter__()

四、迭代器 iterator

调用 iter(可迭代对象) 之后,会得到一个迭代器。

python 复制代码
names = ['张三', '李四', '王五']

it = iter(names)

print(next(it)) # 张三
print(next(it)) # 李四
print(next(it)) # 王五
print(next(it)) # StopIteration

迭代器的核心能力是:记住当前取到哪里了,每次 next() 返回下一个值。

也就是说,迭代器内部有状态,类似一个指针:

python 复制代码
初始位置
  -> next() 取第 1 个
  -> next() 取第 2 个
  -> next() 取第 3 个
  -> 没有数据了,抛 StopIteration

如果用 while 手动模拟 for,大概是这样:

python 复制代码
names = ['张三', '李四', '王五']

it = iter(names)

while True:
    try:
        item = next(it)
        print(item)
    except StopIteration:
        break

所以 for item in names 并不神秘,它背后就是:

scss 复制代码
先调用 iter(names) 得到迭代器
再不断调用 next(迭代器)
遇到 StopIteration 后结束循环

迭代器自己也是可迭代对象

迭代器一般也有 __iter__ 方法,并且返回自己。

python 复制代码
names = ['张三', '李四', '王五']

it = iter(names)

print(iter(it) is it) # True

这样设计的原因是:for 循环第一步一定会调用 iter(x)。如果传进去的已经是迭代器,iter(迭代器) 必须也能正常工作。

迭代器会被消耗

迭代器不是列表,它是一次性向前取值的过程。

python 复制代码
names = ['张三', '李四', '王五']

it = iter(names)

print(next(it)) # 张三

for name in it:
    print(name)

# 只会继续输出:
# 李四
# 王五

前面已经被 next(it) 取走的值,不会在后面的 for 里重新出现。

这点很像前端里已经调用过几次 iterator.next() 后,再继续 for...of 或继续 .next(),状态会接着往后走,而不是自动重置。

五、自定义可迭代对象

如果希望自己的类能被 for 遍历,就要实现迭代器协议。

需求:让 Person 实例可以被遍历,依次取出姓名、年龄、性别、地址。

python 复制代码
p1 = Person('张三', 18, '男', '北京昌平')

for item in p1:
    print(item)

写法一:对象和迭代器分开

这种写法最清晰:Person 负责保存业务数据,PersonIterator 负责遍历过程。

python 复制代码
class Person:
    def __init__(self, name, age, gender, address):
        self.name = name
        self.age = age
        self.gender = gender
        self.address = address

    def __iter__(self):
        # 返回一个专门负责遍历 Person 的迭代器
        return PersonIterator(self)


class PersonIterator:
    def __init__(self, person):
        # 保存外部传进来的 Person 对象
        self.person = person
        # 记录当前取到哪个位置
        self.index = 0
        # 配置要遍历哪些字段
        self.attrs = [
            person.name,
            person.age,
            person.gender,
            person.address,
        ]

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

    def __next__(self):
        if self.index >= len(self.attrs):
            raise StopIteration

        value = self.attrs[self.index]
        self.index += 1
        return value

执行:

python 复制代码
p1 = Person('张三', 18, '男', '北京昌平')

for item in p1:
    print(item)

输出:

复制代码
张三
18
男
北京昌平

这个写法适合业务对象比较复杂的场景。业务对象和遍历状态分开,Person 不需要关心当前遍历到第几个字段。

写法二:对象自己也是迭代器

也可以让 Person 同时实现 __iter____next__

python 复制代码
class Person:
    def __init__(self, name, age, gender, address):
        self.name = name
        self.age = age
        self.gender = gender
        self.address = address
        self.attrs = [name, age, gender, address]

    def __iter__(self):
        self.index = 0
        return self

    def __next__(self):
        if self.index >= len(self.attrs):
            raise StopIteration

        value = self.attrs[self.index]
        self.index += 1
        return value

这种写法代码更少,但要注意:遍历状态放在对象自己身上。多个地方同时遍历同一个对象时,更容易相互影响。

学习阶段可以先写这种,真实业务里更推荐"对象和迭代器分开",职责更清楚。

六、为什么需要迭代器

迭代器最大的价值是惰性计算:不一次性生成所有结果,而是在需要时才计算下一个。

比如生成斐波那契数列,如果一次性生成 100000 个数字并放进列表,内存会越来越大。

python 复制代码
def fib_list(total):
    result = []
    a = 0
    b = 1

    for _ in range(total):
        result.append(a)
        a, b = b, a + b

    return result

如果改成迭代器,每次只返回当前这个数:

python 复制代码
class Fibo:
    def __init__(self, total):
        self.total = total
        self.index = 0
        self.a = 0
        self.b = 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.index >= self.total:
            raise StopIteration

        value = self.a
        self.a, self.b = self.b, self.a + self.b
        self.index += 1
        return value

使用:

python 复制代码
for number in Fibo(10):
    print(number)

迭代器适合这些场景:

  • 数据量很大,不想一次性放进内存
  • 不确定用户最终会消费多少结果
  • 数据来自文件、网络、数据库游标这类流式来源
  • 每个结果只依赖当前状态和上一个状态

七、生成器 generator

生成器可以理解成:Python 帮你自动实现迭代器协议的语法糖。

只要一个函数体里出现 yield,这个函数就不是普通函数,而是生成器函数。

python 复制代码
def demo():
    print('demo 函数开始执行了')
    print(100)
    yield '我是第 1 个 yield 返回的数据'

    a = 200
    print(a)
    yield '我是第 2 个 yield 返回的数据'

    b = 300
    print(b)
    return '执行结束'

调用生成器函数时,函数体不会立刻执行,而是返回一个生成器对象。

python 复制代码
d = demo()

print(hasattr(d, '__iter__')) # True
print(hasattr(d, '__next__')) # True

生成器对象本质上是一种迭代器,所以可以用 next() 取值:

python 复制代码
d = demo()

print(next(d))
print(next(d))

try:
    print(next(d))
except StopIteration as e:
    print(e.value) # 执行结束

执行过程可以这样理解:

python 复制代码
第一次 next()
  -> 函数从开头执行
  -> 遇到第一个 yield 暂停
  -> yield 后面的值作为本次 next() 的返回值

第二次 next()
  -> 从上次暂停的位置继续执行
  -> 遇到第二个 yield 再暂停

第三次 next()
  -> 继续执行
  -> 遇到 return
  -> 抛 StopIteration
  -> return 后面的值会放到异常对象的 value 里

生成器和普通函数最大的差异是:普通函数一次调用跑到底,生成器函数可以在 yield 处暂停,下次再接着跑。

前端里可以对照 function*

js 复制代码
function* demo() {
  console.log("demo 开始执行");
  yield "第 1 个值";
  yield "第 2 个值";
}

const d = demo();

console.log(d.next());
console.log(d.next());
console.log(d.next());

八、yield 的几个常见写法

yield 写在循环里

最常见的生成器写法,是在循环里不断 yield

python 复制代码
def fib(total):
    a = 0
    b = 1

    for _ in range(total):
        yield a
        a, b = b, a + b

使用:

python 复制代码
for number in fib(10):
    print(number)

这比手写 class Fibo 简洁很多,但效果类似:每次需要下一个值时,才继续往后计算。

yield from

yield from 可以把另一个可迭代对象里的值依次产出。

python 复制代码
def demo():
    nums = [10, 20, 30, 40]
    yield from nums

它大致等价于:

python 复制代码
def demo():
    nums = [10, 20, 30, 40]

    for num in nums:
        yield num

所以 yield from 可以记成:

arduino 复制代码
把某个可迭代对象里的数据,一个一个 yield 出去

send()

生成器除了能往外吐值,也能在继续执行时接收外部传进来的值。

python 复制代码
def demo():
    print('demo 函数开始执行了')

    a = yield '第 1 个 yield 的返回值'
    print(f'a 接收到:{a}')

    b = yield '第 2 个 yield 的返回值'
    print(f'b 接收到:{b}')

使用:

python 复制代码
d = demo()

print(next(d))        # 先启动生成器,停在第一个 yield
print(d.send('张三')) # 把 '张三' 传给变量 a,然后继续执行

try:
    d.send('李四')     # 把 '李四' 传给变量 b,然后继续执行到函数结束
except StopIteration:
    print('生成器执行结束')

注意:第一次启动生成器时不能直接传普通值,因为代码还没有运行到任何一个 yield 位置,没有地方接收这个值。

python 复制代码
d = demo()

# d.send('张三') # TypeError
d.send(None)    # 等价于 next(d)

next() 只能取值;send(value) 既能让生成器继续执行,也能把值传回上一次暂停的 yield 表达式。

九、生成器表达式

生成器表达式是一种快速创建生成器对象的写法,长得很像列表推导式。

python 复制代码
nums = [10, 20, 30, 40]

result1 = [n * 2 for n in nums]
result2 = (n * 2 for n in nums)

print(result1) # [20, 40, 60, 80]
print(result2) # <generator object ...>

区别在于:

写法 结果 是否立刻生成全部结果
[n * 2 for n in nums] 列表
(n * 2 for n in nums) 生成器对象

生成器表达式适合"每个结果只依赖当前元素"的场景。

python 复制代码
nums = [10, 20, 30, 40]

result = (n * 2 for n in nums)

for item in result:
    print(item)

它不会一次性创建 [20, 40, 60, 80],而是每次循环时才计算当前这个 item

如果数据量很小,并且后面要反复使用结果,列表推导式更直观。如果数据量很大,只需要顺序消费一遍,生成器表达式更省内存。

十、最后怎么选

可以按这个顺序判断:

rust 复制代码
只是遍历已有 list / tuple / dict / str
  -> 直接 for

想让自己的类能被 for
  -> 实现 __iter__
  -> 如果要自己控制取值过程,再实现 __next__

要一个一个惰性产出结果
  -> 优先写生成器函数 yield

只是把一个可迭代对象映射成另一个惰性结果
  -> 用生成器表达式

需要复杂状态、多个方法、可维护的对象封装
  -> 手写迭代器类

最容易混淆的点:

问题 结论
for 的一定是迭代器吗 不一定,可能只是可迭代对象
迭代器能 for 能,因为迭代器的 __iter__ 返回自己
iter(obj) 做了什么 调用 obj.__iter__(),拿到迭代器
next(it) 做了什么 调用 it.__next__(),拿下一个值
取完后怎么结束 Python 抛 StopIteration
生成器是什么 yield 自动创建出来的迭代器
生成器会立刻执行函数体吗 不会,第一次 next() 才开始执行
迭代器能重复遍历吗 通常不能,它会被消耗
相关推荐
ZC跨境爬虫1 小时前
跟着 MDN 学JavaScript day_6:JavaScript 中的基础数学——数字与运算符
开发语言·前端·javascript·学习·ecmascript
小小测试开发8 小时前
安装 Python 3.10+
开发语言·人工智能·python
KaMeidebaby9 小时前
卡梅德生物技术快报|PD1 单克隆抗体定制配套 N 糖全谱质控开发
前端·人工智能·算法·数据挖掘·数据分析
梦想不只是梦与想9 小时前
Python 中的装饰器
python·装饰器
我叫唧唧波9 小时前
Python+AI 全栈学习笔记
人工智能·python·学习
nuIl10 小时前
实现一个 Coding Agent(3):工具调用
前端·agent·cursor
nuIl10 小时前
实现一个 Coding Agent(4):ReAct 循环
前端·agent·cursor
nuIl10 小时前
实现一个 Coding Agent(1):一次 LLM 调用
前端·agent·cursor
nuIl10 小时前
实现一个 Coding Agent(2):让 LLM 流式响应
前端·agent·cursor