yield关键字

[转载于:]{https://liam0205.me/2017/06/30/understanding-yield-in-python/}
yield关键字,及其相关的概念

迭代、可迭代、迭代器

迭代(iteration)与可迭代(iterable)

迭代是一种操作;可迭代是对象的一种特性
很多数据对象都是容器;它们包含了很多其它类型的元素,实际使用容器时,我们常常需要逐个获取其中的元素
逐个获取元素的过程,就是[迭代]

a_list = [1,2,3]
for i in a_list:
    print(i)

如果我们可以从一个对象中,逐个地获取元素,那么我们就说这个对象是可迭代的
Python中的顺序类型,都是可迭代的(list,tuple,string),其余包括dict,set,file也是可迭代的,对于用户自己实现的类型,如果提供了iter()或者getitem()方法,那么该类的对象也是可迭代的

迭代器(iterator)
迭代器是一种对象
迭代器抽象的是一个[数据流],是只允许迭代一次的对象,对迭代器不断调用next()方法,则可以依次获取下一个元素;当迭代器中没有元素时,调用next()方法会抛出StopIteration异常,迭代器的iter()方法返回迭代器自身;因此迭代器也是可迭代的

迭代器协议(iterator protocol)

迭代器协议指的是容器类需要包含一个特殊方法
如果一个容器类提供了iter()方法,并且该方法能返回一个能够逐个访问容器内所有元素的迭代器,则我们说该容器类实现了迭代器协议
Python中的迭代器和Python中的for循环是紧密相连的

for x in someting:
    print(x)

Python 处理 for循环时,首先会调用内建函数iter(someting),它实际上会调用something.__iter__(),返回something对应的迭代器。而后,for循环会调用内建函数next(),作用在迭代器上,获取迭代器的下一个元素,并赋值给x。此后,Python才开始执行循环体。

生成器、yield表达式

生成器函数(generator function)和生成器(generator)
** 如果一个函数包含yield表达式,那么它是一个生成器函数,调用它会返回一个特殊的迭代器,称为生成器。**

def func():
    return 1

def gen():
    yield 1

print(type(func))
print(type(gen))

print(type(func()))
print(type(gen()))

如上,生成器gen看起来和普通的函数没有太大区别。仅只是将return换成了yield。用type()函数打印二者的类型区别也能发现,funcgen都是函数。然而,二者的返回值类型就不同了。func()是一个int类型的对象;而gen()则是一个迭代器对象。

与普通函数不同,生成器函数被调用后,其函数体内的代码并不会立即执行,而是返回一个生成器(generator-iterator)。当返回生成器调用成员方法时,相应的生成器函数中的代码才会执行。

def square():
    for x in range(4):
        yield x ** 2
square_gen = square()
print(square_gen)
print(type(square_gen))
print(square_gen.__next__())
print(square_gen.__next__())

for x in square_gen:
    print(x)

前面说到,for循环会调用iter()函数,获取一个生成器;而后调用next()函数,将生成器中的下一个值赋值给x;再执行循环体。因此,上述for循环基本等价于:

geniter = square_gen.__iter__()
while True:
    x = geniter.next()  #Python3是__next__()
    print(x)

注意到,square是一个生成器函数;作为它的返回值,square_gen 已经是一个迭代器;迭代器的iter()返回它自己。因此geniter对应的生成器函数,即是square.
每次执行到x = geniter.next()时,square函数会从上一次暂停的位置开始,一直执行到下一个yield表达式,将yield关键字后的表达式列表返回给调用者,并再次暂停。注意,** 每次暂停恢复时,生成器函数的内部变量、指令指针、内部求值栈等内容和暂停时完全一致 **

生成器的方法

生成器有一些方法。调用这些方法可以控制对应的生成器函数,不过,若是生成器函数已在执行过程中,调用这些方法则会抛出ValueError异常。

  • 从上一次在yield表达式暂停的状态恢复,继续执行到下一次遇见yield表达式。当该方法被调用时,当前yield表达式的值为None,下一个yield表达式中的表达式列表会被返回给该方法的调用者。若没有遇到yield表达式,生成器函数就已经退出了,那么该方法会抛出StopIterator异常
  • generator.send(value)和generator.next()类似,差别仅在与它会将当前yield表达式的值设置为value
  • generator.throw(type[, value[, traceback[]]]):向生成器函数抛出一个类型type值为value调用栈为traceback的异常,而后让生成器函数继续执行到下一个yield表达式。其余行为与generator.next()类似.
  • generator.close():告诉生成器函数,当前生成器作废不再使用。

举例说明

如果你还是不太能理解生成器函数,那么大致上你可以这样去理解

  • 在函数开始处,加入result = list();
  • 将每个yield表达式yield expr替换为result.append(expr)
  • 在函数末尾处,加入return result

关于[下一个]yield表达式

介绍[生成器的方法]时,我们说当调用generator.next()时,生成器函数会从当前位置开始执行到下一个yield表达式。这里的[下一个]指的是执行逻辑的下一个,因此

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print(item)

def f13():
    yield 1
    while False:
        yield 2
    yield 3

for item in f13():
    print(item)

使用send()方法与生成器函数通信

def func():
    x = 1
    while True:
        y = (yield x)
        x += y

geniter = func()
geniter.next()
geniter.send(3)
geniter.send(10)

此处,生成器函数funcyield表达式,将处理好的x发送给生成器调用者;与此同时,生成器的调用者通过send函数,将外部信息作为生成器函数内部的yield表达式的值,保存在y当中,并参与后续的处理

yield的好处
Python的老用户应该会熟悉Python2中的一个特性:内建函数rangexrange,其中,range函数返回的是一个列表;而xrange返回的是一个迭代器.

  • 在Python3中,range相当于Python2中的xrange;而Python2中的range可以用list(range())来实现。

现在假设,我们有一个函数,其产出(返回值)是一个列表。而若我们知道,调用者对该函数的返回值,只有逐个迭代这一种方式。那么,如果函数生产列表中的每一个元素都需要耗费非常多的时间,或者生成所有元素需要等待很长时间,则使用yield把函数变成一个生成器函数,每次只生成一个元素,就不能节省很多开销了。


文章作者: 阿培
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 阿培 !
 上一篇
gevent简明教程 gevent简明教程
转载于: 进程 线程 协程 异步并发编程(不是并行)目前有四种方式:多进程、多线程、协程、和异步 多进程编程在python中有类似C的os.fork,更高层封装的有multiprocessing标准库 多线程编程python中有Thre
2018-08-01
下一篇 
mha原理 mha原理
转载至:MHA原理:MHA(Master High Availability)目前在MySQL高可用方面是一个相对成熟的解决方案,它由日本人youshimaton开发,是一套优秀的作为MySQL高可用性环境下故障切换和主从提升的高可用软件。
2018-07-11
  目录