[注:文中代码在Python3.5中运行通过]
1. yield是Python中的关键字,带有yield的函数被称为生成器(generator)。
先来看一个使用yield的例子:
def generator1(path):
for line in open(path, mode="r", encoding="utf8"):
datetime, price = line.replace('\n', '').split(",")
yield datetime, price
def generator2(path):
for datetime, price in generator1(path):
date, time = datetime.split()
yield date, time, price
path = "demo.txt"
for date, time, price in generator2(path):
print("date:%s time:%s price:%s" % (date, time, price))
输出结果:
date:20120502 time:09:31 price:2664.02
date:20120502 time:09:32 price:2662.25
date:20120502 time:09:33 price:2659.81
date:20120502 time:09:34 price:2657.52
date:20120502 time:09:35 price:2656.07
date:20120502 time:09:36 price:2655.55
date:20120502 time:09:37 price:2656.43
date:20120502 time:09:38 price:2658.00
date:20120502 time:09:39 price:2659.27
date:20120502 time:09:40 price:2660.83
由于generator2(path)是一个generator对象,该对象具有__next__方法,运行该方法可以更直观地查看其迭代过程。当函数执行结束时,generator自动抛出StopIteration异常,表示迭代完成。在for循环里,无需处理StopIteration异常,循环会正常结束。
iterable_obj = generator2(path)
print(iterable_obj.__next__()) # 显示:('20120502', '09:31', '2664.02')
print(iterable_obj.__next__()) # 显示:('20120502', '09:32', '2662.25')
print(iterable_obj.__next__()) # 显示:('20120502', '09:33', '2659.81')
print(iterable_obj.__next__()) # 显示:('20120502', '09:34', '2657.52')
print(iterable_obj.__next__()) # 显示:('20120502', '09:35', '2656.07')
print(iterable_obj.__next__()) # 显示:('20120502', '09:36', '2655.55')
print(iterable_obj.__next__()) # 显示:('20120502', '09:37', '2656.43')
print(iterable_obj.__next__()) # 显示:('20120502', '09:38', '2658.00')
print(iterable_obj.__next__()) # 显示:('20120502', '09:39', '2659.27')
print(iterable_obj.__next__()) # 显示:('20120502', '09:40', '2660.83')
print(iterable_obj.__next__()) # 错误:StopIteration
基本上,yield关键字的作用就是把一个函数变成一个generator,带有yield关键字的函数不再是一个普通函数,Python解释器会将其看作是一个 generator,调用generator2(path)不会执行generator2函数,而是返回一个iterable对象。在 for 循环执行时,每次循环都会执行generator2函数内部的代码,执行到 yield xxx 时,generator2 函数就返回一个迭代值,下次迭代时,代码从yield xxx 的下一条语句继续执行,而函数的局部变量是保持和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到yield xxx。
带有yield的函数作为一个 generator,虽然看起来象普通函数,但是直接调用不会执行任何函数代码,直到对其调用__next__方法(在 for 循环中会自动调用__next__方法)才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。
yield函数的迭代会避免在运行中占用的内存会随着一些参数 的增大而增大(比如使用list内存就会显著增加)。所以,如果要控制内存占用,最好不要用 List来保存中间结果,而是通过 iterable 对象来迭代。例如在 Python2.x 中(Python3.x中不需要),代码:
for i in range(100): pass
会导致生成一个 100 个元素的 List,而代码:
for i in xrange(100): pass
则不会生成一个 100 个元素的 List,而是在每次迭代中返回下一个数值,内存空间占用很小。因为 xrange 不返回 List,而是返回一个 iterable 对象。
2. 如何判断一个函数是否是generator函数?
可以利用isgeneratorfunction来判断:
from inspect import isgeneratorfunction
isgeneratorfunction(generator1) # 返回:True
generator1 是一个generator function,但generator1(None)是一个实例,是调用generator1函数返回的一个generator。
通过isinstance()函数可以验证:
import types
isinstance(generator1, types.GeneratorType) # 返回:False
isinstance(generator1(None), types.GeneratorType) # 返回:True
generator1是无法迭代的,generator1(None)是可迭代的:
from collections import Iterable
isinstance(generator1, Iterable) # 返回:False
isinstance(generator1(None), Iterable) # 返回:True
每次调用generator1函数都会生成一个新的 generator 实例,不同的实例之间互不影响:
iterable_obj1 = generator2(path)
iterable_obj2 = generator2(path)
print(iterable_obj1.__next__()) # 显示:('20120502', '09:31', '2664.02')
print(iterable_obj1.__next__()) # 显示:('20120502', '09:32', '2662.25')
print(iterable_obj1.__next__()) # 显示:('20120502', '09:33', '2659.81')
print(iterable_obj2.__next__()) # 显示:('20120502', '09:31', '2664.02')
print(iterable_obj2.__next__()) # 显示:('20120502', '09:32', '2662.25')
3. yield在读取超大文件时的应用:
为避免大量的内存占用,可以使用固定长度的buffer来不断读取内容直到结束。
def read_file(path, BLOCK_SIZE=20):
with open(path, 'rb') as f:
while True:
content = f.read(BLOCK_SIZE)
if content:
yield content
else:
return
path = "demo.txt"
for content in read_file(path):
print(content)
输出结果:
b'20120502 09:31,2664.'
b'02\n20120502 09:32,26'
b'62.25\n20120502 09:33'
b',2659.81\n20120502 09'
b':34,2657.52\n20120502'
b' 09:35,2656.07\n20120'
b'502 09:36,2655.55\n20'
b'120502 09:37,2656.43'
b'\n20120502 09:38,2658'
b'.00\n20120502 09:39,2'
b'659.27\n20120502 09:4'
b'0,2660.83'
4. return在yield函数中的作用
在一个 generator function 中,如果没有 return,则默认执行至函数完毕,如果在执行过程中return,则直接抛出异常 StopIteration 终止迭代。
比如前面的generator2函数中增加return后:
def generator2(path):
count = 0
for datetime, price in generator1(path):
count += 1
if count > 5:
return
date, time = datetime.split()
yield date, time, price
在for循环中,只返回5次迭代就结束了,不会抛出异常(内部已经处理了StopIteration)。
for date, time, price in generator2(path):
print("date:%s time:%s price:%s" % (date, time, price))
输出结果:
date:20120502 time:09:31 price:2664.02
date:20120502 time:09:32 price:2662.25
date:20120502 time:09:33 price:2659.81
date:20120502 time:09:34 price:2657.52
date:20120502 time:09:35 price:2656.07
但是在调用__next__方法进行迭代时,5次迭代之后就会报错:
print(iterable_obj1.__next__())
print(iterable_obj1.__next__())
print(iterable_obj1.__next__())
print(iterable_obj1.__next__())
print(iterable_obj1.__next__())
print(iterable_obj1.__next__())
输出结果:
('20120502', '09:31', '2664.02')
('20120502', '09:32', '2662.25')
('20120502', '09:33', '2659.81')
('20120502', '09:34', '2657.52')
('20120502', '09:35', '2656.07')
Traceback (most recent call last):
StopIteration