❝Python入门第二十六课,主要是学习了迭代器,迭代器提供按需逐个访问元素的方式,统一了序列的遍历接口,实现惰性计算与内存优化。
什么是迭代器
知道什么是迭代器之前,需要先区分清楚两个概念:可迭代对象(iterable)、迭代器(iterator)。
可迭代对象(iterable)
概念:能被for循环遍历的对象,就是可迭代对象(iterable)。
例如,列表、元组、字符串等都是可迭代对象,但是数字int、float就不是迭代对象。
names = ['乔丹', '科比', '詹姆斯']cities =('北京', '上海', '深圳')lang = 'python'for name in names: print(name)print()for city in cities: print(city)print()for char in lang: print(char)
可迭代对象都拥有__iter__方法。
names = ['乔丹', '科比', '詹姆斯']cities =('北京', '上海', '深圳')lang = 'python'age = 100deftest():pass# 主动调用__iter__()方法names.__iter__()cities.__iter__()lang.__iter__()# age.__iter__() # 报错:AttributeError: 'int' object has no attribute '__iter__'# 通过hasattr()检测对象是否有__iter__()方法print(hasattr(names, '__iter__')) # Trueprint(hasattr(cities, '__iter__')) # Trueprint(hasattr(lang, '__iter__')) # Trueprint(hasattr(age, '__iter__')) # Falseprint(hasattr(test, '__iter__')) # False
迭代器(iterator)
可迭代对象调用__iter__()方法会得到:迭代器(iterator)。
注意:
❏ __iter__()方法是一个魔术方法,当调用iter()函数时,__iter__()会自动调用。
❏ 可迭代对象.__iter__()等价于:iter(可迭代对象)。
❏ 如果item(obj)能得到一个迭代器(iterator),那obj就是可迭代对象。
names = ['乔丹', '科比', '詹姆斯']cities =('北京', '上海', '深圳')lang = 'python'print(names.__iter__()) # <list_iterator object at 0x000001794C4ABBB0>print(iter(names)) # <list_iterator object at 0x000001794C4ABBB0>print(cities.__iter__()) # <tuple_iterator object at 0x000001794C4ABBB0>print(iter(cities)) # <tuple_iterator object at 0x000001794C4ABBB0>print(lang.__iter__()) # <str_ascii_iterator object at 0x000001794C4ABBB0>print(iter(lang)) # <str_ascii_iterator object at 0x000001794C4ABBB0>
迭代器(iterator)拥有__next__()方法,每次调用都会根据当前的状态,返回下一个元素。
注意:
❏ 迭代器.__next__()等价于next(迭代器)。
❏ 当所有元素全部都取出后,若继续调用__next()__(),Python 会抛出StopIteration异常。
names = ['乔丹', '科比', '詹姆斯']it = names.__iter__()print(it.__next__())print(it.__next__())print(it.__next__())# print(it.__next__()) # 此行代码运行时会抛出 StopIteration 异常it2 = iter(names)print(next(it2))print(next(it2))print(next(it2))# print(next(it2)) # 此行代码运行时会抛出 StopIteration 异常
拓展:使用迭代器实现for循环背后的逻辑
# 拓展:使用迭代器实现for循环背后的逻辑names = ['乔丹', '科比', '詹姆斯']# 1. 调用【可迭代对象的__iter__方法】获取到一个迭代器it = iter(names)# 2. 开启一个无限循环loop_index: int = 0whileTrue: loop_index += 1try:# 3. 调用__next__方法,获取下一个元素 item = next(it) print(f'{loop_index}. {item}')except StopIteration:# 4. 捕获StopIteration异常,随后结束循环break
迭代器(iterator)也拥有__iter__方法,并且其返回值是迭代器自身。
这样设计的原因是:让for循环也能遍历迭代器(即:为了让iter(迭代器)不出错)。
names = ['乔丹', '科比', '詹姆斯']it = iter(names)print(it) # <list_iterator object at 0x000001E43E19BAC0>result = iter(it)print(result) # <list_iterator object at 0x000001E43E19BAC0>xt = iter(result)print(xt) # <list_iterator object at 0x000001E43E19BAC0>for item in xt: print(item)
迭代器协议
❝一个对象如果同时满足如下规范,那么该对象就是一个迭代器:
迭代器是一次性的,状态只会向前推进,且不会自动重置(迭代器在遍历的过程中会被“消耗”)。
names = ['乔丹', '科比', '詹姆斯']it1 = iter(names)it2 = iter(names)# it1 和 it2 是两个迭代器,内存地址不同print(it1) # <list_iterator object at 0x000001E4396CB8E0>print(it2) # <list_iterator object at 0x000001E4396CB910>print(next(it1)) # 乔丹print(next(it1)) # 科比print(next(it1)) # 詹姆斯# print(next(it1)) # 因为迭代器元素通过next已经被读取完了,运行到此行会抛出StopIteration异常# 如果想重新依次获取元素,需要使用新的迭代器it2print(next(it2)) # 乔丹print(next(it2)) # 科比print(next(it2)) # 詹姆斯
迭代器的应用
❝需求:让for循环可以遍历Person的实例对象。
➊ 实现方式一:Person借助额外的迭代器实现。
classPerson:def__init__(self, name, age, gender, address): self.name = name self.age = age self.gender = gender self.address = addressdef__iter__(self):return PersonIterator(self)classPersonIterator:def__init__(self, person):# 将外部传进来的数据保存好 self.person = person# 设置迭代器的初始化状态(指针位置) self.index = 0# 配置好要遍历的内容 self.attrs = [person.name, person.age, person.gender, person.address]# 迭代器的__iter__方法会返回迭代器自身def__iter__(self):return self# 每次调用__next__方法,会根据当前的状态,返回下一个元素def__next__(self):# 如果指针的位置超出范围,那就抛出StopIteration异常if self.index >= len(self.attrs):raise StopIteration# 获取要返回的内容 value = self.attrs[self.index]# 更新迭代器状态(指针位置) self.index += 1# 返回valuereturn valuep1 = Person('远方', 41, '男', '山西太原')for attr in p1: print(attr)print('--' * 10)for attr in p1: print(attr)# 运行结果如下:# 远方# 41# 男# 山西太原# --------------------# 远方# 41# 男# 山西太原
➋ 实现方式二:Person自身扩展成为迭代器。
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 = 0return selfdef__next__(self):# 如果指针的位置超出了范围,则抛出StopIteration异常if self.__index >= len(self.__attrs):raise StopIteration# 获取要返回的内容 value = self.__attrs[self.__index]# 更新迭代器的状态(指针位置) self.__index += 1# 返回valuereturn valuep2 = Person('远方', 41, '男', '山西太原')for item in p2: print(item)print('--' * 10)for item in p2: print(item)# 运行结果如下:# 远方# 41# 男# 山西太原# --------------------# 远方# 41# 男# 山西太原
重点:迭代器的关键应用就是__next__,在__next__方法中可以加工处理迭代的元素。
from cn2an import an2cnclassPerson: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 = 0return selfdef__next__(self):# 如果指针的位置超出了范围,则抛出StopIteration异常if self.__index >= len(self.__attrs):raise StopIteration# 获取要返回的内容 value = self.__attrs[self.__index]# 加工value:将字符串转为大写if isinstance(value, str): value = value.upper()# 加工value:将数字转为汉语形式if isinstance(value, int): value = an2cn(value)# 更新迭代器的状态(指针位置) self.__index += 1# 返回valuereturn valuep2 = Person('yuanfang', 41, '男', '山西太原')for item in p2: print(item)# 运行结果如下:# YUANFANG# 四十一# 男# 山西太原
迭代器的优势
迭代器是惰性计算,不会一次性生成所有结果,所以能显著降低内存占用。
当数据量很大,不确定要用多少结果时,推荐使用迭代器。
➊ 使用迭代器实现【斐波那契额数列】:
classFibo:def__init__(self, total):# 要生成多少个数 self.total = total# 当前生成到第几个了(计数器,指针) self.index = 0# 初始的两个值 self.pre = 1 self.cur = 1def__iter__(self):return selfdef__next__(self):# 当生成足够数量后,抛出StopIteration异常if self.index >= self.total:raise StopIteration# 前两项都是1if self.index < 2: value = 1else:# 新的结果等于前两项的和 value = self.pre + self.cur# 更新pre和cur self.pre = self.cur self.cur = value# 移动迭代器指针+1 self.index += 1# 返回valuereturn valuef = Fibo(10)print(f)result = []for n in f: result.append(n)print(result) # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
➋ 不使用迭代器实现【斐波那契额数列】:
deffibo(total):if total <= 0:return []elif total == 1:return [1] nums = [1, 1]for i in range (2, total): nums.append(nums[-1] + nums[-2])return numsresult2 = fibo(10)print(result2) # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
对比分析上面两种实现方式内存占用情况:
import tracemalloctracemalloc.start()f1 = Fibo(100000)m1 = tracemalloc.get_traced_memory()[1]print(f'迭代器实现方式占用内存:{m1 / 1024 / 1024}MB')# 迭代器实现方式占用内存:0.00030517578125MBtracemalloc.stop()tracemalloc.start()f2 = fibo(100000)m2 = tracemalloc.get_traced_memory()[1]print(f'非迭代器实现方式占用内存:{m2 / 1024 / 1024}MB')# 非迭代器实现方式占用内存:444.99246978759766MBtracemalloc.stop()
通过运行结果,对于数据量比较大的情况下,会发现迭代器实现明显节约内存。
❝原理是:迭代器创建实例对象后,并未真正生成数据,只有遍历迭代器或者通过next()方法获取元素时,才会真正生成元素占用内存;而非迭代器模式,则运行时就会生成完整的数据。