对python中的生成器进行下总结。

在python中生成器可以很方便的实现迭代协议。生成器通过生成器函数产生,生成器函数可以通过常规的def语句来定义,但是不用return返回,而是用yield一次返回一个结果,在每个结果之间挂起和继续它们的状态,来自动实现迭代协议。也就是说,yield是一个语法糖,内部实现支持了迭代器协议。

生成器的强大之一就在于他提供了协同程序的概念,协同程序是可以运行的独立函数调用,可以暂停或者挂起,并从程序离开的地方继续或者重新开始。同时调用者也可以向程序传入额外的数据或者异常等,传入完毕后仍能在上次暂停的地方继续执行。

简单的生成器特性

我先以上一篇的列表的例子实现一个生成器:

1
2
3
def ListGenerator(data):
for i in data:
yield i

之所以我把函数写的像类一样,是因为含有yield的函数并不是一般的函数,它更像一个类,因为它返回一个对象,这个对象就叫生成器。
现在我生成一个生成器对象:

1
2
3
4
5
6
In [1]: from list_iter import *
In [2]: a = ListGenerator([1,2,3,4,5])
In [3]: a
Out[3]: <generator object ListGenerator at 0x035DA6C0>

这样我们可以像使用迭代器一样去使用生成器对象:

1
2
In [5]: [i for i in a]
Out[5]: [1, 2, 3, 4, 5]


生成器的加强特性

在python2.5中一些加强特性加入到生成器中,让生成器更加的强大,所以除了__next__()可以获取下一个生成的值,用户可以将值回送个生成器(send()),在生成器抛出异常,以及要求生成器退出(close()

send()方法

由于双向的动作涉及到一个叫send()的代码来向生成器发送值,因此现在yield必须是一个表达式,因为函数会在执行yield之后暂停,等他回来的时候必须要有一个对象接手值,因此要写成a = yield b这样。
举个简单的例子:

1
2
3
4
5
6
7
def ListGenerator(data):
for i in data:
receive = (yield i)
if receive:
print("receive {}.".format(receive))
else:
print("receive nothing.")

执行的结果:

1
2
3
4
5
6
7
8
9
10
11
12
In [47]: a = ListGenerator([1,2,3])
In [48]: a.__next__()
Out[48]: 1
In [49]: a.__next__()
receive nothing.
Out[49]: 2
In [50]: a.send(1.3)
receive 1.3.
Out[50]: 3

简单分析下上面这段程序就很清楚了:

  1. 创建生成器对象
    首先使用函数ListGenerator,python看到函数里有yield关键字就知道这个函数不一般要返回一个生成器对象,并赋值给变量a

  2. 第一次调用__next__()
    然后a就是个迭代器对象,它实现了迭代协议(即实现了__iter__()__next__()方法,其中__iter__()返回生成器自身),我们调用__next__()函数函数体就开始执行,当执行到

    1
    receive = (yield i)

    python看到了yield就把i返回并暂停函数执行。这也就是为什么第一次调用__next__()时先只输出了1。

  3. 第二次调用__next__()
    当再次调用__next__()函数时候,函数便会在上次停止的地方继续执行,上次执行到了哪里?yield执行之后是一个赋值操作,要把一个值赋给receive,但是这时候我们没有给生成器发送值,因此python默认将这个值赋值为None。然后进行下面的判断操作,由于receive的值为None所以直接打印receive nothing.,然后继续执行,这时i变为2并在再次遇到yield生成器返回2并暂停。

  4. 调用send()方法
    这是在生成器恢复执行之前,我调用了生成器的send()函数,向生成器传入了一个值1.3,传入之后生成器恢复函数的执行,将我传入的值赋值给receive然后接着向下进行判断这是由于receive的值是1.3了因此输出receive 1.3.。然后接着第三轮迭代输出3.

throw方法

throw主要是向生成器发送异常,我将上面的代码进行了修改,是生成器能够捕获异常:

1
2
3
4
5
6
7
8
9
10
def ListGenerator(data):
for i in data:
try:
receive = (yield i)
if receive:
print("receive {}.".format(receive))
else:
print("receive nothing.")
except ValueError:
print("receive a ValueError.")

在shell中执行:

1
2
3
4
5
6
7
8
9
10
In [1]: from list_iter import *
In [2]: a = ListGenerator([1,2,3,4])
In [3]: a.__next__()
Out[3]: 1
In [4]: a.throw(ValueError)
receive a ValueError.
Out[4]: 2

当第一次调用__next__()的时候,执行到yield返回1并暂停,第二次我传入了一个VauleError异常,生成器继续执行函数,由于接收到一个异常,并被下面的except ValueError捕获,这时便输出了recieve a ValueError.然后接着执行到yield返回2。

close()方法

当一个生成器是一个永远执行的时候(while True的时候),我们就用到了close()来终止它。

通过生成器,可以快速创建一个可迭代对象。

上一篇中提到过,我们可以通过将可迭代对象中的__iter__()方法返回一个迭代器对象来实现多次重复迭代。有了生成器,我们可以让可迭代对象的__iter__()方法直接返回一个生成器,也就是在__iter__()中使用yield关键字,这样创建迭代对象就很方便。我之前处理VASP文件的库VASPy中就是这样使用来从大文件中获取数据的,代码不贴上来了,直接放个链接吧: VASPy/atomco.py at master · PytLab/VASPy

Comments