先来学几个英文单词,本文统一使用英文单词表示以下概念:
- 「Iterable」:「可迭代对象」
- 「Iterator」:「迭代器」
- 「Generator」:「生成器」
本文的重点是「Generator」,之所以上来就说这3个概念,是因为:
❝
Generator只不过是Iterable和Iterator的一种简单形式。
用类的形式写Iterable/Iterator,要定义类,并包含至少三个函数:__init__,__iter__和__next__,但用Generator只要定义一个函数就搞定了。
❞
我在面试Python程序员的时候,连续几个号称资深的程序员,回答Generator的问题都回答的不好。如果你能理解透彻这篇文章,对你的加薪作用可能不止一千。
虽然有标题党的嫌疑,但具有实在的意义。所以既然进来了,就耐心读下去吧。一周能理解透一个重要概念,假以时日,你就是高手。
Generator是Iterable/Iterator的简单写法
如果你对Iterable和Iterator一无所知,建议先阅读本文的姊妹篇:
- 『Python终结者#2』可迭代对象(iterable)vs迭代器(iterator)vs生成器(Generator)
我们先用「Iterable/Iterator」的方式来定一个「随机数生成器」,我把它命名为「Randable」,它的功能是:
❝
随机生成若干个1到100之间的随机数。
❞
importrandomclassRandable():def__init__(self,total):self.count=0self.total=totaldef__iter__(self):returnselfdef__next__(self):ifself.count==self.total:raiseStopIterationrand_num=random.randint(1,100)self.count+=1returnrand_num
使用上面的Randable类生成88个随机数:
foriinRandable(88):print(i)
这是一个类,包含__init__,__iter__和__next__3个函数:
- Randable(100)调用__init__函数创建了一个可以生成100个随机数的对象。
- for语句循环这个对象的时候首先调用__iter__函数获取Iterator,也就是这个对象本身。
- 然后for不停调用__next__函数做循环,直到抛出StopIteration异常。
整个过程有点小复杂,也难以理解。Generator的出现就是为了简化这种复杂的写法。
实现同样的功能,Generator只需要一个「函数」就够了:
defrandgen(total):for_inrange(0,total):yieldrandom.randint(1,100)
调用过程不变:
foriinrandgen(88):print(i)
Generator的原理
for循环过程
结合上面的Geneator的例子,我们看一下for循环的过程:
- 调用generator函数randgen(88):,并不会马上执行函数中的代码,而是返回一个generator对象。
- for循环通过Python内置的next函数调用这个对象,直到对象抛出StopIteration异常为止。
试验一下:
defrandgen(total):for_inrange(0,total):yieldrandom.randint(1,100)g=randgen(88)print(type(g))
执行上面这段代码,会打印出:
<class'generator'>
Generator特征
- Generator函数中没有return语句,只有「yield」语句。所以生成器就是:「有yield关键词的函数」。
- Generator也可以有return语句,return语句就相当于抛出了StopIteration异常,会结束函数。
- 使用next()函数执行Generator中的代码,上面的for循环也是这个原理。
- 当代码执行到「yield」语句的时候,yield会返回一个值给调用者,然后函数暂定在原地,等待下次调用。
- 下次调用会从上次暂定的地方继续执行代码。这个过程会重复直到所有代码都执行完成,或者抛出了异常。
来看一个例子:
#Generatordefthree_step():print('这是第一步,你好!!')yieldprint('这是第二步,你还好吗?')yieldprint('这是第三步,再见!')s=three_step()next(s)next(s)next(s)
运行一下,打印的结果如下:
---第一次调用这是第一步,你好!!---第二次调用这是第二步,你还好吗?---第三次调用这是第三步,再见!Traceback(mostrecentcalllast):File"/Users/zjueman/git/python/weixin/generator/gen.py",line47,in<module>next(s)StopIteration
说明一下:
- yield关键词会让函数暂停,也可以没有返回值
- 「可以把generator理解成有状态的函数」。一般的函数没有自己的状态,执行一次就结束了。但是generator有自己的状态可以被多次调用。
- 实际上Generator背后就是一个类,所以它有状态。上一节中我们说过,Generator就是Iterable/Iterator类的一种简单写法。
Generator表达式
Genertor除了函数的写法之外,还可以用表达式的写法。它的写法和列表推导式类似,区别就是把中括号**[...]「改成小括号」(...)**。
这是一个列表推导式:
importsys#生成1到1万的数字的平方nums_squared_list=[i*2foriinrange(10000)]
这是Generator表达式:
importsys#生成1到1万的数字的平方nums_squared_gen=(i*2foriinrange(10000))
前者会在内存中生成10000个数字,放在列表中。
后者不会马上生成,当你每次用next(nums_squared_gen)函数去调用它的时候,它会生成一个并返回。
如果你对推导式不熟悉,请看终结者系列的另一篇文章。链接见文末。
Generator的性能优势
Generator因为可以被循环,经常被拿来和list做对比。它最让人津津乐道的是它的性能优越性。
假如你开了一家汉堡店,有个大客户向你订购1000万个汉堡。你会一次性生产完这些汉堡吗?
傻的汉堡店主会这样:
❝
一次性生产完1000万个汉堡,可是店里根本放不下啊。再租个仓库放。可是后来发现汉堡都坏掉了。
❞
聪明的汉堡店主会这样:
❝
分批生产!客户什么时候来要,就给他们马上生产一批,既不会把店占满,汉堡还新鲜!
❞
那如果有个需求,让你生成100亿个随机数,再求和。你会这样写吗?
foriinRandable(88):print(i)0
如果这样写,你的程序会在内存中生成100亿个整数,这也许会占满你的内存。
正确的写法是使用Generator,就用我们上面的randgen吧:
foriinRandable(88):print(i)1
前面使用list的时候,要先在内存中生成100亿个数字,然后再求和,这占空间又费时间。
而用Generator是每次用到的时候才生成1个,不用那么多空间。
我们可以测试一下前面的推导式的例子中占用的内存情况:
foriinRandable(88):print(i)2
这个例子中只生成10000个数字,区别还没那么大。如果是生成100亿个数字,区别会更大,因为Generator占用的内存基本是恒定的,和数字多少无关。
如果你曾经在写代码的时候犯了「傻汉堡店主」的问题,那么不要羞愧,因为Python语言的设计者们都犯过这样的错误!
在Python2中很多标准库使用列表形式,出现内存问题。所以在Python3中很多标准库都改用了Generator。
比如:
- range()函数在Python2中返回的是一个列表,在Python3中返回的是一个Genator。
- 字符串的迭代器也是一个Generator
foriinRandable(88):print(i)3
打印结果:
foriinRandable(88):print(i)4
帮你熟悉Generator的几个代码例子
我们再来多看几个代码例子,有的很简单,目的是为了给你增加更多的代码感觉。
- range是一个Generator,所以多大的range内存都不会爆foriinrange(5):
print(i) - 三次方生成器defmygenerator(n):
foriinrange(1,n,2):
yieldi**3 - 表达式形式的三次方生成器mygenerator=(i**3foriinrange(1,10,2))
- Generator不只是用在for循环中,我们可以手动用next()函数调用它defmygenerator(n):
foriinrange(1,n,2):
yieldi*(i+1)
my_gen=mygenerator(6)
next(my_gen)
2
next(my_gen)
12
next(my_gen)
30
next(my_gen)
>StopIterationerror
高级的Generator方法
普通的Generator执行到yield就暂停,可以返回一个值或者不返回。
Generator除了可以返回值,它还可以接收调用者传值进来,这就要使用send()方法。
除了send()方法,还有throws,close()方法。
能通过yield返回值,也能够通过send()接收值,这就不是普通的Generator,而是进入了协程coroutine的领域了,需要专门的文章来讲,「我们下次再终结」。
基本的Generator有上面这些知识就足够了。