• 【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=',')
    
    • 1
    • 2
    0,1,2,3,4,5,6,7,8,9,
    
    • 1

    2.2 可迭代对象

    什么样的对象是可迭代对象,字符串、列表、元组、字典、集合都是可迭代对象,可以参考博主之前写过的一篇文章【Python零基础入门笔记 | 06】字符串、列表、元组原来是一伙的?快看他们祖宗:序列Sequence,可迭代对象有什么特性:

    • 他们都有__iter__方法,该方法的功能就是用于创建迭代器
    # 字符串
    s = "hello python"
    dir(s)
    
    • 1
    • 2
    • 3
    ['__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']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    可以看到在dir的输出中,有__iter__方法。

    # 列表
    l = ['R', 'o', 'b', 'o', 't', 'F', 'e', 't', 'u', 'r', 'e']
    dir(l)
    
    • 1
    • 2
    • 3
    ['__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']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    在列表l的dir输出中也发现了__iter__方法

    # 用列表生成集合
    s = set(l)
    print(type(s), s)
    dir(s)
    
    • 1
    • 2
    • 3
    • 4
    <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']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    从集合的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
    • 8
    <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__']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    可以看到__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
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    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: 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    列表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
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    <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: 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    为什么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))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    224 56
    
    • 1

    迭代器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))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    list create cost:0.29482173919677734
    list traversal cost:0.05396842956542969
    iterator create cost:0.0
    iterator traversal cost:0.07095742225646973
    8697464 32
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到创建列表耗时0.29s,迭代器0.0秒,列表遍历时间0.05396秒,迭代器遍历时间0.0709秒,比列表稍慢,列表占用空间8.29MB,迭代器占用空间32Bytes

    2.4 常见的迭代器函数

    • enumerate

    基于一个可迭代对象生成一个枚举对象,它是一个索引序列,例如将列表[9, 7, 45]添加索引后成这样[(0, 9), (1, 7), (2, 45)]。

    enumerate??
    
    • 1
    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: 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    x = [9, 7, 45]
    x2 = enumerate(x)
    print(x2, list(x2))
    
    • 1
    • 2
    • 3
    <enumerate object at 0x000001933270B168> [(0, 9), (1, 7), (2, 45)]
    
    • 1
    • zip

    压缩一个或多个可迭代对象中的对应元素为新的元组元素,然后再由这些元组元素构成新的列表。

    zip??
    
    • 1
    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:  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    x1 = [9, 7, 45]
    x2 = zip(x1, range(len(x1)))
    
    print(x2, type(x2), list(x2))
    
    • 1
    • 2
    • 3
    • 4
    <zip object at 0x000001933466B608> <class 'zip'> [(9, 0), (7, 1), (45, 2)]
    
    • 1
    • reversed

    反转一个可迭代序列,返回迭代器

    reversed??
    
    • 1
    Init signature: reversed(sequence, /)
    Docstring:      Return a reverse iterator over the values of the given sequence.
    Type:           type
    Subclasses:     
    
    • 1
    • 2
    • 3
    • 4
    import numpy as np
    
    x1 = [9, 7, 45]
    x2 = reversed(x1)
    
    print(x2, type(x2), list(x2))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    <list_reverseiterator object at 0x0000019334760630> <class 'list_reverseiterator'> [45, 7, 9]
    
    • 1

    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
    
    • 1
    • 2
    • 3
    <generator object <genexpr> at 0x00000193363C09A8>
    
    • 1

    可以看到输出表明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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    <generator object fn at 0x0000019336686318>
    访问第1个元素
    第0次返回前
    0
    访问第2个元素
    第0次返回后
    第1次返回前
    1
    访问第3个元素
    第1次返回后
    第2次返回前
    2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    从打印日志中可以看到,在返回值0后,没有继续执行后面的打印【第0次返回后】,而是停留在yield关键字位置,下一次访问从yield关键字继续往后,才打印输出了【第0次返回后】。

    2.2 生成器总结

    • 生成器本质上是一个迭代器,有__iter__方法和__next__方法
    • 生成器有2种定义方式:元组推导式和带有yield关键字的函数,对于复杂的数据加载,常使用带有yield关键字的函数
    • 生成器函数被访问1次后,会挂起在yield位置,对生成器函数的第2次以上的调用,会直接跳转到yield挂起的位置执行,而不会重新从函数的入口执行

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

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

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

    《Python零基础快速入门系列》快速导航:

  • 相关阅读:
    who命令
    angular测试API
    熟练利用切片操作
    AD9371 官方例程HDL详解之JESD204B TX侧时钟生成 (三)
    .NET DataGridView数据绑定说明
    JDBC入门、数据库连接方式
    股票的涨跌答案
    Delegate介绍
    Android ViewBinding和DataBinding功能作用区别
    搭建一个Vue项目
  • 原文地址:https://blog.csdn.net/RobotFutures/article/details/125476929