可迭代对象、迭代器、生成器以及yield关键字使用

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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值