Bootstrap

Python的生成器(generator)

如果函数要产生一系列结果,那么最简单的做法是把这些结果放在一份列表(list)中,并将其返回给函数调用者。例如我们要获得一份文本中每个单词的长度。

def get_word(text):
    result = []
    for i, letter in enumerate(text.split(' ')):
        result.append(len(letter))

    return result

输入验证下:

text = 'Five score years ago, a great American, in whose symbolic shadow we stand today, signed the Emancipation Proclamation.'

print(get_word(text))

输出结果:

[4, 5, 5, 4, 1, 5, 9, 2, 5, 8, 6, 2, 5, 6, 6, 3, 12, 13]

这个get_word()函数本身没有问题,但是有2个方面值得注意,一是这个函数稍显拥挤,需要预先定义一个列表,然后在循环中添加元素,最后返回,并不是很简练,当然这不是什么大问题。二是当你有大量数据并把所有值放在内存时,这种处理方式可能并不是很好,例如text文本非常大,有好几个GB,就无法使用这种方式了。

这时,需要用到生成器(generator),生成器就是使用yield表达式的函数,使用很简单,直接把你函数需要返回的列表内容前面加上yield即可。如上面那个函数,改写如下:

def gen_get_word(text):
    for i, letter in enumerate(text.split(' ')):
        yield len(letter)

可以看到,这个改写后的函数清晰了许多,没有列表的定义、添加元素、返回列表等,列表中的元素,现在都分别传给yield了。看看试验结果:

print(list(gen_get_word(text)))

输出:

[4, 5, 5, 4, 1, 5, 9, 2, 5, 8, 6, 2, 5, 6, 6, 3, 12, 13]

这和最开始的函数得到的结果是一致的。

生成器的优点是它无须将对象的所有元素都存入内存后,才开始进行操作。而是仅在迭代至某个元素时才会将该元素放入内存。这个特点使得它特别适合用于遍历一些巨大序列对象,例如大文件、大集合、大字典等。这个特点也被称为延迟计算或惰性求值(lazy evaluation),可以有效的节省内存空间。

例如,如果直接对文件对象调用read()方法,会导致不可预测的内存占用。可以用生成器读取固定长度的缓冲区,不用将文件一次性读入内存。

def read_file(path):
    with open(path, 'rb') as f:
        while True:
            block = f.read(1024)
            if block:
                yield block
            else:
                return

总结下:

  • 所有需要返回序列对象的函数都可以使用生成器。
  • 使用生成器比收集结果放入列表然后返回的方式更加简练清晰。
  • 生成器函数返回的是一个迭代器,可以把传给yield些值,逐次产生出来。
  • 生成器不会影响内存的消耗,特别适合于处理大数据文件、大的数据对象等。
;