首先要说明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异常精确设置,也可以在子迭代器是生成器时(子生成器会产出值)自动设置。