预计阅读本页时间:-
8.1 生成器
生成器(generator)就是对象,在每次调用它的next()方法时返回一个值,直到它抛出StopIteration。生成器是在PEP 255(https://www.python.org/dev/peps/pep-0255/)中引入的,并提供了一种比较简单的实现迭代器(iterator)协议(https://docs.python.org/2/library/stdtypes.html#iterator-types)的方式来创建对象。
要创建一个生成器所需要做的只是写一个普通的包含yield语句的Python函数。Python会检测对yield的使用并将这个函数标识为一个生成器。当函数执行到yield语句时,它会像return语句那样返回一个值,但一个明显不同在于:解释器会保存对栈的引用,它将被用来在下一次调用next函数时恢复函数的执行。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
创建一个生成器
>> def mygenerator():
... yield 1
... yield 2
... yield 'a'
...
>>> mygenerator()
<generator object mygenerator at 0x10d77fa50>
>>> g = mygenerator()
>>> next(g)
1
>>> next(g)
2
>>> next(g)
'a'
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
可以通过inspect.isgeneratorfunction检查一个函数是否是生成器。
>>> import inspect
>>> def mygenerator():
... yield 1
...
>>> inspect.isgeneratorfunction(mygenerator)
True
>>> inspect.isgeneratorfunction(sum)
False
通过阅读inspect.isgeneratorfunction的源代码也可以洞察一些前面提到的生成器函数标识的内部实现。
inspect.isgeneratorfunction的源代码
def isgeneratorfunction(object):
"""Return true if the object is a user-defined generator function.
Generator function objects provides same attributes as functions.
See help(isfunction) for attributes listing."""
return bool((isfunction(object) or ismethod(object)) and
object.func_code.co_flags & CO_GENERATOR)
Python 3中提供了另一个有用的函数inspect.getgeneratorstate。
>>> import inspect
>>> def mygenerator():
... yield 1
...
>>> gen = mygenerator()
>>> gen
<generator object mygenerator at 0x94b44fec30>
>>> inspect.getgeneratorstate(gen)
'GEN_CREATED'
>>> next(gen)
1
>>> inspect.getgeneratorstate(gen)
'GEN_SUSPENDED'
>>> next(gen)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> inspect.getgeneratorstate(gen)
'GEN_CLOSED'
这个函数能够给出生成器的当前状态,允许我们判断它是否正在等待第一次被执行(GEN_CREATED),当前正在被解析器执行(GEN_RUNNING),等待被next()调用唤醒(GEN_SUSPENDED),或者已经结束运行(GEN_CLOSED)。
在Python中,生成器的构建是通过当函数产生某对象时保持一个对栈的引用来实现的,并在需要时恢复这个栈,例如,当调用next()时会再次执行。
当迭代某种类型的数据时,直观的方式是先构建整个列表,这非常浪费内存。假设我们想找到1~10 000 000的第一个等于50 000的数字。听上去很简单,不是吗?让我们来挑战一下。这里将Python的运行内存限制在128 MB。
$ ulimit -v 131072
$ python
>>> a = list(range(10000000))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
MemoryError
啊!证明不能以128 MB内存创建一个拥有1000万个元素的列表。
警告
在Python 3中,range()会返回生成器。要在Python 2中获取生成器,需要使用xrange()。(这个函数在Python 3中不存在,因为已经重复了。)
换成生成器试试。
$ python
>> for value in xrange(10000000):
... if value == 50000:
... print("Found it")
... break
...
Found it
这次程序运行没有任何问题。range()函数返回一个可迭代对象,它会动态地生成整数列表。更妙的是,我们只关心第50 000个数字,生成器会只生成50 000个数字。
生成器运行通过即时生成的值以极少的内存消耗来应对大规模的数据集和循环处理。任何时候想要操作大规模数据,生成器都可以帮助确保有效地对数据进行处理。
yield还有一个不太常用的功能:它可以像函数调用一样返回值。这允许通过调用它的send()函数来向生成器传入一个值。
示例 8.1 通过yield返回值
def shorten(string_list):
length = len(string_list[0])
for s in string_list:
length = yield s[:length]
mystringlist = ['loremipsum', 'dolorsit', 'ametfoobar']
shortstringlist = shorten(mystringlist)
result = []
try:
s = next(shortstringlist)
result.append(s)
while True:
number_of_vowels = len(filter(lambda letter: letter in 'aeiou', s))
# Truncate the next string depending
# on the number of vowels in the previous one
s = shortstringlist.send(number_of_vowels)
result.append(s)
except StopIteration:
pass
在这个例子中,写了一个名为shorten的函数,它接收一个字符串列表并返回一个同样的字符串组成的列表,只不过是截断的。每个字符串的长度取决于前一个字符串中元音字母的个数。"loremipsum"包含四个元音,所以生成器返回第二个字符串"dolorsit"的前四个字母。"dolo"包含两个元音字母,所以"ametfoobar"将被截断成其前两个字母("am")。因此,这个然后生成器便停止并抛出StopIteration。生成器将返回:
['loremipsum', 'dolo', 'am']
通过这种方式使用yield和send()使得Python生成器的作用类似于Lua(http:// www.lua.org/)和其他语言中的协同程序(coroutine)。
提示
PEP 289(https://www.python.org/dev/peps/pep-0289/)引入了生成器表达式,通过使用类似列表解析的语法可以构建单行生成器。
>> (x.upper() for x in ['hello', 'world'])
<generator object <genexpr> at 0x7ffab3832fa0>
>> gen = (x.upper() for x in ['hello', 'world'])
>> list(gen)
['HELLO', 'WORLD']