首页 > 技术文章 > 浅谈Python生成器

blueberry-mint 2020-08-20 17:52 原文

生成器概念

我们可以通过列表生成式创建一个列表,但是由于内存限制,列表的容量是有限的,不能占用太大的存储空间。如果我们只需要使用列表中前面几个元素,那么后面的大多数元素占用的空间都是浪费的。

因此,如果列表的元素可以按某种算法推算出来,那么我们就可以在循环的过程中推算出后续的元素,从而避免创建完整的list,并且节省了大量空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator

生成器是一个特殊的程序,可以被用作控制循环迭代。它会使用yield返回函数的值,每次调用yield会暂停,而可以使用next()和send()函数恢复生成器。

生成器类似于返回值是数组的一个函数,这个函数可以接收参数,可以被调用。但是,一般函数会一次性返回包括了所有数值的数组,而生成器一次只能产生一个值,这样消耗的内存数量将大大减小,而且允许调用函数可以很快的处理前面的几个返回值。因此生成器看起来像一个函数,实际上它也是一种迭代器,但你只能对其迭代一次,它的值是在迭代时产生的。

生成器表达式

创建一个generator的方法有很多,其中比较简单的一种方法是,把一个列表生成式的[]换成(),就可以得到一个generator。如(x for x in range(3))就是一个生成式表达式,它会返回一个生成器对象,这个对象只有在需要的时候才产生结果。

# 列表生成式
l = [x * 2 for x in range(10)]
print(l)

# 生成器表达式
g = (x*2 for x in range(10))
print(g)

结果如下:

[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
<generator object <genexpr> at 0x00000227FEC04A48>

可以看出,列表生成式得到的是一个列表,而生成器得到的是一个generator对象(可迭代对象)。

我们可以用for循环遍历generator对象获取里面的每一个值

g = (x*2 for x in range(3))
for i in g:
    print(i)

结果如下:

0
2
4

也可以用next()方法得到下一个返回值

g = (x*2 for x in range(3))
print(next(g))
print(next(g))
print(next(g))
print(next(g))

结果如下:

0
2
4
Traceback (most recent call last):
  File "generator.py", line 10, in <module>
    print(next(g))
StopIteration

next()方法会不断取下一个值,直到没有找到元素为止,然后抛出StopIteration异常。

生成器表达式来源于迭代和列表解析的组合,生成器和列表解析类似。

生成器表达式对内存空间进行优化,它不需要像方括号的列表解析一样,一次构造出整个结果列表。它运行起来比列表解析式稍微慢些,适用于非常大的结果集合运算。

生成器函数

定义:一个用def定义的函数,利用关键字yield一次性返回一个结果。

一般函数在执行完毕后会返回一个值然后退出,但是生成器函数会自动挂起,然后等待继续执行。它利用yield关键字挂起函数,给调用者返回一个值,同时保留当前的状态,等待指令继续执行函数。

生成器和迭代器协议是密切相关的,迭代器都有一个__next__()成员方法,这个方法要么返回迭代下一项,要么抛出StopIteration异常结束迭代。

我们将上面的生成器表达式改造成生成器函数如下:

def get_even(n):
    for x in range(n):
        yield x * 2

g = get_even(3)
print(g)

得到g的值为<generator object get_even at 0x0000029E955B7648>,可以看出g是一个生成器对象。

用next()方法取生成器g的下一个值

print(next(g))
print(next(g))
print(next(g))
print(next(g))

结果如下:

0
2
4
Traceback (most recent call last):
  File "generator.py", line 10, in <module>
    print(next(g))
StopIteration

既然g是一个生成器,那么我们也可以用for循环来遍历

for i in get_even(3):
    print(i)

结果如下:

0
2
4

知识点总结

  1. 可迭代对象可以用for循环进行遍历,如果这个迭代对象是字符串、列表、文件等,那么遍历前数据都生成并存放在内存中,这样会非常耗内存。

  2. 生成器是可迭代的,但只能迭代一次。

  3. 生成器之所以可迭代是因为它含有__next__()方法,工作原理就是通过重复调用next()方法,直到捕获一个异常。

  4. 带有yield的函数不再是一个普通的函数,而是一个生成器generator,可用于迭代。

  5. yield是一个类似return的关键字,迭代一次遇到yield的时候就返回yield后面的值。而且下一次迭代的时候,从上一次迭代遇到的yield后面的代码开始执行。

  6. 带有yield的函数不仅仅只是用于for循环,还可以用作某个函数的参数,只要这个函数的参数也允许迭代参数。

推荐阅读