跳转到内容

Python/生成器

维基教科书,自由的教学读本

生成器(generator)是一种返回一个值的迭代器,每次从该迭代器取下一个值。

生成器表达式(generator expression)类似列表解析(list comprehension)表达式的语法,只不过把列表解析的[]换成(),但是它返回的是一个生成器对象而不是列表对象。

生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅。它不需要迭代器的类一样写__iter__()和__next__()方法了,只需要一个yield关键字。 生成器一定是迭代器(反之不成立),因此任何生成器也是以一种惰性加载的模式生成值。

使用生成器表达式

[编辑]
# 生成器表达式
my_generator = (x for x in range(10))
# 遍历生成器
for item in my_generator:
   print(item)

基于yield的生成器

[编辑]

Python 2.2的PEP 255开始引入生成器。函数如果包含yield指令,该函数调用的返回值是一个生成器对象,此时函数体中的代码并不会执行,只有显示或隐示地调用next的时候才会真正执行里面的代码。yield可以暂停一个函数并返回此时的中间结果。该函数将保存执行环境并在下一次恢复。

生成器中,如果没有return语句,则执行到函数完毕时将返回StopIteration异常。如果在执行过程中遇到return语句,则直接抛出StopIteration异常,终止迭代。如果在return后返回一个值,那么这个值作为StopIteration异常的说明,不是程序的返回值。

可用close()手动关闭生成器函数,后面再调用生成器会直接返回StopIteration异常。

使用 yield 关键字创建生成器函数。生成器函数使用 yield 关键字来生成值,每次调用生成器的 __next__() 方法时,生成器会从上一次停止的地方继续执行。

# 生成器函数
def my_generator():
    for x in range(10):
        yield x

# 遍历生成器
for item in my_generator():
    print(item)

生成器函数最核心、最特殊的特性:支持通过 send() 方法从外部向生成器内部传入数据,生成器接收该数据完成运算后,再向外产出结果。这既是生成器语法中最难理解的知识点,也是协程实现的底层核心依赖。核心语法格式:

receive = yield value

执行逻辑拆解:上一轮迭代执行至 yield value 时,函数暂停,并将 value 作为产出值返回给调用方;当本次外部调用 gen.send(传入参数) 时,本次传入的参数会赋值给等式左侧变量 receive。

生成器首次激活限制 生成器初始化后,不能直接调用 send(非None值),仅允许两种启动方式:next(gen)gen.send(None);若首次传入任意非空参数,程序会直接抛出异常。因为生成器需要先执行到第一条 yield 表达式,才会进入暂停、等待外部传值的状态,首次运行前不存在可接收数据的 yield 点位。

除第一次启动外,每一次调用 next()send() 的执行顺序固定:

  1. 回到上一轮暂停的 yield 表达式位置,将本次外部传入的值赋值给 yield 左侧接收变量(若存在);
  2. 继续向下执行函数代码,直至遇见下一条 yield 语句;
  3. 将该 yield 右侧表达式的计算结果产出、返回给外部,随后再次暂停。
def cumulative_sum():
    total = 0
    while True:
        value = (yield total)
        if value is None:
            break
        total += value

gen = cumulative_sum()
print(next(gen))  # 输出: 0,启动生成器并得到初始总和

print(gen.send(10))  # 输出: 10,发送10,总和变为10
print(gen.send(20))  # 输出: 30,发送20,总和变为30
print(gen.send(5))   # 输出: 35,发送5,总和变为35

gen.close()  # 关闭生成器

throw()用来向生成器函数送入一个异常。生成器内部可以捕捉、处理此异常,也可以不处理此异常。

def my_generator():
    try:
        while True:
            yield
    except Exception as e:
        print("Error:", e)

# 使用 throw() 方法向生成器抛出异常
my_generator = my_generator()
next(my_generator)
my_generator.throw(ValueError("Error "))

基于yield from的生成器

[编辑]

Python3.3版本的PEP 380中添加了yield from语法,允许一个生成器将其部分操作委派给另一个生成器。

基于yield的生成器的局限性在于只能向它的直接调用者yield值。例如:

def g(x):
    yield 1
    yield 2

def foo():
    iterator = g(0)
    print(next(iterator))

if __name__ == '__main__':
    next(foo()) # 错误,只能直接调用生成器

这意味着那些包含yield的代码不能想其他代码那样被分离出来放到一个单独的函数中,让外界去调用这个函数。这也正是yield from要解决的。yield from为调用者和子生成器之间提供了一个透明的双向通道,包括从子生成器获取数据以及向子生成器传送数据。

虽然yield from主要设计用来向子生成器委派操作任务,但yield from可以向任意的迭代器委派操作。

yield from iterable本质上等于for item in iterable: yield item的缩写版。例如:

def g(x):
     yield from range(x, 0, -1)
     yield from range(x)

list(g(5)) #结果为[5, 4, 3, 2, 1, 0, 1, 2, 3, 4]

委托生成器的作用:在调用方和子生成器之间行成双向通道。双向通道的含义:调用方可以通过send()直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方。只有子生成器结束(return)了,yield from左边的变量才会被赋值,委托生成器后面的代码才会执行。

不同于普通的循环,yield from允许子生成器直接从调用者接收其发送的信息或者抛出调用时遇到的异常,并且返回给委派生产器一个值。如果对子生成器的调用产生StopIteration异常,委派生成器恢复继续执行yield from后面的语句;若子生成器产生其他任何异常,则都传递给委派生成器。如果GeneratorExit异常被抛给委派生成器,或者委派生成产器的close()方法被调用,如果子生成器有close()的话也将被调用。当子生成器结束并抛出异常时,yield from表达式的值是其StopIteration异常中的第一个参数。例如:

def accumulate():    # 子生成器,将传进的非None值累加,传进的值若为None,则返回累加结果
    tally = 0
    while 1:
        next1 = yield
        if next1 is None:
            return tally
        tally += next1

def gather_tallies(tallies):    # 外部生成器,将累加操作任务委托给子生成器
    while 1:
        tally = yield from accumulate()
        tallies.append(tally)

 
tallies = []
acc = gather_tallies(tallies)
next(acc)    # 使累加生成器准备好接收传入值
for i in range(4):
    acc.send(i)

acc.send(None)
for i in range(5):
    acc.send(i)
    
acc.send(None)
print(tallies) #输出[6,10]