Python中的yield是如何使用的

[注:文中代码在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  

 isgeneratorfunction(generator1(None))  # 返回:False 
 


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

猜你喜欢

转载自blog.csdn.net/youngwhz1/article/details/51692327
今日推荐