这是机器未来的第23篇文章
1. 概述原文首发地址:https://blog.csdn.net/RobotFutures/article/details/125454677
深度学习的数据集动辄几十上百G,面对海量数据,如何进行加载呢,本篇文章来聊一聊迭代器和生成器。
2. 迭代器 2.1 迭代的概念使用for循环遍历取值的过程
for i in range(10):
print(i, end=',')
0,1,2,3,4,5,6,7,8,9,2.2 可迭代对象
什么样的对象是可迭代对象,字符串、列表、元组、字典、集合都是可迭代对象,可以参考博主之前写过的一篇文章【Python零基础入门笔记 | 06】字符串、列表、元组原来是一伙的?快看他们祖宗:序列Sequence,可迭代对象有什么特性:
- 他们都有__iter__方法,该方法的功能就是用于创建迭代器
# 字符串 s = "hello python" dir(s)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
可以看到在dir的输出中,有__iter__方法。
# 列表 l = ['R', 'o', 'b', 'o', 't', 'F', 'e', 't', 'u', 'r', 'e'] dir(l)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__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']
在列表l的dir输出中也发现了__iter__方法
# 用列表生成集合 s = set(l) print(type(s), s) dir(s)
{'t', 'F', 'o', 'u', 'R', 'r', 'e', 'b'} ['__and__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__iand__', '__init__', '__init_subclass__', '__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']
从集合的dir输出中同样发现了__iter__方法。
__iter__的目的是为了生成迭代器,我们做一下验证:
l = [1, 2, 3, 4, 5, 6, 7] # 此处输出列表的类型和值 print(type(l), l, id(l)) # 调用__iter__()方法生成迭代器 i = l.__iter__() # 此处输出迭代器的类型和值 print(type(i), i, id(i))
[1, 2, 3, 4, 5, 6, 7] 1731750044552 1731751815040 ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__']
可以看到__iter__()方法基于l创建了一个迭代器,打印它的值时不显示具体值,而是显示一个迭代器对象,新的迭代器对象和原来的列表对象不是同一个对象,可以从id方法的输出可以看出来。
2.3 迭代器什么是迭代器呢,简单理解是可迭代对象的代理
那么怎么获取迭代器的值呢?通过__next__()方法访问迭代器中的元素。
- 每调用1次__next__()方法访问一个元素,且将这个元素从迭代器删除
- 从第一个元素开始访问,直至访问到最后一个元素,__next__()访问不到元素后,抛出StopIteration异常
l = [1, 2, 3]
# 调用__iter__()方法生成迭代器
i = l.__iter__()
print(i.__next__())
print(i.__next__())
print(i.__next__())
# 此处展示迭代器数据已经被取完,已为空
print(f"迭代器当前状态:{[x for x in i]}")
# 再次取数据,抛出异常
print(i.__next__())
1 2 3 迭代器当前状态:[] --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) C:UsersZHOUSH~1AppDataLocalTemp/ipykernel_22120/3590542549.py in9 print(f"迭代器当前状态:{[x for x in i]}") 10 # 再次取数据,抛出异常 ---> 11 print(i.__next__()) StopIteration:
列表l的元素为3个,可以看到前3次__next__()方法正常调用,使用列表推导式访问迭代器,发现已经为空了,第4次抛出了StopIteration异常
除了使用可迭代对象的__iter__方法创建迭代器之外,也可以使用python内置的iter函数创建迭代器,使用next访问迭代器。
l = [1, 2, 3] # 使用列表l作为可迭代对象创建迭代器 it = iter(l) print(type(it), it) # 访问第1个元素 print(next(it)) # 访问第2个元素 print(next(it)) # 访问第3个元素 print(next(it)) # 已经到了末尾,抛出StopIteration异常 print(next(it))
1 2 3 --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) C:UsersZHOUSH~1AppDataLocalTemp/ipykernel_24084/1283156051.py in 13 print(next(it)) 14 # 已经到了末尾,抛出StopIteration异常 ---> 15 print(next(it)) 16 StopIteration:
为什么for循环可以遍历列表、元组等可迭代对象吗?
for循环在循环开始之前,首先自动调用可迭代对象的__iter__方法创建一个迭代器,然后每一次循环自动调用__next__方法取出可迭代对象中的一个值。
迭代器的优点:
- 省内存
迭代器是惰性计算,采用延时创建的方式生成一个序列,它的元素不会存在内存中,仅在__next__被调用时才会创建(意味着仅创建单次__next__获取的数据),而且取走后直接扔掉。
import sys l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] # 使用列表l作为可迭代对象创建迭代器 it = iter(l) # 查看对象占用的内存 print(sys.getsizeof(l), sys.getsizeof(it))
224 56
迭代器it和列表l创建的对象,可以看到差距很大,而且迭代器对象占用空间的大小不会随着列表l的元素个数发生变化,列表l有10000个元素,迭代器it占用的空间也是56,非常节省空间。
迭代器在创建是速度非常快,调用时速度要比可迭代对象慢。
"""
创建100万个元素的列表
"""
import sys
from time import time
l = []
t1 = time()
for x in range(1000000):
l.append(x)
t2 = time()
print(f"list create cost:{t2-t1}")
t1 = time()
for item in l:
pass
# print(item, end=',')
t2 = time()
print(f"list traversal cost:{t2-t1}")
# 使用列表l作为可迭代对象创建迭代器
t1 = time()
it = iter(range(1000000))
t2 = time()
print(f"iterator create cost:{t2-t1}")
t1 = time()
for item in it:
pass
# print(item, end=',')
t2 = time()
print(f"iterator traversal cost:{t2-t1}")
# 查看对象占用的内存
print(sys.getsizeof(l), sys.getsizeof(it))
list create cost:0.29482173919677734 list traversal cost:0.05396842956542969 iterator create cost:0.0 iterator traversal cost:0.07095742225646973 8697464 32
可以看到创建列表耗时0.29s,迭代器0.0秒,列表遍历时间0.05396秒,迭代器遍历时间0.0709秒,比列表稍慢,列表占用空间8.29MB,迭代器占用空间32Bytes
2.4 常见的迭代器函数- enumerate
基于一个可迭代对象生成一个枚举对象,它是一个索引序列,例如将列表[9, 7, 45]添加索引后成这样[(0, 9), (1, 7), (2, 45)]。
enumerate??
Init signature: enumerate(iterable, start=0)
Docstring:
Return an enumerate object.
iterable
an object supporting iteration
The enumerate object yields pairs containing a count (from start, which
defaults to zero) and a value yielded by the iterable argument.
enumerate is useful for obtaining an indexed list:
(0, seq[0]), (1, seq[1]), (2, seq[2]), ...
Type: type
Subclasses:
x = [9, 7, 45] x2 = enumerate(x) print(x2, list(x2))
[(0, 9), (1, 7), (2, 45)]
- zip
压缩一个或多个可迭代对象中的对应元素为新的元组元素,然后再由这些元组元素构成新的列表。
zip??
Init signature: zip(self, /, *args, **kwargs) Docstring: zip(iter1 [,iter2 [...]]) --> zip object Return a zip object whose .__next__() method returns a tuple where the i-th element comes from the i-th iterable argument. The .__next__() method continues until the shortest iterable in the argument sequence is exhausted and then it raises StopIteration. Type: type Subclasses:
x1 = [9, 7, 45] x2 = zip(x1, range(len(x1))) print(x2, type(x2), list(x2))
[(9, 0), (7, 1), (45, 2)]
- reversed
反转一个可迭代序列,返回迭代器
reversed??
Init signature: reversed(sequence, /) Docstring: Return a reverse iterator over the values of the given sequence. Type: type Subclasses:
import numpy as np x1 = [9, 7, 45] x2 = reversed(x1) print(x2, type(x2), list(x2))
2.5 迭代器总结[45, 7, 9]
- 迭代器是惰性可迭代对象,采用延时加载的方式创建一个序列,迭代器创建时它的元素并不会加载到内存
- 迭代器是一个有序序列
- 通过__next__或next方法访问迭代器;
- 每次调用__next__或next方法仅能访问一个元素
- 每次访问时创建元素,访问结束后销毁元素,省内存
- 调用__next__或next方法访问元素从第一个开始到最后一个结束,依次访问
- 访问到迭代器的末尾,抛出StopIteration异常;
- 迭代器创建时速度非常快,调用时比元素存储在内存中的可迭代对象慢
- 按需产生结果,而不是立即产出结果
- 生成器的底层是迭代器
有2种方法:元组推导式和含有yield关键字的函数
3.1.1 元组推导式生成X1 = range(15) X = (it for it in X1) X
at 0x00000193363C09A8>
可以看到输出表明X是一个生成器。
2.1.2 yield关键字函数包含yield关键字的特殊函数,yield关键字同return一样,可以返回值,但是yield关键字有个特殊的地方,在于它在返回值后,会挂起当前的执行位置,下次运行时会从挂起的位置继续执行,而不会从头开始。
def fn(num):
for i in range(num):
print(f"第{i}次返回前")
yield(i)
print(f"第{i}次返回后")
g = fn(100)
print(g) # 查看g的类型
print(f"访问第1个元素")
print(next(g)) # 访问第1个元素
print(f"访问第2个元素")
print(next(g)) # 访问第2个元素
print(f"访问第3个元素")
print(next(g)) # 访问第3个元素
访问第1个元素 第0次返回前 0 访问第2个元素 第0次返回后 第1次返回前 1 访问第3个元素 第1次返回后 第2次返回前 2
从打印日志中可以看到,在返回值0后,没有继续执行后面的打印【第0次返回后】,而是停留在yield关键字位置,下一次访问从yield关键字继续往后,才打印输出了【第0次返回后】。
2.2 生成器总结- 生成器本质上是一个迭代器,有__iter__方法和__next__方法
- 生成器有2种定义方式:元组推导式和带有yield关键字的函数,对于复杂的数据加载,常使用带有yield关键字的函数
- 生成器函数被访问1次后,会挂起在yield位置,对生成器函数的第2次以上的调用,会直接跳转到yield挂起的位置执行,而不会重新从函数的入口执行
- 生成器更多体现为带有yield关键字的函数,对生成器函数的调用会跳转到上次挂起的位置,而不是重新开始运行
- 迭代器是一种包含next方法的对象
- 生成器也是迭代器
生成器被广泛应用于深度学习和机器学习的数据加载,深度学习动辄上百G的数据集,全部加载到内存中,内存就崩溃了,基于生成器的特性,访问时创建元素,访问后销毁,生成器有效地避免了创建迭代器对象所占用的大量内存空间,大大降低了对硬件资源的占用,炼丹就可以很愉快的玩耍了。
《Python零基础快速入门系列》快速导航:
- 【Python零基础入门笔记 | 01】 人工智能序章:开发环境搭建Anaconda+VsCode+JupyterNotebook(零基础启动)
- 【Python零基础入门笔记 | 02】一文快速掌握Python基础语法
- 【Python零基础入门笔记 | 03】AI数据容器底层核心之Python列表
- 【Python零基础入门笔记 | 04】为什么内存中最多只有一个“Love“?一文读懂Python内存存储机制
- 【Python零基础入门笔记 | 05】Python只读数据容器:列表List的兄弟,元组tuple
- 【Python零基础入门笔记 | 06】字符串、列表、元组原来是一伙的?快看序列Sequence
- 【Python零基础入门笔记 | 07】成双成对之Python数据容器字典
- 【Python零基础入门笔记 | 08】无序、不重复、元素只读,Python数据容器之集合
- 【Python零基础入门笔记 | 09】高级程序员绝世心法——模块化之函数封装
- 【Python零基础入门笔记 | 10】类的设计哲学:自然法则的具现
- 【Python零基础入门笔记 | 11】函数、类、模块和包如何构建四级模块化体系
- 【Python零基础入门笔记 | 12】程序员为什么自嘲面向Bug编程?



