【Python】保姆级新手教程------第 11 章 迭代器 vs 生成器
第 11 章 迭代器 vs 生成器
迭代器
先区分两个概念:可迭代对象(iterable)、迭代器(iterator)
1️⃣可迭代对象(iterable)
概念:能被 for 循环遍历的对象,就是可迭代对象(iterable)
如下这些都是可迭代对象(iterable):
names=['张三','李四','王五']citys=('北京','上海','深圳')msg='hello'foriteminnames:print(item)如下这些不是可迭代对象(iterable):
age=10deftest():passforitemintest:print(item)endregion可迭代对象都拥有__iter__方法。
names=['张三','李四','王五']citys=('北京','上海','深圳')msg='hello'age=10deftest():passnames.__iter__()citys.__iter__()msg.__iter__()print(hasattr(names,'__iter__'))print(hasattr(citys,'__iter__'))print(hasattr(msg,'__iter__'))print(hasattr(age,'__iter__'))print(hasattr(test,'__iter__'))2️⃣迭代器(iterator)
调用__iter__方法会得到:迭代器(iterator)
- 备注1:
__iter__是一个魔法方法,当调用iter函数时,__iter__会自动调用。 - 备注2:
可迭代对象.__iter__()等价于:iter(可迭代对象)。 - 备注3:如果
iter(obj)能得到一个迭代器(iterator),那obj就是可迭代对象。
names=['张三','李四','王五']citys=('北京','上海','深圳')msg='hello'print(names.__iter__())print(citys.__iter__())print(msg.__iter__())print(iter(names))print(iter(citys))print(iter(msg))迭代器(iterator)拥有__next__方法,每次调用都会根据当前的状态,返回下一个元素。
- 备注1:
迭代器.__next__()等价于next(迭代器)。 - 备注2:当所有元素全都取出后,若继续调用
__next__,Python会抛出StopIteration异常。
names=['张三','李四','王五']it=iter(names)print(it.__next__())print(it.__next__())print(it.__next__())print(it.__next__())print(next(it))print(next(it))print(next(it))print(next(it))编写for循环遍历names列表
names=['张三','李四','王五']foriteminnames:print(item)for循环背后的逻辑
names=['张三','李四','王五']# 1️⃣调用【可迭代对象的__iter__方法】获取到一个迭代器(iterator)it=iter(names)# 2️⃣开启一个无限循环whileTrue:try:# 3️⃣调用__next__方法,获取下一个元素item=next(it)print(item)exceptStopIteration:# 4️⃣捕获 StopIteration 异常,随后结束循环break迭代器(iterator)也拥有__iter__方法,并且其返回值是迭代器自身。
这么设计的原因:让 for 循环也能遍历迭代器(即:为了让 iter(迭代器) 不出错)。
names=['张三','李四','王五']it=iter(names)print(it)result=iter(it)print(result)x=iter(result)print(x)it=iter(names)foriteminit:print(item):::info
迭代器协议:一个对象如果同时满足如下规范,那该对象就是一个迭代器:
- 能被
iter()接受。 - 能被
next()一步一步取值。
:::
迭代器是一次性的,状态只会向前推进,且不会自动重置(迭代器在遍历的过程中会被“消耗”)。
names=['张三','李四','王五']it1=iter(names)it2=iter(names)print(it1)print(it2)print(next(it1))print(next(it1))print(next(it1))print(next(it1))# 此行代码会抛出异常,因为此时迭代器已经被耗尽了# 如想重新依次获取元素,需要使用新的迭代器it2print(next(it2))print(next(it2))print(next(it2))3️⃣迭代器的应用
需求:让for循环可以遍历Person的实例对象。
实现方式1️⃣:
classPerson:def__init__(self,name,age,gender,address):self.name=name self.age=age self.gender=gender self.address=addressdef__iter__(self):returnPersonIterator(self)classPersonIterator:def__init__(self,p):# 将外部传进来的数据保存好self.p=p# 设置迭代器的初始化状态(指针位置)self.index=0# 配置好要遍历的内容self.attrs=[p.name,p.age,p.gender,p.address]# 迭代器的__iter__方法会返回迭代器自身def__iter__(self):returnself# 每次调用__next__方法,会根据当前的状态,返回下一个元素def__next__(self):# 如果指针的位置超出范围,那就抛出StopIteration异常ifself.index>=len(self.attrs):raiseStopIteration# 获取要返回的内容value=self.attrs[self.index]# 更新迭代器状态(指针位置)self.index+=1# 返回valuereturnvalue# 目标:p1=Person('张三',18,'男','北京昌平')foriteminp1:print(item)foriteminp1:print(item)实现方式2️⃣
classPerson:def__init__(self,name,age,gender,address):self.name=name self.age=age self.gender=gender self.address=address# 设置迭代器的初始化状态(指针位置)self.__index=0# 配置好要遍历的内容self.__attrs=[name,age,gender,address]def__iter__(self):self.__index=0returnselfdef__next__(self):# 如果指针的位置超出范围,那就抛出StopIteration异常ifself.__index>=len(self.__attrs):raiseStopIteration# 获取要返回的内容value=self.__attrs[self.__index]# 更新迭代器状态(指针位置)self.__index+=1# 返回valuereturnvalue# 目标:# 下面的p1既是可迭代对象,又是迭代器p1=Person('张三',18,'男','北京昌平')foriteminp1:print(item)foriteminp1:print(item)进阶:迭代器玩的就是__next__
fromcn2animportan2cnclassPerson:def__init__(self,name,age,gender,address):self.name=name self.age=age self.gender=gender self.address=address# 设置迭代器的初始化状态(指针位置)self.__index=0# 配置好要遍历的内容self.__attrs=[name,age,gender,address]def__iter__(self):self.__index=0returnselfdef__next__(self):# 如果指针的位置超出范围,那就抛出StopIteration异常ifself.__index>=len(self.__attrs):raiseStopIteration# 获取要返回的内容value=self.__attrs[self.__index]# 将字符串转为大写ifisinstance(value,str):value=value.upper()# 将数字转为汉语形式ifisinstance(value,int):value=an2cn(value)# 更新迭代器状态(指针位置)self.__index+=1# 返回valuereturnvalue# 目标:# 下面的p1既是可迭代对象,又是迭代器p1=Person('zhangsan',18,'男','北京昌平')foriteminp1:print(item)4️⃣迭代器的优势
- 迭代器是惰性计算,不会一次性生成所有结果,所以能显著降低内存占用。
- 当数据量很大,不确定要用多少结果时,推荐使用迭代器。
使用迭代器实现【斐波那契数列】:
classFibo:def__init__(self,total):# 要生成多少个数self.total=total# 当前生成到第几个了(计数器,指针)self.index=0# 初始的两个值self.pre=1self.cur=1def__iter__(self):returnselfdef__next__(self):# 当生成足够数量后,抛出StopIteration异常ifself.index>=self.total:raiseStopIteration# 前两项都是1ifself.index<2:value=1else:# 新的结果等于前两项的和value=self.pre+self.cur# 更新一下pre和curself.pre=self.cur self.cur=value# 计数器+1self.index+=1# 返回valuereturnvalue不使用迭代器实现【斐波那契数列】:
deffibo(total):iftotal<=0:return[]iftotal==1:return[1]nums=[1,1]foriinrange(2,total):nums.append(nums[-1]+nums[-2])returnnums分析内存占用情况:
分别运行如下两段代码后,会发现迭代器实现明显节约内存。
tracemalloc.start()f1=Fibo(0)m=tracemalloc.get_traced_memory()[1]print(f'内存占用是:{m/1024/1024}MB')tracemalloc.start()f1=fibo(0)m=tracemalloc.get_traced_memory()[1]print(f'内存占用是:{m/1024/1024}MB')生成器
1️⃣两个概念
- 生成器函数:函数体中如果出现了
yield关键字,那该函数是『生成器函数』。 - 生成器对象:调用『生成器函数』时,其函数体不会立刻执行,而是返回一个『生成器对象』。
备注:不管能否执行到
yield所在的位置,只要函数中有yield,那该函数就是『生成器函数』。
defdemo():print('demo函数开始执行了')print(100)yielda=200print(a)d=demo()print(d)2️⃣几个细节
写在『生成器函数』中的代码,需要通过『生成器对象』来执行:
:::info
- 调用『生成器对象』的
__next__方法,会让『生成器函数』中的代码开始执行。 - 当『生成器函数』中的代码开始执行后,遇到
yield会“暂停”,并会记录“暂停”的位置。 - 后续调用
__next__方法时,都会从上一次“暂停”的位置,继续运行,直到再次遇到 yield。 - 遇到
return会抛出StopIteration异常,并将return后面的表达式,作为异常信息。 yield后面所写的表达式,会作为本次__next__方法的返回值。
:::
defdemo():print('demo函数开始执行了')print(100)yield'我是第1个yield所返回的数据'a=200print(a)yield'我是第2个yield所返回的数据'b=300print(b)return'尚硅谷'd=demo()r1=next(d)print(r1)r2=next(d)print(r2)try:next(d)exceptStopIterationase:print(e)生成器对象是一种特殊的迭代器(本质是通过yield自动实现了迭代器协议)
defdemo():print('demo函数开始执行了')print(100)yield'我是第1个yield所返回的数据'a=200print(a)yield'我是第2个yield所返回的数据'b=300print(b)return'尚硅谷'd=demo()# 验证:生成器对象d,和迭代器一样,也拥有:__iter__ 和 __next__ 方法print(hasattr(d,'__iter__'))print(hasattr(d,'__next__'))# 验证:生成器对象的__iter__方法,和迭代器一样,返回的也是自身result=iter(d)print(result==d)# for循环遍历生成器foritemind:print(item)# for循环背后的逻辑gen=iter(d)whileTrue:try:value=next(gen)print(value)exceptStopIteration:breakyield也能写在循环里
regiondefcreate_car(total):forindexinrange(1,total+1):yieldf'我是第{index}台车'# cars是生成器对象cars=create_car(5)# 调用一次cars的__next__方法,就会得到一台车c1=next(cars)print(c1)c2=next(cars)print(c2)c3=next(cars)print(c3)c4=next(cars)print(c4)c5=next(cars)print(c5)forcarincars:print(car)endregionyield from能把一个『可迭代对象』里的东西依次yield出去。(替代:for + yield)
defdemo():nums=[10,20,30,40]yieldfromnums d=demo()r1=next(d)print(r1)r2=next(d)print(r2)r3=next(d)print(r3)r4=next(d)print(r4)foritemind:print(item)使用:生成器.send(值)可以让生成器继续执行的同时,给上一次yield传值。
备注1:
next只能取值,send既能取值,也能送值。备注2:第一次启动生成器,不能传值!(或者说只能传 None 值)
defdemo():print('demo函数开始执行了')print(100)a=yield'我是第1个yield所返回的数据'print(a)b=yield'我是第2个yield所返回的数据'print(b)return'尚硅谷'd=demo()r1=next(d)# 此处等价于 d.send(None)print(r1)r2=d.send(666)print(r2)try:d.send(888)exceptStopIterationase:print(e)3️⃣生成器的应用
用生成器实现遍历Person类的实例对象:
classPerson:def__init__(self,name,age,gender,address):self.name=name self.age=age self.gender=gender self.address=address self.__attr=[name,age,gender,address]def__iter__(self):# yield self.name# yield self.age# yield self.gender# yield self.addressyieldfromself.__attr p1=Person('张三',18,'男','北京昌平')# 目标:forattrinp1:print(attr)用生成器实现斐波那契数列:
deffibo(total):pre=1cur=1forindexinrange(total):ifindex<2:yield1else:value=pre+cur pre=cur cur=valueyieldvalue f1=fibo(10)foriteminf1:print(item)无论是迭代器,还是生成器对象,都可以用list、tuple、set等直接拿到其里面的所有内容(注意:如果数据量很大,可能会挤爆内存)
deffibo(total):pre=1cur=1forindexinrange(total):ifindex<2:yield1else:value=pre+cur pre=cur cur=valueyieldvalue f1=fibo(10)result=set(f1)print(result)4️⃣生成器表达式
生成器表达式:一种用类似列表推导式的语法,快速创建生成器对象的方式。
语法格式:(表达式 for 变量 in 可迭代对象)。
什么时候适合用生成器表达式?———— 当“每个结果,只依赖当前这一个元素”时。
nums=[10,20,30,40]# 列表推导式result1=[n*2forninnums]print(result1)# 生成器表达式(和列表推导式很像,不要搞混)result2=(n*2forninnums)foriteminresult2:print(item)