深入理解Python中的生成器
我们可以通过列表生成直接创建列表。然而,由于内存的限制,列表容量必须有限。此外,创建一个包含100万元素的列表
手表不仅占用了很大的存储空间,而且如果我们只需要访问前几个元素,那么后面绝大多数元素占用的空间就被浪费了。
因此,如果列表元素可以按照某种算法计算,我们能在循环过程中不断计算后续元素吗?这样,就没有必要创建一个完整的列表,以节省大量的空间。在Python中,这种循环计算机制被称为生成器(Generator)。
创建generator的方法有很多。第一种方法很简单。只要将列表生成式[]改为(),就会创建generator:
>>>L=[x*xforxinrange(10)]>>>L [0,1,4,9,16,25,36,49,64>>> g=(x*xforxinrange(10))>>>g <generatorobject<genexpr>at0x104feab4>
L和g的区别只在于最外层的[]和(),L是list,g是generator。
我们可以直接打印list的每一个元素,但是如何打印generator的每一个元素呢?
若要逐一打印,可通过generatornext()方法:
>>>g.next() 0 >>>g.next() 1 >>>g.next() 4 >>>g.next() 9 >>>g.next() 16 >>>g.next() 25 >>>g.next() 36 >>>g.next() 49 >>>g.next() 64 >>>g.next() 81 >>>g.next() Traceback(mostrecentcalllast): File"<stdin>",line1,in<module> StopIteration
正如我们所说,generator保存算法。每次调用next(),计算下一个元素的值,直到计算到最后一个元素,没有更多的元素,抛出stopiteration的错误。
当然,以上不断调用next()的方法太不正常了。正确的方法是使用for循环,因为generator也是可迭代的对象:
>>>g=(x*xforxinrange(10)) >>>forning: ...printn ... 0 1 4 9 16 25 36 49 64 81
因此,在我们创建了一个generator之后,我们基本上永远不会调用next()方法,而是通过for循环迭代它。
generator非常强大。假如算法比较复杂,用类似列表生成的for循环无法实现,也可以用函数来实现。
例如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任何一个数都可以从前两个数加起来:
1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波拉契数列不能用列表生成式写出来,但很容易用函数打印出来:
deffib(max): n,a,b=0,0,1 whilen<max:printb a,b=b,a+b n=n+1
以上函数可输出斐波那契数列的前N个数:
>>>fib(6) 1 1 2 3 5 8
仔细观察,我们可以看到fib函数实际上定义了斐波拉契数列的计算规则,可以从第一个元素开始计算后续任何元素。这种逻辑实际上与generator非常相似。
也就是说,上面的函数离generator只有一步之遥。要把fib函数变成generator,只需要print b改为yield b就可以了:
deffib(max): n,a,b=0,0,1 whilen<max: yieldb a,b=b,a+b n=n+1
这是定义generator的另一种方法。如果一个函数定义包含yield关键字,那么这个函数不再是一个普通的函数,而是一个generator:
>>>fib(6) <generatorobjectfibat0x104feaaa>
在这里,最难理解的是generator和函数的执行过程是不同的。函数是按顺序执行的,遇到return语句或最后一行函数语句时返回。成为generator的函数在每次调用next()时执行,遇到yield语句返回时继续从上次返回的yield语句执行。
举个简单的例子,定义一个generator,依次返回数字1、3、5:
>>>defodd(): ...print'step1' ...yield1 ...print'step2' ...yield3 ...print'step3' ...yield5 ... >>>o=odd() >>>o.next() step1 1 >>>o.next() step2 3 >>>o.next() step3 5 >>>o.next() Traceback(mostrecentcalllast): File"<stdin>",line1,in<module> StopIteration
可以看出,odd不是普通函数,而是generator。在执行过程中,yield被中断,下次将继续执行。执行三次yield后,没有yield可以执行,因此第四次调用next()报告错误。
回到fib的例子中,如果我们在循环过程中不断调用yield,我们将继续中断。当然,我们应该为退出循环设置一个条件,否则会列出一个数字。
同样,在将函数改为generator后,我们基本上从不使用next()来调用它,而是直接使用for循环来迭代:
>>>forninfib(6): ...printn ... 1 1 2 3 5 8
小结
generator是一种非常强大的工具,在Python中,可以简单地将列表生成式改为generator,也可以通过函数实现复杂逻辑的generator。
要了解generator的工作原理,它是在for循环过程中不断计算下一个元素,并在适当的条件下结束for循环。对于函数改成的generator,遇到return语句或执行到函数体的最后一行语句是结束generator的指令,for循环结束。