这是机器未来的第23篇文章
原文首发地址:https://blog.csdn.net/RobotFutures/article/details/125454677
深度学习的数据集动辄几十上百G,面对海量数据,如何进行加载呢,本篇文章来聊一聊迭代器和生成器。
使用for循环遍历取值的过程
for i in range(10):
print(i, end=',')
0,1,2,3,4,5,6,7,8,9,
什么样的对象是可迭代对象,字符串、列表、元组、字典、集合都是可迭代对象,可以参考博主之前写过的一篇文章【Python零基础入门笔记 | 06】字符串、列表、元组原来是一伙的?快看他们祖宗:序列Sequence,可迭代对象有什么特性:
# 字符串
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方法的输出可以看出来。
什么是迭代器呢,简单理解是可迭代对象的代理
那么怎么获取迭代器的值呢?通过__next__()方法访问迭代器中的元素。
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:\Users\ZHOUSH~1\AppData\Local\Temp/ipykernel_22120/3590542549.py in
9 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:\Users\ZHOUSH~1\AppData\Local\Temp/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
基于一个可迭代对象生成一个枚举对象,它是一个索引序列,例如将列表[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??
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??
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))
[45, 7, 9]
有2种方法:元组推导式和含有yield关键字的函数
X1 = range(15)
X = (it for it in X1)
X
at 0x00000193363C09A8>
可以看到输出表明X是一个生成器。
包含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次返回后】。
生成器被广泛应用于深度学习和机器学习的数据加载,深度学习动辄上百G的数据集,全部加载到内存中,内存就崩溃了,基于生成器的特性,访问时创建元素,访问后销毁,生成器有效地避免了创建迭代器对象所占用的大量内存空间,大大降低了对硬件资源的占用,炼丹就可以很愉快的玩耍了。
《Python零基础快速入门系列》快速导航: