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

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

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

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的数据集,全部加载到内存中,内存就崩溃了,基于生成器的特性,访问时创建元素,访问后销毁,生成器有效地避免了创建迭代器对象所占用的大量内存空间,大大降低了对硬件资源的占用,炼丹就可以很愉快的玩耍了。

    《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编程?
  • 来源:机器未来

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【Python零基础快速入门系列 | 13】面对海量数据,如何优雅地加载数据?请看迭代器与生成器

    发表评论