Python3 | 生成器 generator

如何使用生成器中的 yield 关键字?

Posted by Haauleon on November 14, 2017

生成列表的方式

方式一 普通的方式
1
2
3
4
5
gen = []
for i in range(1, 6):
    if i >= 3:
        gen.append(i)
print(gen) # [3, 4, 5]



方式二 列表推导式
1
2
gen = [i for i in range(1, 6) if i >=3]
print(gen) # [3, 4, 5]



理解生成器中

一、yield 关键字的作用

  生成器中的 yield 关键字能把一个函数变成一个生成器 generator ,与 return 不同,yield 在函数中返回值的时会保存函数的状态,使下一次调用函数时会从 yield 的下一条语句处开始执行。若要获得下一个值,仅需要调用 next() 方法即可。生成器 generator 总是生成值,一般是迭代的序列。

1
2
3
4
5
6
7
def gen():
    # 函数中只要出现 yield 关键字,则这个函数就变成一个生成器 generator
    yield i

print(type(gen())) # <class 'generator'>
g = gen()
print(type(g)) # <class 'generator'>



二、使用 yield 关键字的好处

  想要生成一个列表,假如这个列表的存储空间很大,而又只想访问列表前几个元素,那个 yield 这个关键字就能实现一边循环一边计算的机制,节省存储空间又提高运行效率。



生产者和消费者的问题

一、需求描述

  现在我们要让生产者发送 1, 2, 3, 4, 5 给消费者,消费者接受数字同时返回状态给生产者,而消费者只需要 3, 4, 5 就行了,所以当数字等于 3 时将返回一个错误的状态。最终我们需要由主程序来监控生产者-消费者的过程状态,调度结束程序。



二、不使用生成器
1
2
3
4
5
6
7
8
9
10
def consumer(producer):
    return [status for status in producer if status >=3]

def producer():
    return [n for n in range(1, 6)]

if __name__ == '__main__':
    p = producer()
    c = consumer(p)
    print(c) # [3, 4, 5]

分析:这样的做法很简单,但是弊端也很明显。我每一次去调用 producer() 和 consumer() 这两个函数时,都要耗费大量时间去循环做重复的事情。并且无法通过主程序来监控生产者-消费者的过程状态。

三、使用生成器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def consumer():
    status = True
    while True:
        # 将 status 返回给调度它的主程序
        n = yield status
        print("拿到了{}!".format(n))
        if n == 3:
            status = False

def producer(consumer):
    n = 5
    while n > 0:
        yield consumer.send(n)
        n -= 1

if __name__ == '__main__':
    c = consumer()
    # print(type(c)) # <class 'generator'>
    # print("c = {}".format(c)) # c = <generator object consumer at 0x7fa6a7694c50>
    c.send(None) 
    p = producer(c) # 传入了消费者的生成器,让 producer 跟 consumer 进行通信
    for status in p:
        if status == False:
            print("我只要3,4,5就行啦")
            break
    print("程序结束")


运行结果:

1
2
3
4
5
我拿到了5!
我拿到了4!
我拿到了3!
我只要3,4,5就行啦
程序结束


分析:
从主程序 if __name__ == '__main__': 开始看:

1
2
3
4
5
6
7
8
9
if __name__ == '__main__':
    c = consumer()
    c.send(None) 
    p = producer(c) 
    for status in p:
        if status == False:
            print("我只要3,4,5就行啦")
            break
    print("程序结束")


  第一条语句 c = consumer(),因为 consumer 函数中存在 yield 关键字,python 会把它当成一个生成器 generator。因此在运行这条语句后,python 并不会像执行函数一样,而是返回了一个 generator object。即语句 print("c = {}".format(c)) 的执行结果是 ` c = <generator object consumer at 0x7fa6a7694c50>`。

  第二条语句 c.send(None),这条语句的作用是将生成器 consumer 中的语句推进到第一个 yield 语句出现的位置即 yield status。在 consumer 中,status = Truewhile True: 都已经被执行了,此时程序停留在n = yield status的位置(注意:此时这条语句还没有被执行)。

  第三条语句 c.send(None),漏写这一句,程序直接报错 TypeError: can't send non-None value to a just-started generator

  第四条语句 p = producer(c),producer 也是一个生成器,所以 p 是一个生成器对象。这里传入了消费者生成器consumer ,以此来让生产者 producer 和消费者 consumer 进行通信。

  第五条语句 for status in p:,这条语句会循环地运行 producer 和获取它 yield 回来的消费者状态 status。


1
2
3
4
5
6
7
8
9
10
11
12
13
def consumer():
    status = True
    while True:
        n = yield status
        print("拿到了{}!".format(n))
        if n == 3:
            status = False

def producer(consumer):
    n = 5
    while n > 0:
        yield consumer.send(n)
        n -= 1


分析 :producer 中的语句 yield consumer.send(n)
  生产者 producer 调用 consumer.send(n),把 n 的值发送给 consumer。在 consumer 的语句 n = yield status 中, n 拿到的是 producer 发送的数字,同时返回 consumser 中用 yield 的变量 status 给 producer,然后 producer 马上将 status 返回给调度它的主程序,主程序 for status in p:获取 status 的值,判断若为 false 则终止循环,结束程序。



四、generator.send(args)

  generator.send(n) 的作用是:将 args 发送到生成器 generator 中的 yield 关键字处,同时返回 generator 中 yield 的变量(结果)。