【传统for循环】
Python的for循环中,in后面通常跟一个数组,这个数组是一个可迭代的对象,类似的还有链表,字符串,文件。可以是
list = [1, 2, 3]
也可以是
list = [x*x for x in range(3)]
缺点:内存中会加载很多数据,如果数据很大就会非常消耗内存。
【带yield的函数(生成器)】
带有yield的函数在Python中被称之为generator(生成器)。生成器可以迭代,但是只可以读取它一次,因为用的时候才生成。我们先抛开generator,来举一个常见的例子。
例:如何生成斐波那契数列?
斐波那契(Fibonacci)数列是一个非常简单的递归数列,除第一个和第二个数外,任何一个数都可由前两个数相加得到。初学者可以轻易写出如下函数:
#!/usr/bin/python
# -*- coding: UTF-8 -*-
def fab(max):
n, a, b = 0, 0, 1
while n < max:
print b
a, b = b, a + b
n = n + 1
fab(5)
执行以上代码,可以得到输出:
1
1
2
3
5
结果没有问题,但是直接在fab函数中用print打印数字会导致该函数可复用性较差,因为fab函数返回None,其他函数无法获取该函数生成的数列。
因此我们会想到返回一个List,而不是直接打印,如下:
#!/usr/bin/python
# -*- coding: UTF-8 -*-
def fab(max):
n, a, b = 0, 0, 1
L = []
while n < max:
L.append(b)
a, b = b, a + b
n = n + 1
return L
for n in fab(5):
print n
打印结果和第一段代码一样,但是又有一个问题,就是函数在运行的过程中占用的内存会随着参数max的增大而增大,如果要控制内存占用,最好不要用List来保存中间结果,而是通过iterable对象来迭代。如下(Python2.X):
for i in range(1000):pass
会导致生成一个1000个元素的List,而代码:
for i in xrange(1000):pass
则不会生成一个1000个元素的List,而是在每次迭代中返回下一个数值,内存空间占用很小。因为 xrange 不返回 List,而是返回一个 iterable 对象。
利用 iterable 我们可以把 fab 函数改写为一个支持 iterable 的 class,如下:
#!/usr/bin/python
# -*- coding: UTF-8 -*-
class Fab(object):
def __init__(self, max):
self.max = max
self.n, self.a, self.b = 0, 0, 1
def __iter__(self):
return self
def next(self):
if self.n < self.max:
r = self.b
self.a, self.b = self.b, self.a + self.b
self.n = self.n + 1
return r
raise StopIteration()
for n in Fab(5):
print n
其中iter()函数用来生成迭代器,语法:iter(object[, sentinel])
object是支持迭代的集合对象,sentinel–如果传递了第二个参数,则参数 object 必须是一个可调用的对象(如,函数),此时,iter 创建了一个迭代器对象,每次调用这个迭代器对象的_next_()方法时,都会调用 object。如:
>>>lst = [1, 2, 3]
>>> for i in iter(lst):
... print(i)
...
1
2
3
再返回到我们写的第三版输出斐波那契数列的代码,for…in…这个语句其实做了两件事,第一件事是获得一个可迭代器,即调用了__iter_()函数。第二件事是循环的过程,循环调用__next_()函数。Fab类通过next()不断返回数列的下一个数,内存占用始终为常数。然而,使用class改写的这个版本,代码远远没有第一版的fab函数来的简洁,想要的到既简洁又有iterable效果的代码,yield就派上用场了。
#!/usr/bin/python
# -*- coding: UTF-8 -*-
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b # 使用 yield
# print b
a, b = b, a + b
n = n + 1
for n in fab(5):
print n
第四版和第一版相比,仅仅把print b改为了yield b,效果和第三版一样。
简单的讲,yield的作用就是把一个函数变成一个 generator。Python 解释器会将带有yield的函数视为一个 generator,调用 fab(5) 不会执行 fab 函数,而是返回一个 iterable 对象!在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。
也可以手动调用 fab(5) 的 next() 方法(因为 fab(5) 是一个 generator 对象,该对象具有 next() 方法),这样我们就可以更清楚地看到 fab 的执行流程:
>>>f = fab(5)
>>> f.next()
1
>>> f.next()
1
>>> f.next()
2
>>> f.next()
3
>>> f.next()
5
>>> f.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
当函数执行结束时,generator 自动抛出 StopIteration 异常,表示迭代完成。在 for 循环里,无需处理 StopIteration 异常,循环会正常结束。
【示例2】
def node._get_child_candidates(self, distance, min_dist, max_dist):
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
if self._rightchild and distance - max_dist >= self._median:
yield self._rightchild
这个函数中没有for循环,但它依然可以用于迭代。node._get_child_candidates函数中有yield,所以它变成了一个迭代器,可以用于迭代。执行第一次迭代时(其实就是调用next()方法),如果有左节点并且距离满足要求,会执行第一个yield,这时会返回self._leftchild并完成第一个迭代。执行第二次迭代时,从第一个yield后面开始,如果有右节点并且距离满足要求,会执行第二个yield,这时会返回self._rightchild并完成第一个迭代。执行第三次迭代时,第二个yield后再无代码,捕获异常,退出迭代。
调用过程:
result, candidates = list(), [self]
while candidates:
node = candidates.pop()
distance <= node._get_dist(obj)
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
上面的node._get_child_candidates(self, distance, min_dist, max_dist)是放在extend()函数中作为参数的,为什么可以这么用,就因为extend函数的参数不仅仅支持array,只要它是一个迭代器就可以。它的原型是array.extend(iterable)。
【示例3:yield实现协程】
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
import time
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
time.sleep(1)
r = '200 OK'
def produce(c):
c.next()
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
if __name__=='__main__':
c = consumer()
produce(c)
执行结果:
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
注意到consumer函数是一个generator(生成器),把一个consumer传入produce后:
1.首先调用c.next()启动生成器;
2.然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
3.consumer通过yield拿到消息,处理,又通过yield把结果传回;
4.produce拿到consumer处理的结果,继续生产下一条消息;
5.produce决定不生产了,通过c.close()关闭consumer,整个过程结束。
send(msg)与next()的区别在于send可以传递参数给yield表达式,这时传递的参数会作为yield表达式的值,而yield的参数是返回给调用者的值。
换句话说,就是send可以强行修改上一个yield表达式值。比如函数中有一个yield赋值,a = yield 5,第一次迭代到这里会返回5,a还没有赋值。第二次迭代时,使用.send(10),那么,就是强行修改yield 5表达式的值为10,本来是5的,那么a=10。
send(msg)与next()都有返回值,它们的返回值是当前迭代遇到yield时,yield后面表达式的值,其实就是当前迭代中yield后面的参数。
第一次调用时必须先next()或send(None),否则会报错,send后之所以为None是因为这时候没有上一个yield(根据第8条)。可以认为,next()等同于send(None)。
【结论】
一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。
yield 的好处:把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。
【判断函数的迭代能力】
使用 isgeneratorfunction 判断
>>>from inspect import isgeneratorfunction
>>> isgeneratorfunction(fab)
True
要注意区分 fab 和 fab(5),fab 是一个 generator function,而 fab(5) 是调用 fab 返回的一个 generator,好比类的定义和类的实例的区别。
每次调用 fab 函数都会生成一个新的 generator 实例,各实例互不影响:
>>>f1 = fab(3)
>>> f2 = fab(5)
>>> print 'f1:', f1.next()
f1: 1
>>> print 'f2:', f2.next()
f2: 1
>>> print 'f1:', f1.next()
f1: 1
>>> print 'f2:', f2.next()
f2: 1
>>> print 'f1:', f1.next()
f1: 2
>>> print 'f2:', f2.next()
f2: 2
>>> print 'f2:', f2.next()
f2: 3
>>> print 'f2:', f2.next()
f2: 5
【return的作用】
在一个 generator function 中,如果没有 return,则默认执行至函数完毕,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代。
【扩展:读取大的文件】
如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过 yield,我们不再需要编写读文件的迭代类,就可以轻松实现文件读取:
def read_file(fpath):
BLOCK_SIZE = 1024
with open(fpath, 'rb') as f:
while True:
block = f.read(BLOCK_SIZE)
if block:
yield block
else:
return
参考链接:
https://www.runoob.com/w3cnote/python-yield-used-analysis.html
https://www.liaoxuefeng.com/wiki/897692888725344/923057403198272#0
https://blog.csdn.net/qq_33472765/article/details/80839417