Python基础之迭代器和生成器
一、迭代器
1.1 什么是可迭代对象?
字符串、列表、元组、字典、集合都可以被for循环,说明它们都是可迭代的。
我们怎么来证明这一点呢?
from collections import Iterable lst = [1, 2, 3, 4] tup = (1, 2, 3, 4) dic = {1: 2, 3: 4} se = {1, 2, 3, 4} print(isinstance(lst, Iterable)) # True print(isinstance(tup, Iterable)) # True print(isinstance(dic, Iterable)) # True print(isinstance(se, Iterable)) # True
1.2 可迭代协议
我们现在是从结果分析原因,能被for循环的就是“可迭代的”,但是如果正着想,for怎么知道可迭代的呢?
假如我们自己写了一个数据类型,希望这个数据类型里的东西也可以使用for被一个一个的取出来,那我们就必须满足for的要求,这个要求就叫做“协议”。
可以被迭代要吗uzud要求,就叫做可迭代协议。
可迭代协议的定义非常简单,就是内部实现了__iter__方法。
接下来我们就来验证一下:
1 print(dir([1,2])) 2 print(dir((2,3))) 3 print(dir({1:2})) 4 print(dir({1,2})) 5 6 结果: 7 ['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort'] 8 ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index'] 9 ['__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values'] 10 ['__and__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__iand__', '__init__', '__ior__', '__isub__', '__iter__', '__ixor__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__xor__', 'add', 'clear', 'copy', 'difference', 'difference_update', 'discard', 'intersection', 'intersection_update', 'isdisjoint', 'issubset', 'issuperset', 'pop', 'remove', 'symmetric_difference', 'symmetric_difference_update', 'union', 'update']
总结一下我们现在所知道的:可以被for循环的都是可迭代的,要想可迭代,内部必须有一个__iter__方法。
接着分析,__iter__方法做了什么事情呢?
1.3 迭代器
什么叫做迭代器?迭代器的英文意思是iterator。
lst = [1, 2, 3, 4] lst_iter = lst.__iter__() # 将可迭代的转化成迭代器 item = lst_iter.__next__() print(item) item = lst_iter.__next__() print(item) item = lst_iter.__next__() print(item) item = lst_iter.__next__() print(item)
迭代器遵循迭代器协议:必须拥有__iter__方法和__next__方法。
for循环,能遍历一个可迭代对象,它的内部到底进行了什么?
将可迭代对象转换成迭代器。(可迭代对象.__iter__())
内部使用__next__方法,一个一个取值。
加了异常处理功能,取值到底后自动停止。
用while循环模拟for循环:
lst = [1, 2, 3, 4] lst_iter = lst.__iter__() while True: try: item = lst_iter.__next__() print(item) except StopIteration: break
1.4 为什么要有for循环
基于上面所讲的一大堆遍历公式,都是可以通过下标访问的,那么为什么还要通过for循环来调用呢?
lst = [1, 2, 3] index = 0 while index < len(lst): print(lst[index]) index += 1
没错,序列类型字符串,列表,元组都有下标,可以通过上面循环的方式来访问。但是非序列类型象字典、集合、文件对象是没有办法通过下标来获取的。
所以,for循环就是基于迭代器协议提供了一个统一的可以遍历所有对象的方法,即在遍历之前,先调用对象的__iter__方法将其转换成一个迭代器,然后使用迭代器协议去实现循环访问,这样所有的对象都可以通过for循环来遍历了。
最重要的一点是,转换成迭代器,在循环时,同一时刻在内存中只出现一条数据,极大限度的节省了内存。
二、生成器
2.1 初始生成器
我们知道的迭代器有两种:一种是调用方法直接返回的,一种是可迭代对象通过执行iter方法得到的,迭代器的好处是可以节省内存。
如果在某些情况下,我们也需要节省内存,就只能自己写。我们自己写的这个能实现迭代器功能东西就叫生成器。
Python中提供的生成器:
(1)生成器函数:常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次只返回一个结果,在每个结果中间,挂起函数的状态,以便下次从它离开的地方继续执行。
(2)生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表。
2.2 生成器函数
一个包含yield关键字的函数就是一个生成器函数。yield可以为我们从函数中返回值,但是yield又不同于return,return的执行意外着程序的结束,调用生成器函数不会得到返回的具体的值,而是得到一个可迭代的对象。
每一次获取这个可迭代对象的值,就能推动函数的执行,获取新的返回值,直到函数执行结束。
import time def genrator_func(): a = 1 print("现在定义了a变量") yield a b = 2 print("现在定义了b变量") yield b g = genrator_func() print("g", g) # 打印g发现g就是一个生成器 print("*" * 10) print(next(g)) time.sleep(2) print(next(g))
生成器有什么好处呢?就是不会一下子在内存中生成太多数据。
加入我想让工厂给学生做校服,生产2000000件校服,工厂应该是先答应下来,然后再去生产,我可以一件一件的要,也可以根据学生一批一批的找工厂拿。而不是一说要生产2000000件衣服,工厂就一次性生产2000000件衣服。
def produce(): """ 生产衣服 :return: """ for i in range(1, 20000001): yield f"生产了{i}件衣服" production = produce() print(production.__next__()) print(production.__next__()) print(production.__next__()) num = 0 for i in production: print(i) num += 1 if num > 5: break
2.3 send
send获取下一个值的效果和next基本一致,只是在获取下一个值的时候,给上一个yield的位置传递一个数据。
使用send的注意事项:
(1)第一次使用生成器的时候,是用next获取下一个值
(2)最后一个yield不能接受外部的值
def generator(): print(123) content = yield 1 print("===========>", content) print(456) yield 2 g = generator() result = g.__next__() print("===", result) result = g.send("hello") # send的效果和next一样 print("===", result)
三、列表推导式和生成器表达式
lst = [i for i in range(10)] print(lst) lst = [f"选项{i}" for i in range(10)] print(lst)
(1)把列表解析的[]换成()得到的就是生成器表达式
lst = (i for i in range(10)) print(lst)
(2)列表解析与生成器表达式都是一种便利的编程方式,只不过生成器表达式更节省内存。
(3)python不但使用迭代器协议,让for循环变得更加通用。大部分内置函数,也是使用迭代器协议访问对象的。例如,sum函数是python的内置函数,该函数使用迭代器协议访问对象,而生成器实现了迭代器协议。所以我们可以直接这样计算一系列值的和:
sum(x ** 2 for x in range(4))