Python中的Yield关键字
在理解yield
之前,我们必须明白生成器(generators)是什么,在这之前我们先讲一下可迭代对象(iterables)
可迭代对象(iterables)
你可以创建一个列表,逐个读取列表项的过程叫做迭代(iteration)
>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
mylist
是可迭代的,当你使用列表生成式(list comprehension)的创建他的时候,且他是一个可迭代对象:
>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4
你可以使用"for ... in ...
"去遍历一个可迭代对象;lists
,strings
,files…
你可以很方便的读取可迭代对象的数据,
但是你把所有数据都保存在了内存中,当数据量非常大的时候,这就成了一个问题。
生成器(Generators)
生成器(Generators)是一个迭代器(iterators),一种你只能迭代一次的可迭代对象,生成器没有把所有值都放在内存里,而是动态的生成值:
生成器代码和列表生成式一样,除了用()
替代了[]
,但是你不能迭代生成器两次,下面上一个对比:
# 生成器
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
...
0
1
4
# 在上面已经计算完了所有值,这里生成器已经是空的了
>>> for i in mygenerator:
... print(i)
...
# 列表生成式
>>> mygenerator = [x*x for x in range(3)]
>>> for i in mygenerator:
... print(i)
...
0
1
4
# 列表生成器可以再次迭代,还是生成一样的值
>>> for i in mygenerator:
... print(i)
...
0
1
4
生成器在每次调用的时候都计算一次值,第一次0,第二次1,第三次4,最后生成器为空。
Yield关键字
yield
的和return
关键字相似,不同的地方是yield关键字会返回一个生成器:
>>> def createGenerator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4
这个例子在真实世界并没有什么意义,但是当你知道你的函数要返回一个大量且只需要读取一次值的时候,用这种方法会更好。
要掌握yield
你还必须要明白,当你调用方法的时候,你编写在方法内的代码并没有执行,这个函数只返回生成器对象:
例如
>>> def generator():
... print('run')
... yield 1
...
>>> gen = generator()
>>> next(gen)
run
1
当你调用next
的时候函数才真正运行。
生成器是怎么迭代的?
>>> def generator(a):
... print('---1---')
... while a:
... print('---2---')
... yield 'yield 2'
... print('---3---')
... print('---4---')
... yield 'yield 2'
... print('---5---')
... yield 'yield 3'
...
这里我们用输出信息来了解生成器的迭代过程
a=1
时
>>> gen = generator(1)
>>> for i in range(3):
... print('i', i)
... print(next(gen))
...
i 0
---1---
---2---
yield 2
i 1
---3---
---2---
yield 2
i 2
---3---
---2---
yield 2
由上面的输出我们可以看到
第一次迭代 从generator(a)
方法开始执行,首先输出了---1---
然后进入了while
语句内输出了---2---
,遇到第一个yield关键字返回yield
后面的值。
第二次迭代 从上次yield
关键字后面开始执行输出---3---
、---2---
,遇到yield
关键字的时候返回值。
第三次迭代 同第二次迭代一样。
>>> gen = generator(0)
>>> for i in range(3):
... print('i', i)
... print(next(gen))
...
i 0
---1---
---4---
yield 2
i 1
---5---
yield 3
i 2
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
StopIteration
这个时候代码不会执行到while
循环里面
第一次迭代从generator(a)
方法开始执行,首先输出了---1---
然后输出了---4---
,遇到第一个yield关键字返回值。
第二次迭代从上次yield
关键字后面开始执行输出---5---
遇到yield
关键字的时候返回值
第三次迭代这个时候函数已经运行完了,生成器为空,抛出StopIteration
,这里如果使用for ... in gen
就不会抛出异常,因为当gen
为空时for ... in ...
就不会再做迭代了。
一个使用生成器控制资源访问的例子
>>> class Bank(): # 我们创建一个银行类
... crisis = False
... def create_atm(self):
... while not self.crisis:
... yield "$100"
>>> hsbc = Bank() # 银行类的实例
>>> corner_street_atm = hsbc.create_atm() # 创建一台atm,crisis 默认为False,可以取钱。
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # 危机发生了,crisis设置为True
>>> print(corner_street_atm.next()) # 原先的atm都取不出来钱了(生成器执行到了while语句,不符合条件,生成器里面没有值了)
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # 创建一台新的atm(这个时候返回的生成器仍然为空)
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # 危机过后
>>> print(corner_street_atm.next()) # 这个时候生成器已经是空的了,不会再执行代码,直接报错StopIteration
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # 建立一台新的atm恢复以前的业务
>>> for cash in brand_new_atm:
... print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...
这里比较有趣的是,即使中间执行了hsbc.crisis = False
,原先的生成器还是会抛异常,因为这个时候生成器已经为空了,不会再执行代码,所以 hsbc.crisis = False
并没有影响到原来的corner_street_atm
斐波拉契数列(Fibonacci)生成器
>>> def fib():
... n, a, b = 0, 0, 1
... while True:
... yield b
... a, b = b , a+b
>>> gen = fib()
>>> next(gen)
1
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
3
>>> next(gen)
5
yield的经典用法,面试会考。
关于itertools
itertools
模块包含操作iterables
的特殊函数。 例如,复制一个生成器,链接两个生成器,使用单行分组嵌套列表中的值,使用 Map / Zip
而不创建另一个列表,等等。
这里介绍一个用itertools.permutations
去排列赛马可能到达的顺序
>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
(1, 2, 4, 3),
(1, 3, 2, 4),
(1, 3, 4, 2),
(1, 4, 2, 3),
(1, 4, 3, 2),
(2, 1, 3, 4),
(2, 1, 4, 3),
(2, 3, 1, 4),
(2, 3, 4, 1),
(2, 4, 1, 3),
(2, 4, 3, 1),
(3, 1, 2, 4),
(3, 1, 4, 2),
(3, 2, 1, 4),
(3, 2, 4, 1),
(3, 4, 1, 2),
(3, 4, 2, 1),
(4, 1, 2, 3),
(4, 1, 3, 2),
(4, 2, 1, 3),
(4, 2, 3, 1),
(4, 3, 1, 2),
(4, 3, 2, 1)]
真实情况下我们需要排列组合的列表长度可能会很大,这个时候将排列组合的结果都放在内存中会占用很大的内存,实际上上周我实现Apriori
算法的时候就干了这事,后面电脑就死机了。
这篇文章部分来自Stack Overflow上的高票回答,感兴趣可以直接移步Stack Overflow,如果文章中有错误欢迎指正,非常感谢。
What does the “yield” keyword do? — Stack Overflow