生成器、迭代器、可迭代对象
可迭代对象
在编程中,我们最常见的需求就是"遍历一堆数据"。比如一个列表:
my_list = [10, 20, 30]
for item in my_list:
print(item)
运行结果
10
20
30
我们自然有一个疑问,什么情况下可以使用for循环? python常用数据类型无非就是:int,float,str,tuple,list,dict,set及类定义的对象。 这些是不是可以用for去遍历?set无序是不是也可以遍历?代码验证:
- int: TypeError: 'int' object is not iterable
- float: TypeError: 'float' object is not iterable
- str: 可以遍历
- tuple: 可以遍历
- list: 可以遍历
- dict 可以遍历
- set 可以遍历,但是每次结果顺序不一样
为何有些可以,有些不可以,我们对所有类型进行for操作,for碰到int,float就不行。我们知道python一切皆对象,是不是遇到了对象含有某个属性或者方法就可以实现遍历?
Python 规定,只要你的对象内部实现了一个叫 iter()方法,你就能通过for循环遍历,那我们就用代码打印看一下:
n1 = 100
f1 = 100.123
s1 = "hello"
t1 = (1, 2, 3)
l1 = [4, 5, 6]
d1 = {"k1": "v1", "k2": "v2"}
set1 = {"name", "age"}
print("__iter__" in dir(n1))
print("__iter__" in dir(f1))
print("__iter__" in dir(s1))
print("__iter__" in dir(t1))
print("__iter__" in dir(l1))
print("__iter__" in dir(d1))
print("__iter__" in dir(set1))
输出结果:
False
False
True
True
True
True
True
注:更严谨的判断用 from collections.abc import Iterable print(isinstance(obj, Iterable)) 结果显示可以遍历的都有__iter__()方法,所以可以使用for,这些内部有 iter() 方法的称为可迭代对象
除了这几个类型,那就还有基于类实现的对象,对象无非继承object,要么自定义类,自定义类也是继承object,没写就出发MRO找,我们看下object有没有:
print("__iter__" in dir(object))
False
没有属于正常,object几乎是所有类基类,你要遍历啥,我object怎么会知道?还不如你自己实现 所以类对象是没有的,但是我们可以在类中自己写__iter__()方法,借助系统内置方法iter()去实现,不过传进去的也必须是可迭代的,因为iter()就是借用了你传进去可迭代对象的__iter__()。
所以for循环需要可迭代对象,意思就是for底层就是借助了可迭代对象的写__iter__()方法? 答案是的,我们先继续往下讲
迭代器
在 Python 的世界里,迭代器(Iterator) 的通俗定义是:一个专门用来记录“当前循环到哪了”,并且能够一件件把数据送出来的“光标传送带”。 说可迭代对象(如打开的普通文件 f)是一个静态的、允许被读取的“数据源头”;那么迭代器就是那个在这个源头上移动的、动态的“读取光标”。 假设我们打开了一个记录了 3 行用户数据的普通文本文件:
f = open("users.txt", "r")
此时,你把 f 想象成一本书。书就静静地躺在桌子上,它是一个静态的对象。 如果我们要去读这本书,我们必须做两件事:
- 拿一根手指指着当前正在读的那一行。
- 读完一行后,手指要向下滑动到下一行。 在计算机的内存世界里,这根负责指引、滑动的“手指”,就是【迭代器】。 如果没有这根“手指”,光靠书(文件对象)本身,它是没办法自己记住“刚才读到哪一行了”的。所以,当我们用 for line in f: 去遍历文件时,Python 必须在后台通过 f.iter() 帮我们创造出一根专门针对这个文件的“手指(迭代器)”。
迭代器的核心机制
这根由 Python 秘密创建出来的“手指(迭代器)”,在底层必须严格遵守两项行为规范(在 Python 中被称为迭代器协议):
-
它必须有 next() 方法 这是迭代器最核心的本领。它像一个“打卡机”,你每调用一次 next(),它在底层就把文件指针向后滑动一行,并把这一行的内容吐给你。它是惰性的。你调一次,它动一次。不调,它就在内存里死死按住当前位置,绝不提前多读任何数据。
-
它只能往前,不能回头 这根“手指”在文件里只能从头滑到尾,没有“倒车档”。一旦它读完了最后一行,你再次调 next(),它就会在内存里触发一个 StopIteration 的报警信号,告诉外层的 for 循环:“我已经指到最后一页的空白处了,没东西了!”
迭代器更专业的描述 迭代器是一个实现了迭代器协议(Iterator Protocol)的对象。该协议强制要求对象必须同时具备两个特殊方法:
- iter():返回迭代器对象本身(确保其可被 for 循环或 iter() 函数识别)。
- next():返回序列中的下一个元素。若没有更多元素,则抛出 StopIteration 异常以终止遍历。
可迭代对象本身具有__iter__(),所以迭代器就是实现了__next__()方法的可迭代对象是就是迭代器 python内置了一个next()函数,他会调用你传入的对象的__next__()方法,没有报错说明传入的对象实现了这个方法,我们尝试将列表,字典,元组,集合数据类型放进去,发现都会报错,说明这些类型虽然是可迭代对象,但是不是迭代器。 我们对列表,字典,元组,集合遍历时往往用for循环,那说明for底层根据__iter__()方法生成了一个迭代器,事实也是这样。
for 循环的步骤:
- (1)获取迭代器(初始化阶段,仅执行 1 次): for 循环底层自动调用 可迭代对象的的__iter__()方法生成迭代器
- (2)调用 next()方法(循环阶段 - 取数据) 迭代器接收到单步指令,光标向前移动一格(或执行一步计算公式),将当前位置的单个元素内容读取并加载到内存中。
- (3)执行业务代码(循环阶段 - 用数据) 将读取到的元素赋值给循环变量(如 item 或 line),随后执行 for 循环内部的用户自定义业务代码。当前轮次业务执行完毕后,该元素占用的局部内存会被立即释放或冲走,实现极致的内存节约。
- (4) 触发异常,优雅退出(触底收尾阶段,仅执行 1 次) 重复执行步骤 (2) 和 (3)。当数据全部读取完毕、光标触底时,再次调用 next() 会触发迭代器协议规定的 StopIteration 异常。for 循环在底层捕获该异常后,会自动安全地切断循环,优雅退出。
for循环经常会与range的搭配,控制循环多少次。所有某些场景有些大佬总会问你几个问题:
- range(100)是什么?
- python2: 是一个列表,里面数字多大,这个列表就多大
- python3: 是一个可以迭代对象,而且是一个惰性对象对象,无论数字多大,内存开销永远是恒定的(O(1) 复杂度)。
- range(10000000)内存开销多大?
这里就只讲python3:它的内存占用极其微小(只有几十个字节),且不管括号里的数字变成多大,它的内存占用都是恒定不变的。
底层原因:
- 因为 range 遵循了惰性求值原则。它在内存中根本不存储一亿个数字,而是仅仅存储了 start(起始值)、stop(结束值)和 step(步长)这三个基础整数。默认是start 0,步骤 1,stop 填入的数字(不包含)
- 当它配合 for 循环进入“通用步骤”时,只有在步骤 (2) 调用 next() 的那一瞬间,它才会根据公式现场做一次数学加法计算,把当前的数字吐出来。用完之后立刻释放,因此实现了空气般的轻量化。
生成器
生成器是一种特殊的迭代器,它通过 yield 关键字实现惰性求值,能够按需逐个产生数据而无需一次性将所有结果加载到内存中
假设我们要处理内存中的 1,000,000(100万)条电商订单积分数据。我们需要把这 100 万个积分全部乘以 2(翻倍),然后传给下一个业务去发放奖励。
- 方案 A:没有生成器
import sys
import time
print("--- [ 方案A:传统列表模型 ] 开始运行 ---")
# 1. 模拟 1000 万条原始订单积分数据
print("1. 正在内存中初始化 1000 万条原始订单数据...")
start_time = time.time()
point_data = list(range(10000000)) # 1000万数据
print(f" 原始数据初始化耗时: {time.time() - start_time:.2f} 秒")
# 2. 没有生成器的列表计算函数
def double_points_list(points_list):
result_list = [] # 👈 必须开辟新列表囤货
for p in points_list:
result_list.append(p * 2) # 💥 瞬间在内存里堆积 1000 万个新数字
return result_list
# 3. 触发业务调用
print("\n2. 开始执行翻倍计算并整体返回大列表...")
calc_start = time.time()
# 这一行执行完后,1000万个计算结果已经全部死死卡在内存里了!
final_list = double_points_list(point_data)
print(f" 函数计算并返回耗时: {time.time() - calc_start:.2f} 秒")
# 4. 模拟外界一个一个消费
print("\n3. 外界 for 循环开始‘一个一个’消费大列表...")
for new_point in final_list:
pass # 模拟真正的业务处理
# 5. 打印这个成品大列表此时在内存里死死咬住的空间
list_memory = sys.getsizeof(final_list) / 1024 / 1024
print(f"\n🚨 【内存结果】列表计算容器在内存中死死咬住了: {list_memory:.2f} MB")
print("--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ---")
import sys
import time
print("--- [ 方案A:传统列表模型 ] 开始运行 ---")
# 1. 模拟 1000 万条原始订单积分数据
print("1. 正在内存中初始化 1000 万条原始订单数据...")
start_time = time.time()
point_data = list(range(10000000)) # 1000万数据
print(f" 原始数据初始化耗时: {time.time() - start_time:.2f} 秒")
# 2. 没有生成器的列表计算函数
def double_points_list(points_list):
result_list = [] # 👈 必须开辟新列表囤货
for p in points_list:
result_list.append(p * 2) # 💥 瞬间在内存里堆积 1000 万个新数字
return result_list
# 3. 触发业务调用
print("\n2. 开始执行翻倍计算并整体返回大列表...")
calc_start = time.time()
# 这一行执行完后,1000万个计算结果已经全部死死卡在内存里了!
final_list = double_points_list(point_data)
print(f" 函数计算并返回耗时: {time.time() - calc_start:.2f} 秒")
# 4. 模拟外界一个一个消费
print("\n3. 外界 for 循环开始‘一个一个’消费大列表...")
for new_point in final_list:
pass # 模拟真正的业务处理
# 5. 打印这个成品大列表此时在内存里死死咬住的空间
list_memory = sys.getsizeof(final_list) / 1024 / 1024
print(f"\n🚨 【内存结果】列表计算容器在内存中死死咬住了: {list_memory:.2f} MB")
print("--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ---")
import sys
import time
print("--- [ 方案A:传统列表模型 ] 开始运行 ---")
# 1. 模拟 1000 万条原始订单积分数据
print("1. 正在内存中初始化 1000 万条原始订单数据...")
start_time = time.time()
point_data = list(range(10000000)) # 1000万数据
print(f" 原始数据初始化耗时: {time.time() - start_time:.2f} 秒")
# 2. 没有生成器的列表计算函数
def double_points_list(points_list):
result_list = [] # 👈 必须开辟新列表囤货
for p in points_list:
result_list.append(p * 2) # 💥 瞬间在内存里堆积 1000 万个新数字
return result_list
# 3. 触发业务调用
print("\n2. 开始执行翻倍计算并整体返回大列表...")
calc_start = time.time()
# 这一行执行完后,1000万个计算结果已经全部死死卡在内存里了!
final_list = double_points_list(point_data)
print(f" 函数计算并返回耗时: {time.time() - calc_start:.2f} 秒")
# 4. 模拟外界一个一个消费
print("\n3. 外界 for 循环开始‘一个一个’消费大列表...")
for new_point in final_list:
pass # 模拟真正的业务处理
# 5. 打印这个成品大列表此时在内存里死死咬住的空间
list_memory = sys.getsizeof(final_list) / 1024 / 1024
print(f"\n🚨 【内存结果】列表计算容器在内存中死死咬住了: {list_memory:.2f} MB")
print("--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ---")
import sys
import time
print("--- [ 方案A:传统列表模型 ] 开始运行 ---")
# 1. 模拟 1000 万条原始订单积分数据
print("1. 正在内存中初始化 1000 万条原始订单数据...")
start_time = time.time()
point_data = list(range(10000000)) # 1000万数据
print(f" 原始数据初始化耗时: {time.time() - start_time:.2f} 秒")
# 2. 没有生成器的列表计算函数
def double_points_list(points_list):
result_list = [] # 👈 必须开辟新列表囤货
for p in points_list:
result_list.append(p * 2) # 💥 瞬间在内存里堆积 1000 万个新数字
return result_list
# 3. 触发业务调用
print("\n2. 开始执行翻倍计算并整体返回大列表...")
calc_start = time.time()
# 这一行执行完后,1000万个计算结果已经全部死死卡在内存里了!
final_list = double_points_list(point_data)
print(f" 函数计算并返回耗时: {time.time() - calc_start:.2f} 秒")
# 4. 模拟外界一个一个消费
print("\n3. 外界 for 循环开始‘一个一个’消费大列表...")
for new_point in final_list:
pass # 模拟真正的业务处理
# 5. 打印这个成品大列表此时在内存里死死咬住的空间
list_memory = sys.getsizeof(final_list) / 1024 / 1024
print(f"\n🚨 【内存结果】列表计算容器在内存中死死咬住了: {list_memory:.2f} MB")
print("--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ---")
import sys
import time
print("--- [ 方案A:传统列表模型 ] 开始运行 ---")
# 1. 模拟 1000 万条原始订单积分数据
print("1. 正在内存中初始化 1000 万条原始订单数据...")
start_time = time.time()
point_data = list(range(10000000)) # 1000万数据
print(f" 原始数据初始化耗时: {time.time() - start_time:.2f} 秒")
# 2. 没有生成器的列表计算函数
def double_points_list(points_list):
result_list = [] # 👈 必须开辟新列表囤货
for p in points_list:
result_list.append(p * 2) # 💥 瞬间在内存里堆积 1000 万个新数字
return result_list
# 3. 触发业务调用
print("\n2. 开始执行翻倍计算并整体返回大列表...")
calc_start = time.time()
# 这一行执行完后,1000万个计算结果已经全部死死卡在内存里了!
final_list = double_points_list(point_data)
print(f" 函数计算并返回耗时: {time.time() - calc_start:.2f} 秒")
# 4. 模拟外界一个一个消费
print("\n3. 外界 for 循环开始‘一个一个’消费大列表...")
for new_point in final_list:
pass # 模拟真正的业务处理
# 5. 打印这个成品大列表此时在内存里死死咬住的空间
list_memory = sys.getsizeof(final_list) / 1024 / 1024
print(f"\n🚨 【内存结果】列表计算容器在内存中死死咬住了: {list_memory:.2f} MB")
print("--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ---")
import sys
import time
print("--- [ 方案A:传统列表模型 ] 开始运行 ---")
# 1. 模拟 1000 万条原始订单积分数据
print("1. 正在内存中初始化 1000 万条原始订单数据...")
start_time = time.time()
point_data = list(range(10000000)) # 1000万数据
print(f" 原始数据初始化耗时: {time.time() - start_time:.2f} 秒")
# 2. 没有生成器的列表计算函数
def double_points_list(points_list):
result_list = [] # 👈 必须开辟新列表囤货
for p in points_list:
result_list.append(p * 2) # 💥 瞬间在内存里堆积 1000 万个新数字
return result_list
# 3. 触发业务调用
print("\n2. 开始执行翻倍计算并整体返回大列表...")
calc_start = time.time()
# 这一行执行完后,1000万个计算结果已经全部死死卡在内存里了!
final_list = double_points_list(point_data)
print(f" 函数计算并返回耗时: {time.time() - calc_start:.2f} 秒")
# 4. 模拟外界一个一个消费
print("\n3. 外界 for 循环开始‘一个一个’消费大列表...")
for new_point in final_list:
pass # 模拟真正的业务处理
# 5. 打印这个成品大列表此时在内存里死死咬住的空间
list_memory = sys.getsizeof(final_list) / 1024 / 1024
print(f"\n🚨 【内存结果】列表计算容器在内存中死死咬住了: {list_memory:.2f} MB")
print("--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ---")
运行结果
--- [ 方案A:传统列表模型 ] 开始运行 ---
1. 正在内存中初始化 1000 万条原始订单数据...
原始数据初始化耗时: 0.10 秒
2. 开始执行翻倍计算并整体返回大列表...
函数计算并返回耗时: 0.29 秒
3. 外界 for 循环开始‘一个一个’消费大列表...
🚨 【内存结果】列表计算容器在内存中死死咬住了: 84.97 MB
--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ---
--- [ 方案A:传统列表模型 ] 开始运行 ---
1. 正在内存中初始化 1000 万条原始订单数据...
原始数据初始化耗时: 0.21 秒
2. 开始执行翻倍计算并整体返回大列表...
函数计算并返回耗时: 0.36 秒
3. 外界 for 循环开始‘一个一个’消费大列表...
🚨 【内存结果】列表计算容器在内存中死死咬住了: 84.97 MB
--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ---
--- [ 方案A:传统列表模型 ] 开始运行 ---
1. 正在内存中初始化 1000 万条原始订单数据...
原始数据初始化耗时: 0.14 秒
2. 开始执行翻倍计算并整体返回大列表...
函数计算并返回耗时: 0.33 秒
3. 外界 for 循环开始‘一个一个’消费大列表...
🚨 【内存结果】列表计算容器在内存中死死咬住了: 84.97 MB
--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ------ [ 方案A:传统列表模型 ] 开始运行 ---
1. 正在内存中初始化 1000 万条原始订单数据...
原始数据初始化耗时: 0.10 秒
2. 开始执行翻倍计算并整体返回大列表...
函数计算并返回耗时: 0.29 秒
3. 外界 for 循环开始‘一个一个’消费大列表...
🚨 【内存结果】列表计算容器在内存中死死咬住了: 84.97 MB
--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ---
--- [ 方案A:传统列表模型 ] 开始运行 ---
1. 正在内存中初始化 1000 万条原始订单数据...
原始数据初始化耗时: 0.21 秒
2. 开始执行翻倍计算并整体返回大列表...
函数计算并返回耗时: 0.36 秒
3. 外界 for 循环开始‘一个一个’消费大列表...
🚨 【内存结果】列表计算容器在内存中死死咬住了: 84.97 MB
--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ---
--- [ 方案A:传统列表模型 ] 开始运行 ---
1. 正在内存中初始化 1000 万条原始订单数据...
原始数据初始化耗时: 0.14 秒
2. 开始执行翻倍计算并整体返回大列表...
函数计算并返回耗时: 0.33 秒
3. 外界 for 循环开始‘一个一个’消费大列表...
🚨 【内存结果】列表计算容器在内存中死死咬住了: 84.97 MB
--- 方案A 运行结束,此时如果你不关闭程序,这 80MB+ 内存会永远被它霸占 ---
方案 B:有生成器
import sys
import time
print("--- [ 方案B:yield 生成器模型 ] 开始运行 ---")
# 1. 模拟 1000 万条原始订单积分数据
print("1. 正在内存中初始化 1000 万条原始订单数据...")
start_time = time.time()
point_data = list(range(10000000)) # 1000万数据
print(f" 原始数据初始化耗时: {time.time() - start_time:.2f} 秒")
# 2. 带有 yield 的生成器计算函数
def double_points_generator(points_list):
for p in points_list:
yield p * 2 # 🟢 绝不囤货!配合外界的 __next__() 要一个才现场算一个
# 3. 触发业务调用
print("\n2. 开始调用生成器函数...")
calc_start = time.time()
# 💥 核心震撼点:这一行执行完只需要 0.00001 秒!
# 因为它根本没有去算数字,只是在内存里捏了一个 200 字节的“冬眠仓外壳”!
gen_obj = double_points_generator(point_data)
print(f" 生成器对象创建耗时: {time.time() - calc_start:.6f} 秒")
# 4. 模拟外界一个一个消费(套入你的 __next__() 自动化流水线)
print("\n3. 外界 for 循环开始按 next(),触发 yield 现场现产现销...")
for new_point in gen_obj:
pass # 模拟外层要一个,内部 yield 醒来算一个,用完单条数据内存立刻冲走释放
# 5. 打印生成器冬眠仓在内存中占用的空间
gen_memory = sys.getsizeof(gen_obj)
print(f"\n✨ 【内存结果】生成器计算容器(冬眠仓自身)在内存中仅仅占用了: {gen_memory} 字节")
print("--- 方案B 运行结束,用时极短且没有产生任何中间临时大列表 ---")
import sys
import time
print("--- [ 方案B:yield 生成器模型 ] 开始运行 ---")
# 1. 模拟 1000 万条原始订单积分数据
print("1. 正在内存中初始化 1000 万条原始订单数据...")
start_time = time.time()
point_data = list(range(10000000)) # 1000万数据
print(f" 原始数据初始化耗时: {time.time() - start_time:.2f} 秒")
# 2. 带有 yield 的生成器计算函数
def double_points_generator(points_list):
for p in points_list:
yield p * 2 # 🟢 绝不囤货!配合外界的 __next__() 要一个才现场算一个
# 3. 触发业务调用
print("\n2. 开始调用生成器函数...")
calc_start = time.time()
# 💥 核心震撼点:这一行执行完只需要 0.00001 秒!
# 因为它根本没有去算数字,只是在内存里捏了一个 200 字节的“冬眠仓外壳”!
gen_obj = double_points_generator(point_data)
print(f" 生成器对象创建耗时: {time.time() - calc_start:.6f} 秒")
# 4. 模拟外界一个一个消费(套入你的 __next__() 自动化流水线)
print("\n3. 外界 for 循环开始按 next(),触发 yield 现场现产现销...")
for new_point in gen_obj:
pass # 模拟外层要一个,内部 yield 醒来算一个,用完单条数据内存立刻冲走释放
# 5. 打印生成器冬眠仓在内存中占用的空间
gen_memory = sys.getsizeof(gen_obj)
print(f"\n✨ 【内存结果】生成器计算容器(冬眠仓自身)在内存中仅仅占用了: {gen_memory} 字节")
print("--- 方案B 运行结束,用时极短且没有产生任何中间临时大列表 ---")
运行结果
--- [ 方案B:yield 生成器模型 ] 开始运行 ---
1. 正在内存中初始化 1000 万条原始订单数据...
原始数据初始化耗时: 0.09 秒
2. 开始调用生成器函数...
生成器对象创建耗时: 0.000003 秒
3. 外界 for 循环开始按 next(),触发 yield 现场现产现销...
✨ 【内存结果】生成器计算容器(冬眠仓自身)在内存中仅仅占用了: 200 字节
--- 方案B 运行结束,用时极短且没有产生任何中间临时大列表 ---
两者使用内存相差很大。