1.可迭代对象(iterable object)
能使用for遍历的对象,都是可迭代对象,例如:list、set、dict、str、tuple等容器都属于可迭代对象。可迭代对象必然实现了__iter__()函数,换言之,一个对象实现了__iter__()函数,就是可迭代对象。
2.迭代器(iterator)
一个对象同时实现了__iter__()和__next__()函数,称之为迭代器。可迭代对象不一定是迭代器,迭代器一定是可迭代对象。
iter()函数的作用是返回一个迭代器对象,next()函数作用是返回迭代器的下一个值。
3.生成器(generator)
能够一边迭代一边计算并产生值的对象,称为生成器。生成器仅仅保存了一套生成数值的算法,不是让算法一次性生成所有值,而是在调用它时才计算产生一个新的值并返回。将一个对象变为生成器的方法是在函数内部使用yield关键字。生成器是一种特殊的迭代器,迭代器只能够返回值,而生成器不光可以返回值,还能接收值(send函数)。
为什么要使用生成器?假设现在需要读取0,2,4,6,8··· ···等一亿个数字存到list中,但每次只会使用1个值,如果一次性读取全部数字到内存中将非常消耗内存。因为这些数字是有规则的,它们都是偶数,所以可以按照规则每次生成一个值使用,用完便释放内存,这样内存中只会消耗一个数字的存储空间。简单说就是:如果我想使用的海量数据存在规则,如果想节省内存,便可以使用生成器。
3.1 创建生成器
1.使用列表生成式创建生成器:
下面是使用列表生成式创建一个列表的方法,得到的类型是list:
test_list = [i*2 for i in range(10)]
print(type(test_list))
for i in test_list:
print(i)
只需要把列表生成式的中括号改为小括号,便可以得到一个生成器对象:
test_list = (i*2 for i in range(10))
print(type(test_list))
for i in test_list:
print(i)
2.一个函数中使用了yield关键字,调用函数时就生成了一个生成器。
3.2 yield关键字
下面来看一下yield的使用方式和作用。
首先先看下面这段函数:
def test_generator(): # 0.定义函数
print("generator start")
for i in range(2):
yield i # 1.yield关键字将函数暂停,并抛出i
print(f"i is {i}")
gen = test_generator() # 3.调用函数,获得一个生成器
print(next(gen)) # 4.调用next(),获取下一个值
# print(gen.send('msg'))
print(next(gen))
print(next(gen))
解释一下这段代码的含义:
1.第0处,定义了一个函数test_generator(),因为函数内部使用了yield关键字,所以这不再是一个普通函数,当调用它时函数内的代码并不会执行,而是返回一个迭代器对象。
2.关于yield应该怎样理解?很多人建议当做return理解,我觉得yield=pause+raise(抛出)更容易理解,并且此处的raise并不会终止函数,仅仅抛出一个值而已,且抛出后值便不存在了。当程序执行到yield时,会先将程序暂停,然后将yield后的值抛给调用方。
3.next()每次调用将返回迭代器的下一个值,相当于每次执行时都会让原先暂停的程序继续执行,直到遇见下一次yield。
执行结果如下:
下面看一下程序的执行顺序:
1.第1次调用next()函数时,程序开始执行,打印了generator start。然后test_generator()函数到达第一个含有yield的语句:yield i
,此时程序暂停,将yield后的i抛出,即将i=0返回,并将0打印出来;
2.第2次调用next()函数,程序从yield i
后面执行,打印出i is 0
,然后i=1,程序又执行到yield i
处,再次将i抛出(此时i=1),然后打印1;
3.第3次调用next(),程序从上次yield处继续执行,打印出第二个i is 1
;此时由于遍历完,生成器结束,抛出停止迭代异常(StopIteration)。关于此异常,可以了解一下for循环的执行原理。
再看一下下面这段代码,不同之处是1处的res = yield
。
def test_generator(): # 0.定义函数
print("generator start")
for i in range(2):
res = yield i # 1.yield关键字将函数暂停,并抛出i
print(f"res:{res}")
gen = test_generator() # 3.调用函数,获得一个生成器
print(next(gen)) # 4.调用next(),获取下一个值
print(next(gen))
print(next(gen))
执行结果如下:
下面看一下程序的执行顺序:
1.第1次调用next()函数时,程序开始执行,打印了generator start。然后test_generator()函数到达第一个含有yield的语句:res = yield i
,此时程序暂停,将yield后的i抛出,即将i=0返回,并将0打印出来。需要注意的是,程序暂停的位置是yield,此时i=0并没有赋给res;
2.第2次调用next()函数,程序从res = yield i
继续执行,由于上一步并没有将i=0赋值给res就抛出了,所以此时首先应该执行的是赋值语句,但是由于i已经被抛出,此时已经没有i=0了,res会被赋值为None,便打印出了第一个res:None
,然后i=1,程序又执行到res = yield i
处,再次将i抛出(此时i=1),然后打印1;
3.第3次调用next(),程序从上次yield处继续执行,同样由于i=1已经被抛出,res被赋值为None,打印出第二个res:None
;此时由于遍历完,生成器结束,抛出停止迭代异常(StopIteration)。关于此异常,可以了解一下for循环的执行原理。
总结一下就是:当执行到yield关键字时,程序会先暂停,并将yield后面的值抛出去(抛出去本地也就没有了),调用方会接到抛出的值,调用next会让程序继续执行直到再次遇到yield暂停。
那么问题来了,如果我想让下次执行时res可以被赋值怎么办?send()函数派上用场。
3.3 send()函数的使用
send()函数和next()函数是类似的,只不过在next()的基础上增加了传参功能。执行send()函数相当于执行了next() + 传参两个行为。
def test_generator(): # 0.定义函数
print("generator start")
for i in range(2):
res = yield i # 1.yield关键字将函数暂停,并返回i
print(f"res:{res}")
gen = test_generator() # 3.调用函数,获得一个生成器
print(next(gen)) # 4.调用next(),获取下一个值
print(gen.send('msg'))
print(next(gen))
结果:
注意,我将第二个print(next(gen))
变成了print(gen.send('msg'))
,执行到这里的时候,就不单纯是执行next(gen)
了,而是先将yield i
替换为'msg'
,即将'msg'
赋值给res,然后继续执行。
总结一下,next()和send()的不同是send()会传入一个参数替代yield xxx
。
3.4 使用yield实现协程:生产者-消费者模型
def consumer():
print("first...")
res = 'default'
while True:
product = yield res
# if not product:
# return
print(f"consuming {product}")
res = f"{product} is finished..."
def produce(c):
ret = c.send(None) # 开始迭代第一次,等同于执行next(c)
print(ret)
product = 0
while product < 3:
product += 1
print(f"producing {product}")
r = c.send(product)
print(f"Consumer return:{r}")
c.close()
c = consumer()
produce(c)
每当生产者生产一个产品,就切换到消费者去消费,消费完成后再切换到生产者继续生产…这便是用户级线程——协程的一个经典实现。以上代码的执行结果如下:
1.消费者是一个生成器,生产者首先通过c.send(None)
使迭代器开始执行,此处也可以使用next(c)
。消费者首先打印first...
,当消费者执行到product = yield res
时,程序暂停,并将res = 'default'
抛出,由ret接收到抛出的'default'
值,打印出default;
2.生产者生产出1号产品后,打印producing 1
,然后调用send(product)
函数使消费者生成器继续执行,由于send(product)
携带了product 1
,所以相当于把yield res
替换为了product 1
,先对product进行赋值,即:product = product 1
,然后继续执行,打印出consuming 1
,并给res赋值为1 is finished...
,程序继续执行循环,到yield再次暂停,将res = '1 is finished...'
抛出;
3.生产者接收到res,将其打印出来。然后生产出2号产品,打印出producing 2
,然后调用send(product)
,生成器接收到后将其赋值给product,并打印consuming 2
,并给res赋值为2 is finished...
,程序到yield再次暂停,将res = '2 is finished...'
抛出;
4.生产者接收到res,将其打印出来。然后生产出3号产品,打印出producing 3
,然后调用send(product)
,生成器接收到后将其赋值给product,并打印consuming 3
,并给res赋值为3 is finished...
,程序到yield再次暂停,将res = '3 is finished...'
抛出;
3.生产者接收到res,将其打印出来。此时生产者跳出循环,调用close()
函数关闭消费者生成器,结束程序。
3.5 使用生成器多次读取固定大小的文件内容
读取文件时,如果一次性读取大文件会占用大量内存,如果使用生成器每次读取固定大小的文件块便可以避免这种问题。
def read_file(file_path):
BLOCK_SIZE = 2**3
with open(file_path, 'r') as fs: # 带'b'模式是按字节读取,默认是按字符
while True:
block = fs.read(BLOCK_SIZE)
if block:
yield block
else:
return
rf = read_file('./test.txt') # 创建生成器对象
# print(next(rf))
for i in rf:
print(i)