python - 生成器

前言

在开始讲解协程和生成器之前,说明一下协程和生成器之间的关系是很有必要的,生成器在学习python的时候,并没有深入的学习,所以感觉很抽象很难懂,和普通函数单向调用的逻辑思维并不一样。协程则是和线程,进程是同一类别的概念。


首先要认识yield关键字,是yield关键词,yield放在函数中可以使得函数变成生成器,也可以变成协程。
在生成器中, yield 只对外产出值,在协程中,yield能对外产出值,而且能接收通过send()方法传入值
yielld构造的生成器可以作为协程使用,协程是指一个过程,这个过程与调用方协作,由调用方提供的值,来计算并产出。


纯粹的生,这样可以交接给for调用。成器只输出值,和迭代有关
协程与函数的区别,函数是一种上下级调用关系,而协程是通过_yield_方式转移执行权,对称而平级的调用对方,典型的有生产者和消费者。

从调试过程中理解

区别于其他教程,前几段都是云里雾里的讲概念,但是对于生成器,一上来就看概念,真的很难懂。所以我准备了三个实例,建议上来先通过打断点,分步调试过程中,通过看调用顺序和返回值来初步了解生成器的原理,接下来在看概念解释,则比较容易理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# encoding:UTF-8
def yield_test(n):
for i in range(n):
yield call(i)
print("i=", i)
# 做一些其它的事情
print("do something.")
print("end.")


def call(i):
return i * 2


# 使用for循环
for i in yield_test(5):
print(i, ",")


下图是打断点调试的过程,其中程序注释中会标注,运行步骤顺序,用step x来表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'

def produce(c):
c.send(None)
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()

c = consumer()
produce(c)


下图是打断点调试的过程,其中程序注释中会标注,运行步骤顺序,用step x来表示

1
2
3
4
5
6
7
8
9
10
def h():
print 'Wen Chuan',
m = yield 5
print m
d = yield 12
print 'We are together!'

c = h()
m = c.__next__()
c.send('Fighting!')


下图是打断点调试的过程,其中程序注释中会标注,运行步骤顺序,用step x来表示



经过上面的三个程序的调试步骤后,下面开始带着心中初步了解的程序运行顺序来看生成器相关的原理

生成器

what
生成器是一次生成一个值的特殊类型函数。可以将其视为可恢复函数。调用该函数将返回一个可用于生成连续 x 值的生成【Generator】,简单的说就是在函数的执行过程中,yield语句会把你需要的值返回给调用生成器的地方,然后退出函数,下一次调用生成器函数的时候又从上次中断的地方开始执行,而生成器内的所有变量参数都会被保存下来供下一次使用。
why
列表所有数据都在内存中,如果有海量数据的话将会非常耗内存。
如:仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
如果列表元素按照某种算法推算出来,那我们就可以在循环的过程中不断推算出后续的元素,这样就不必创建完整的list,从而节省大量的空间。
简单一句话:我又想要得到庞大的数据,又想让它占用空间少,那就用生成器!
How
方法一

1
2
3
4
5
6
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>


方法二
如果一个函数中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator。调用函数就是创建了一个生成器(generator)对象。

yield关键字

yield 的用法起源于对一般 function 中 return 的扩展。在一个 function 中,必须有一个返回值列于 return 之后,可以返回数字也可以返回空值,但是必须要有一个返回值,标志着这个 function 的结束。一旦它结束,那么这个 function 中产生的一切变量将被统统抛弃,有什么可以使一个 function 暂停下来,并且返回当前所在地方的值,当接收到继续的命令时可以继续前进呢?换个说法,就是 return 返回一个值,并且记住这个返回的位置。这个操作有点儿像
Python2.5以前,yield是一个语句,但现在2.5中,yield是一个表达式(Expression),比如:

1
m = yield 5


大家不要认为,m值为5,而是表达式(yield 5)的返回值将赋值给m,那么yield 5的返回值是什么呢?yield 5的返回值是下面将要提到的send(msg)方法传递过来的msg参数。

生成器的工作原理

  1. 生成器(generator)能够迭代的关键是它有一个 next() 方法 。 工作原理就是通过重复调用next()方法,直到捕获一个异常。
  2. 带有 yield 的函数不再是一个普通函数,而是一个生成器generator。可用next()调用生成器对象来取值。next 两种方式 t.next() | next(t)。 可用for 循环获取返回值(每执行一次,取生成器里面一个值) (基本上不会用next()来获取下一个返回值,而是直接使用for循环来迭代)。
  3. yield相当于 return 返回一个值,_迭代一次遇到yield时就返回yield后面(右边)的值_。并且记住这个返回的位置,下次迭代时,代码从yield的_下一条_语句开始执行。
  4. send() 和next()一样,都能让生成器继续往下走一步(下次遇到yield停),send()能传一个值,这个值作为yield表达式等号左边的值。——换句话说,就是send可以强行修改上一个yield表达式值。比如函数中有一个yield赋值,a = yield
  5. 第一次迭代到这里会返回5,a还没有赋值。第二次迭代时,使用.send(10),那么,就是强行修改yield 5表达式的值为10,本来是5的,那么a=10
  6. send(msg)与next()都有返回值,它们的返回值是当前迭代遇到yield时,yield后面表达式的值,其实就是当前迭代中yield后面的参数。
  7. 感受下yield返回值的过程(关注点:每次停在哪,下次又开始在哪)及send()传参的通讯过程
  8. 生成器还可以使用 next 方法迭代。生成器会在 yield 语句处暂停,这是至关重要的,未来协程中的 IO 阻塞就出现在这里。