首先要说明yield
的中文释义,“产出;生产”。和return
不同,return
是返回,即我们调用一次,就返回一次,而生成器函数的yield
可以不断的生成值。
举个不恰当的例子,函数返回一个值可以看做是一枚鸡蛋,经过一定的过程,只能孵化出一只小鸡。生成器函数内的yield
更像一只母鸡(生成器对象),不断地下蛋(产出结果)。
先看Python2
的用法
1 | In [1]: def get_generator(n): |
yield
仅在定义生成器函数时使用,并且需要在生成器函数的函数体内使用。如示例中的get_generator
函数即为生成器函数。
调用生成器函数时,会返回一个叫做生成器的迭代器。这里需要意识到生成器/迭代器是一个对象。如示例中my_generator = get_generator(5)
不会执行该函数体内的语句,而是返回了一个生成器对象,赋值给my_generator
。
执行生成器对象的方法时(next
, send
, throw
, close
下面会详细说明),生成器函数体内的代码才会执行。如示例中的第3次输入In [3]: item = my_generator.next()
,将使get_generator
函数开始执行。
当执行到yield
语句时,yield
语句中的expression_list
(表达式列表)将作为产出值返回给调用者。如示例中的(i, i + 1, )
将作为my_generator.next()
的返回结果,最终赋值给item
。
同时生成器函数在这里挂起
/冻结
(挂起
是指保留了所有本地状态,包括局部变量,指令指针和内部评估堆栈的绑定关系等,如示例中的局部变量i
),如示例中的result = yield (i, i + 1, )
,只会执行yield (i, i + 1, )
,而不会将yield
表达式的值赋值给result
(yield
表达式的值由下一次调用决定,现在还不存在)。
再次调用生成器对象的方法时,生成器函数会从挂起的地方继续执行,如示例中第5次输入In [5]: item = my_generator.next()
,next()
方法会使生成器函数内上一次挂起的yield
表达式值为None
,然后执行result =
的赋值操作。yield
语句的执行就像是发起了一次外部调用,会保留函数体内部状态,跳出当前生成器函数,执行外部代码,执行完毕后,继续执行函数体内部的代码。
生成器函数恢复执行时使用的生成器对象方法,决定了yield
表达式的值,如示例中第5、7次输入In [5]: item = my_generator.next()
、In [7]: item = my_generator.send('a')
决定了yield (i, i + 1, )
表达式的值,分别为None
和'a'
(后面详细介绍next
、send
方法)。
这些特点使得生成器功能与协程非常相似,可以多次yield
,有多个入口,执行可被暂停。唯一的区别是生成器函数无法控制yield
后继续执行的位置,总是生成器的调用者来进行控制。
生成器的方法
介绍生成器方法之前,我们先解释一下当前yield表达式
。生成器函数恢复执行时,从上一次挂起的yield
表达式的位置执行,这个yield
表达式叫做当前yield表达式
,所以在第一次执行生成器函数时,由于不存在上一次挂起,因此不会有当前yield表达式
的概念,当前yield表达式
只存在于再次恢复执行中。
生成器的方法可以控制生成器函数的执行,但在生成器对象执行过程中调用这些方法,将会发起ValueError
异常。
generator.next()
用于启动和恢复执行生成器函数。
使用next()
方法恢复执行时,会使当前yield
表达式的值为None
,并返回下一个yield
表达式列表的值。
如示例中第5次输入In [5]: item = my_generator.next()
时,首先从上一次yield
表达式挂起的位置执行,使yield (i, i + 1, )
表达式值为None
,然后赋值给result
。然后执行print
,进入下一轮循环,再次yield
时,表达式列表(i, i + 1, )
才会返回给当前的next()
函数。
如果生成器并非以yield
形式退出,那么调用next()
方法就会引起StopIteration
异常,如第11次输入。
generator.send(value)
用于生成器函数的恢复执行,并传递值。
value
参数会成为当前yield
表达式的值。如示例中第7次输入In [7]: item = my_generator.send('a')
,使当前yield
表达式值为'a'
。
send()
方法也会返回下一次yield
产出的表达式列表,或者在生成器函数没有产出值时,也引起StopIteration
异常。
如果用send()
方法启动生成器函数的话,那么value
参数必须为None
。因为第一次执行时没有当前yield表达式
,即没有yield
表达式可以接收这个传入的值。
1 | In [1]: def echo(value=None): |
我们用上面的例子来讲解throw
和close
方法。
generator.throw(type[, value[, traceback]])
在生成器挂起的位置发起type
类型的异常,然后返回下一次生成器产出的值,如果没有产出值则发起StopIteration
异常。
如示例第2-5次输入中第4次输入In [4]: print(g1.throw(TypeError))
,会在上一次挂起的位置即yield value
处发起TypeError
异常,然后被except TypeError:
捕获,然后执行yield 'type_error'
,产出值返回给调用者,所以第4次输入会打印出type_error
。
如果生成器函数没有捕捉到传入的异常,或者发起了其他异常,那么该异常会向上抛出至调用者。
generator.close()
在生成器挂起的位置发起GeneratorExit
异常。
如果生成器函数发起StopIteration
(生成器函数执行完成或执行close()
方法都会使生成器结束,再次获取产出值时会发起StopIteration
异常)或GeneratorExit
(当该异常未被捕获时)异常,那么生成器就会关闭,然后返回至调用者,并且代码不会发起异常。
如上面示例的第6-10次输入。In [6]: g2 = echo(2)
创建了一个生成器。In [7]: print(g2.next())
执行生成器,并在yield value
处挂起。In [8]: print(g2.close())
调用close()
方法,这时会在上次挂起,即yield value
处发起GeneratorExit
异常,所以被except GeneratorExit:
捕捉到,然后生成器函数执行print('GeneratorExit')
,我们可以看到输出。
生成器传入的参数为2,所以接下来会执行raise GeneratorExit
,从执行结果来看,close()
方法触发生成器函数发起的GeneratorExit
,不会传递至调用者,也没有返回值。
继续获取生成器产出值In [9]: print(g2.next())
,会发起StopIteration
,证明生成器已经被停止了。In [10]: print(g2.close())
继续执行close()
方法,会在生成器挂起的位置继续执行,但此时生成器已经停止了,会发起StopIteration
异常,示例中输出None
,表明close()
方法触发生成器函数发起的StopIteration
异常,不会传递至调用者,也没有返回值。
已关闭的生成器继续返回产出值会发起RuntimeError
异常。如示例中第10-13次输入。close()
方法会被except GeneratorExit:
捕获,根据逻辑会执行yield 3
语句,此时报错。
生成器内发起的其他异常会传递至调用者。如果生成器由于异常结束,或执行完毕结束时,调用close()
不会有任何影响。
先看Python3
的用法
这里只说相比Python2
新增的内容。
python3
中可以使用yield from expression
表达式,并且除了可以在定义生成器函数时使用yield
表达式,也可以在定义异步生成器函数(async def
定义的函数体内使用yield
)时使用。
生成器函数
try
结构中可以使用yield
表达式。如果生成器在结束(引用计数为0或被垃圾回收)之前没有恢复执行,那么生成器的close()
方法将被调用,并执行finally
语句。
yield from <expr>
语句将表达式当做子迭代器使用。所有子迭代器产出的值,都会传递给当前生成器方法的调用者。send()
方法传入的值、throw()
传入的异常都会传递至底层的迭代器(子迭代器),前提是底层的迭代器有对应的合适的方法,否则,send()
方法会引起AttributeError
或TypeError
异常,throw()
方法会立即抛出传入的异常。
底层迭代器完成之后,发起的StopIteration
对象的value
属性就会成为当前yield
表达式的值。yield
表达式的值可以由StopIteration
异常精确设置,也可以在子迭代器是生成器时(子生成器会产出值)自动设置。