如果一个函数体内部使用yield关键字,这个函数就称为生成器函数,生成器函数调用时产生的对象就是生成器。生成器是一个特殊的迭代器,在调用该生成器函数时,Python会自动在其内部添加__iter__()方法和__next__()方法。把生成器传给 next() 函数时, 生成器函数会向前继续执行, 执行到函数定义体中的下一个 yield 语句时, 返回产出的值, 并在函数定义体的当前位置暂停, 下一次通过next()方法执行生成器时,又从上一次暂停位置继续向下……,最终, 函数内的所有yield都执行完,如果继续通过yield调用生成器, 则会抛出StopIteration 异常——这一点与迭代器协议一致。
>>> from collections.abc import Iterable >>> from collections.abc import Iterator >>> def gen(): print('第1次执行') yield 1 print('第2次执行') yield 2 print('第3次执行') yield 3 >>> g = gen() >>> isinstance(g, Iterable) True >>> isinstance(g, Iterator) True >>> g <generator object gen at 0x0000021CE9A39A98> >>> next(g) 第1次执行 1 >>> next(g) 第2次执行 2 >>> next(g) 第3次执行 3 >>> next(g) Traceback (most recent call last): File "<pyshell#120>", line 1, in <module> next(g) StopIteration
可以看到,生成器的执行机制与迭代器是极其相似的,生成器本就是迭代器,只不过,有些特殊。那么,生成器特殊在哪呢?或者说,有了迭代器,为什么还要用生成器?
从上面的介绍和代码中可以看出,生成器采用的是一种惰性计算机制,一次调用也只会产生一个值,它不会将所有的值一次性返回给你,你需要一个那就调用一次next()方法取一个值,这样做的好处是如果元素有很多(数以亿计甚至更多),如果用列表一次性返回所有元素,那么会消耗很大内存,如果我们只是想要对所有元素依次一个一个取出来处理,那么,使用生成器就正好,一次返回一个,并不会占用太大内存。
举个例子,假设我们现在要取1亿以内的所有偶数,如果用列表来实现,代码如下:
def fun_list(): index = 1 temp_list = [] while index < 100000000: if index % 2 == 0: temp_list.append(index) print(index) index += 1 return temp_list
上面程序会先获取所有符合要求的偶数,然后一次性返回。如果你运行了代码,你就会发现两个问题——运行时间很长、消耗很多内存。
有时候,我们并不一定需要一次性获得所有的对象,需要一个使用一个就可以,这样的话,可以用生成器来实现:
>>> def fun_gen(): index = 1 while index < 100000000: if index % 2 == 0: yield index index += 1 >>> fun_gen() <generator object fun_gen at 0x00000222DC2F4360> >>> g = fun_gen() >>> next(g) 2 >>> next(g) 4 >>> next(g) 6
看到了吗?对生成器没执行一次next()方法,就会返回一个元素,这样的话无论在速度上还是机器性能消耗上都会好很多。如果你还没感受到生成器的优势,我再说一个应用场景,假如需要取出远程数据库中的100万条记录进行处理,如果一次性获取所有记录,网络带宽、内存都会有很大消耗,但是如果使用生成器,就可以取一条,就在本地处理一条。
不过,生成器也有不足,正因为采用了惰性计算,你不会知道下一个元素是什么,更不会知道后面还有多少元素,所以,对于列表、元组等结构,我们能调用len()方法获知长度,但是对于生成器却不能。
总结一下迭代器与生成器的异同:
(1)生成器是一种特殊的迭代器,拥有迭代器的所有特性;
(2)迭代器使用return返回值而生成器使用yield返回值每一次对生成器执行next()都会在yield处暂停;