11-python生成器

生成器可以认为是一个特殊的迭代器,它可以生成一个值的序列用于迭代,但这个序列不是一次性生成的,而是用一个生成一个,这样就可以节省很大的内存。

创建生成器

用推导式创建

之前讲推导式时讲到有列表推导式,如:

>>> [x**2 for x in range(4)]
[0, 1, 4, 9]

现在只要把 [] 改为 () 就能创建一个生成器

>>> a = (x**2 for x in range(4))
>>> a            #直接打印a不能得到所要的结果,只是返回一个生成器对象
<generator object <genexpr> at 0x0000020E3855D318>

>>> list(a)      #可以转换为list
[0, 1, 4, 9]

>>> for i in a:  #可以使用循环
    print(i)   
0
1
4
9

>>> next(a)    #可以使用next(a)或者 a.__next__()方法打印
0   
>>> next(a)
1
>>> next(a)
4
>>> next(a)
9
>>> next(a)   #当超出范围时会抛出StopIteration异常
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

使用函数创建

生成器对象是使用了 yield 关键字定义的函数对象,因此,在函数中使用了 yield关键字,这个函数就是一个生成器了。

生成器用于生成一个值的序列,以便在迭代中使用。

例如:

def myYield(x):
    while x <= 3:
        yield x**2
        x += 1
        
b = myYield(0)   #实例化生成器

type(b)
#输出:
<class 'generator'>
list(b)
#输出:
[0,1,2,3]
for i in b:
    print(i)

#输出
0
1
4
9

还可以使用 next(b)b.__next__() 来打印结果。

需要注意的是,当所有值都遍历完后,继续执行 next(b) 或 b.__next__() 语句,会引发StopIteration异常

深入生成器

如上所述,生成器可以用 list() 打印值,可以用 for 遍历,也可以用 next()__next__() 方法一个个打印。

实际上,生成器就相当于一个数学表达式,它保存的是计算的方式,你调用一次,它就给你输出一个值;而像列表这些序列,保存的是数据本身,你要获得一个值,就直接从序列里面拿出来就行了。

但是,正是因为这样,如果要存储很多(比如1万个、10万个…)的数据,如果用列表这种序列的方式保存,就会消耗很大的内存,而使用生成器的方式,相当于只是保存了一个表达式,用一个就根据表达式计算一个,这样就能节约内存了。


yield 语句是通过函数式生成器的关键语句,生成器只有在调用 next()__next__() 方法时才会开始运行,且每次执行了 yield 语句就会把值返回,同时“吼(hold)住”,即保持当前状态,停止执行函数后面的语句,等待下一次遍历时才会继续执行这一部分语句,直到再次执行了 yield 语句才停止。如此循环下去。另外,yield 后面也可以不带任何值,这时yield语句就起一个 吼住 的作用。

例如:

def myYield(x):
    while x <= 3:
        print('开始执行')
        yield x**2
        print('执行完成')
        x += 1

c = myYield(0)

调用和输出:

>>> next(c)  #执行完yield语句停止
开始执行
0
>>> next(c)  #从yield语句后面的语句开始执行
执行完成
开始执行
1
>>> next(c)
执行完成
开始执行
4
>>> next(c)
执行完成
开始执行
9

yield 语句不仅可以返回值,还可以让程序员向生成器传入值,使用 send(n) 就可以了,不过要注意的是,第一次调用生成器时不能传入值(或者说不能传入None以外的值),否则会报错。

例如:

def myYield(n):
    while n <= 3:
        rcv = yield '平方值: ',n**2
        print('rcv的值: ',rcv)
        n += 1

d = myYield(0) #实例化生成器

调用如下:

>>> next(d)
('平方值: ', 0)
>>> next(d) 
rcv的值:  None
('平方值: ', 1)

之前已经讲过,每次执行了 yield 语句就会 “吼住”,不往下执行了,所以在下次执行时先从赋值语句 rcv= 执行起,但yield语句已经执行了,所以只好赋值 为 None 了。

接下来看如何改变 rcv 的值:(下面的程序是接着上一个的)

>>> d.send(100)
rcv的值:  100
('平方值: ', 4)

可以看到,我们成功把一个值传入了生成器里面,把他赋值给了 rcv,为什么只是赋值给了rcv,而不是 n,这其中我们只需要知道是 yield 的功劳就行了。

那么传入数据进入生成器后有什么用,这时你可以在生成器内部任意处理这个数据,例如,可以把它赋值给 n:

def myYield(n):
    while n <= 3:
        rcv = yield '三次方值: ',n**3
        n += 1
        if rcv is not None:
            n = rcv

d = myYield(0) #实例化生成器

调用和输出:

>>> next(d) 
('三次方值: ', 0)
>>> next(d)
('三次方值: ', 1)
>>> next(d)
('三次方值: ', 8)
>>> d.send(-3)       #传入数据
('三次方值: ', -27)
>>> next(d)
('三次方值: ', -8)

生成器与协程

上面提到的用 send() 方法给生成器传入值,也被称作 协程,协程是一种解决程序并发的方法。

如果使用一般方式处理生产者与消费者之间这个传统的并发与同步程序设计问题,会很麻烦,但通过生成器实现的协程,可以很简单的解决这个问题(看不懂直接看例子):

例如:

def consumer():                   #定义一个消费者模型(生成器协程)
    print('消费者等待接收任务...')
    while True:
        task = yield
        print('消费者接收到任务{}'.format(task))       #模拟接收任务

def producer():                   #定义生产者模型
    c = consumer()
    c.__next__()
    for task in range(2):
        print('生产者发送任务{}...'.format(task))
        c.send(task)              #模拟向消费者发送任务

producer()  #执行生产者模型

输出如下:

消费者等待接收任务...
生产者发送任务0...
消费者接收到任务0
生产者发送任务1...
消费者接收到任务1
posted @ 2020-07-26 14:37  aJream  阅读(19)  评论(0编辑  收藏  举报