目录
一、可迭代对象、迭代器和生成器
可迭代对象
- 能用for循环进行迭代的对象就是可迭代对象。 # 比如:字符串,列表,元祖,字典,集合等等,都是可迭代对象。
- 可以用过dir()方法看它里面有__iter__方法,有就是可迭代对象。
迭代器
- 那么什么叫迭代器呢?它是一个带状态的对象,他能在你调用next()方法的时候返回容器中的下一个值,任何实现了__iter__和__next__()方法的对象都是迭代器,__iter__返回迭代器自身,__next__返回容器中的下一个值,如果容器中没有更多元素了,则抛出StopIteration异常。
- 迭代器对象相对可迭代对象多了个__next__方法。所以迭代器是在可迭代的基础上实现的。要创建一个迭代器,我们首先得有一个可迭代对象。
生成器
- 生成器是在迭代器的基础上,再实现了yield。
生成器的创建方式
列表生成式
from collections.abc import Generator
L = (x^2 for x in range(10))
print(isinstance(L, Generator)) #True
实现yield函数
def foo(n):
num = 0
while num < 0:
yield num
num += 1
if __name__ == '__main__':
g = foo(5)
print(isinstance(g, Generator)) #True
生成器的激活/运行方式
- 使用next()
- 使用gengerator.send(None)
生成器的执行状态
- GEN_CREATED 等待开始执行
- GEN_RUNNING 解释器正在执行(只有在多线程应用中才能看到这个状态)
- GEN_SUSPENDED 在yield表达式处暂停
- GEN_CLOSED 执行结束
from inspect import getgeneratorstate
def mygen(n):
nums = 0
while nums < n:
yield nums
nums += 1
raise StopIteration # 注意自己编写生成器时要在结束时抛stop异常
if __name__ == '__main__':
gen = mygen(2)
print(getgeneratorstate(gen)) #GEN_CREATED
print(next(gen))
print(getgeneratorstate(gen)) #GEN_SUSPENDED
print(next(gen))
gen.close() # 手动关闭/结束生成器
print(getgeneratorstate(gen)) #GEN_CLOSED
向生成器发送消息
def mygen(n):
nums = 0
while nums < n:
# rec = yield可以接收外部程序通过send()发送的信息,并赋值给rec
rec = yield nums # yield nums是将nums return给外部调用程序。
if rec is None:
rec = 1
nums += rec
raise StopIteration
if __name__=="__main__":
gen = mygen(10)
print(next(gen)) # 【坑】注意第一次一定要next或者send(None)来启动
print(gen.send(2))
print(next(gen))
print(gen.send(4))
# 输出
0
2
3
7
二、yield使用
首先,如果你还没有对yield有个初步分认识,那么你先把yield看做“return”,这个是直观的,它首先是个return,普通的return是什么意思,就是在程序中返回某个值,返回之后程序就不再往下运行了。看做return之后再把它看做一个是生成器(generator)的一部分(带yield的函数才是真正的迭代器),好了,如果你对这些不明白的话,那先把yield看做return,然后直接看下面的程序,你就会明白yield的全部意思了:
def foo():
print("starting...")
while True:
res = yield 4
print("res:",res)
g = foo()
print(next(g))
print("*"*20)
print(next(g))
输出结果:
starting...
4
********************
res: None
4
我直接解释代码运行顺序,相当于代码单步调试:
1.程序开始执行以后,因为foo函数中有yield关键字,所以foo函数并不会真的执行,而是先得到一个生成器g(相当于一个对象)
2.直到调用next方法,foo函数正式开始执行,先执行foo函数中的print方法,然后进入while循环
3.程序遇到yield关键字,然后把yield想想成return,return了一个4之后,程序停止,并没有执行赋值给res操作,此时next(g)语句执行完成,所以输出的前两行(第一个是while上面的print的结果,第二个是return出的结果)是执行print(next(g))的结果,
4.程序执行print("*"*20),输出20个*
5.又开始执行下面的print(next(g)),这个时候和上面那个差不多,不过不同的是,这个时候是从刚才那个next程序停止的地方开始执行的,也就是要执行res的赋值操作,这时候要注意,这个时候赋值操作的右边是没有值的(因为刚才那个是return出去了,并没有给赋值操作的左边传参数),所以这个时候res赋值是None,所以接着下面的输出就是res:None,
6.程序会继续在while里执行,又一次碰到yield,这个时候同样return 出4,然后程序停止,print函数输出的4就是这次return出的4.
到这里你可能就明白yield和return的关系和区别了,带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从foo函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。
三、send和next函数
将上个例子中的最后一行替换成send
def foo():
print('starting...')
while True:
res = yield 4
print('res:', res)
g = foo()
print(next(g))
print('*'*20)
print(g.send(7))
输出结果:
starting...
4
********************
res: 7
4
注:
- 无参数的send和next作用一样
- 第一次调用的send不能带参数
四、yield和return对比分析
相同点:都是返回函数执行的结果。
不同点:return在返回结果后结束函数的运行,而yield则是让函数变成一个生成器,生成器每次产生一个值,函数被冻结,被唤醒后再产生一个值。
五、yield总结
好处总结:
- 不会将所有数据取出来存入内存中;而是返回了一个对象;可以通过对象获取数据;用多少取多少,可以节省内容空间。
- 除了能返回一个值,还不会终止循环的运行;
- 每次执行到yield,因为底层的实现就是中断的原理,保存栈帧,加载栈帧。
- 每次执行结束内存释放,执行的时候占用一点内存,消耗的内存资源就很少
其他补充:
- 通常yield都是放在一个函数中,该函数就变成了生成器函数,该函数就变成了一个迭代器
- 生成器函数一般都是通过for循环调用,for循环自带next方法
- 分布式爬虫会经常使用yield,yield直接放在for循环的内部
- 爬虫代码运行时候,for循环自动调用next方法,yield就会不断执行,直到爬取结束
- 使用yield也会大大减少爬虫运行时候的内存消耗