【Python零基础快速入门系列 | 13】面对海量数据,如何优雅地加载数据?请看迭代器与生成器

这是机器未来的第23篇文章

原文首发地址:https://blog.csdn.net/RobotFutures/article/details/125454677

在这里插入图片描述
《Python零基础快速入门系列》快速导航:

1. 概述

深度学习的数据集动辄几十上百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)
<class 'set'> {'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))
<class 'list'> [1, 2, 3, 4, 5, 6, 7] 1731750044552
<class 'list_iterator'> <list_iterator object at 0x000001933473A780> 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:\Users\ZHOUSH~1\AppData\Local\Temp/ipykernel_22120/3590542549.py in <module>
      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))


<class 'list_iterator'> <list_iterator object at 0x000001933473A7B8>
1
2
3



---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

C:\Users\ZHOUSH~1\AppData\Local\Temp/ipykernel_24084/1283156051.py in <module>
     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))
<enumerate object at 0x000001933270B168> [(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))
<zip object at 0x000001933466B608> <class 'zip'> [(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))
<list_reverseiterator object at 0x0000019334760630> <class 'list_reverseiterator'> [45, 7, 9]

2.5 迭代器总结

  • 迭代器是惰性可迭代对象,采用延时加载的方式创建一个序列,迭代器创建时它的元素并不会加载到内存
  • 迭代器是一个有序序列
  • 通过__next__或next方法访问迭代器;
  • 每次调用__next__或next方法仅能访问一个元素
  • 每次访问时创建元素,访问结束后销毁元素,省内存
  • 调用__next__或next方法访问元素从第一个开始到最后一个结束,依次访问
  • 访问到迭代器的末尾,抛出StopIteration异常;
  • 迭代器创建时速度非常快,调用时比元素存储在内存中的可迭代对象慢

3. 生成器

  • 按需产生结果,而不是立即产出结果
  • 生成器的底层是迭代器

3.1 定义生成器

有2种方法:元组推导式和含有yield关键字的函数

3.1.1 元组推导式生成

X1 = range(15)
X = (it for it in X1)
X
<generator object <genexpr> 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个元素
    
<generator object fn at 0x0000019336686318>
访问第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挂起的位置执行,而不会重新从函数的入口执行

3. 生成器与迭代器的区别

  • 生成器更多体现为带有yield关键字的函数,对生成器函数的调用会跳转到上次挂起的位置,而不是重新开始运行
  • 迭代器是一种包含next方法的对象
  • 生成器也是迭代器

生成器被广泛应用于深度学习和机器学习的数据加载,深度学习动辄上百G的数据集,全部加载到内存中,内存就崩溃了,基于生成器的特性,访问时创建元素,访问后销毁,生成器有效地避免了创建迭代器对象所占用的大量内存空间,大大降低了对硬件资源的占用,炼丹就可以很愉快的玩耍了。

  • 26
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 33
    评论
评论 33
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

机器未来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值