python进阶二-迭代器和生成器

简介

个人认为,迭代器和生成器是python中的比较核心的一个知识点,它几乎在python代码中的方方面面,例如常用的range()、一些内置函数,例如map返回的都是迭代器对象。

迭代器的官方说法

用来表示一连串数据流的对象。重复调用迭代器的 next() 方法(或将其传给内置函数 next())将逐个返回流中的项。当没有数据可用时则将引发 StopIteration 异常。到这时迭代器对象中的数据项已耗尽,继续调用其 next() 方法只会再次引发 StopIteration 异常。迭代器必须具有 iter() 方法用来返回该迭代器对象自身,因此迭代器必定也是可迭代对象,可被用于其他可迭代对象适用的大部分场合。一个显著的例外是那些会多次重复访问迭代项的代码。容器对象(例如 list)在你每次向其传入 iter() 函数或是在 for 循环中使用它时都会产生一个新的迭代器。如果在此情况下你尝试用迭代器则会返回在之前迭代过程中被耗尽的同一迭代器对象,使其看起来就像是一个空容器。

1.手动遍历迭代器

假设存在如下的迭代器,主要功能就是批量给0-9的数字进行平方

data = map(lambda n: n * n, range(10))

通常情况下,我们会使用for循环去遍历这个迭代器,那么我们如何去手动迭代呢,python提供了一个next方法,方便我们去获取迭代器的下一个元素,如下:

print(next(data))
print(next(data))
print(next(data))
print(next(data))
print(next(data))
print(next(data))
print(next(data))
print(next(data))
print(next(data))
print(next(data))

print(next(data))

0
1
4
9
16
25
36
49
64
81
Traceback (most recent call last):
  File "c:\Users\ts\Desktop\2022.7\2022.8.10\test.py", line 151, in <module>
    print(next(data))
StopIteration

我们进行了11次获取, 当第11次时出现了一个StopIteration异常, 代表元素全部获取了,停止迭代,那么由此我们可以大概知道for循环应该是帮我们处理了最后一次的异常,for循环简单实现如下:

def fun(data):
    try:
        return next(data)
    except StopIteration:
        return 

除此之外,对于生成器来说,也可以使用send获取元素的操作,这个下面再说。

2.创建迭代器对象

如果你自定义创建了一个容器对象,并且希望它能够被迭代,就需要实现__iter__方法并且需要返回一个实现了__next_方法的迭代器对象。

class Test(object):
    def __init__(self) -> None:
        self.list_data = []

    def __iter__(self):
        return iter(self.list_data)

    def __repr__(self):
        return f"list_data:{self.list_data}"

    def append(self, value):
        self.list_data.append(value)


t = Test()
t.append("tom")
t.append("mike")
print(t)
for item in t:
    print(item)

iter()将一个对象转为可迭代对象,相当于调用了__iter__方法,

3.使用生成器

生成器是特殊的迭代器,迭代器不一定是生成器,与迭代器类似,生成器也可以进行迭代,但内部并没有实现__iter____next__方法,取而代之的是yieldyield表示暂停当前程序的运行状态并在下次函数调用时继续从yield这里开始,例如下方简单实现了一个range()方法:

def range(n):
    x = 0
    while x < n:
        yield x
        x += 1


print(range(10))
for i in range(10):
    print(i)

<generator object range at 0x0000014AA45C6260>
0
1
2
3
4
5
6
7
8
9

除此之外,还有一个生成器表达式,只需要在需要转为生成器时添加(),,如下:

data = (i for i in range(10))

print(data)

生成器除了可以使用next方法,也可以有send方法,也是获取值,但是可以修改yield的返回值,如下:

def range(n):
    x = 0
    while x < n:
        ret = yield x
        print(f"ret:{ret}")
        x += 1


r = range(10)
print(next(r))
print(r.send(2))
print(r.send(5))

0
ret:2
1
ret:5
2
<generator object <genexpr> at 0x000001D585527300>

4.实现迭代器协议

当想要将自己定义的类或者对象变为迭代器,或者说是要实现迭代器协议,和上述类似,我们只需要实现__iter__方法并且返回的对象实现了__next__方法并且可以使用__next__触发stopIteration,使用这种方式需要我么自己去处理next方法对应的值以及需要我们自己去处理stopIteration异常较为繁琐,下面是一个简单的示例

5.反向迭代

顾名思义,就是将迭代器进行倒序迭代,这个让我们想到了排序中的reversed,它可以将序列进行反转(倒序),如下:

data = [1, 3, 12, 2, 19]
for i in reversed(data):
    print(i)

data = (i for i in range(10))
for i in reversed(data):
    print(i)
19
2
12
3
1
TypeError: 'generator' object is not reversible

由结果可知,reversed对于元素个数确定的情况可以进行反转,对于生成器,准确来说应该是迭代器这种无法直到元素个数的情况,默认是无法进行反转的,此时就需要将其转换为列表,就会消耗较大的内存。
当然我们可以通过对自定义迭代器中实现__reversed__方法来实现反转,最经典的就是range()方法,它虽然也是迭代器,但是它可以反转,它的内部实现如下:

class range(Sequence[int]):
    @property
    def start(self) -> int: ...
    @property
    def stop(self) -> int: ...
    @property
    def step(self) -> int: ...
    @overload
    def __init__(self, __stop: SupportsIndex) -> None: ...
    @overload
    def __init__(self, __start: SupportsIndex, __stop: SupportsIndex, __step: SupportsIndex = ...) -> None: ...
    def count(self, __value: int) -> int: ...
    def index(self, __value: int) -> int: ...  # type: ignore[override]
    def __len__(self) -> int: ...
    def __contains__(self, __o: object) -> bool: ...
    def __iter__(self) -> Iterator[int]: ...
    @overload
    def __getitem__(self, __i: SupportsIndex) -> int: ...
    @overload
    def __getitem__(self, __s: slice) -> range: ...
    def __reversed__(self) -> Iterator[int]: ...

可以看到里面实现了__reversed__方法返回一个迭代器,下面我们自定义一个对象并实现反转功能

class Xrange(object):
    def __init__(self, num) -> None:
        self.num = num

    def __iter__(self):
        n = 0
        while n < self.num:
            yield n
            n += 1

    def __reversed__(self):
        n = self.num - 1
        while n > -1:
            yield n
            n -= 1


print(list(Xrange(10)))
print(list(reversed(Xrange(10))))

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

由结果可以看出,即使不确定序列元素也可以实现反转,只需要实现__reversed__方法并返回一个迭代器对象即可。

6.生成器函数调用的对象或者属性可以被外部访问

对象或属性可以被外部访问可以使用return返回值,也可以使用类的属性对外暴露,由于生成器没有return,因此选择使用类的方式进行调用,具体实现如下,和上述的反转中的示例类似:

class Test(object):
    def __init__(self) -> None:
        self.data = []

    def __iter__(self):
        for item in self.data:
            yield item

    def append(self, val):
        self.data.append(val)

    def clear(self):
        self.data.clear()


t = Test()
t.append("tom")
t.append("mike")
print(t.data)
t.clear()
print(t.data)
['tom', 'mike']
[]

由此可见,生成器操作的对象,可以被类实例对象访问

  • 需要注意的是默认返回的生成器对象由于没有实现__next__方法,所以是当使用next()方法时会出现无法迭代的情况,需要使用iter将其转换为可迭代对象才可以使用next()方法调用.

7.迭代器切片

我们知道字符串、列表等可以进行切片操作,列表属于可迭代对象,那么迭代器可以切片吗?
使用itertool.islice即可实现迭代器切片。

from itertools import islice
from typing import Iterator

data = map(lambda n: n * n, range(10))
print(type(data))
print(isinstance(data, Iterator))

ret = islice(data, 2, 6, 2)
print(isinstance(ret, Iterator))
print(list(ret))
print(list(data))
<class 'map'>
True
True
[4, 16]
[36, 49, 64, 81]

由此可见,itertools可以进行切片并且返回的是一个迭代器。

  • isice原理:遍历迭代器中的元素直到切片的stop位置停止,因此原数据的切片前的数据将不再显示。
  • 需要注意的是itertools.islice操作后会影响到原数据,如果希望在切片后访问原数据,建议先将data保存一下或者深拷贝一下。

8.跳过可迭代对象的开始部分内容

有些情况我们可能需要舍弃部分开始内容,如果我们直到我们需要的详细的开始结束,我们可以直接使用itertools.islice进行切片操作,当我们只是根据一些条件去移除某些内容时,可以使用itertools.dropwhile(),它会舍弃前面为true的对象,返回第一个false及后面的对象。

from itertools import dropwhile


d = (i for i in range(10))

data = dropwhile(lambda n: n < 5, d)
print(list(data))
[5, 6, 7, 8, 9]

由结果可知,前面小于5的为true都没有输出,大于5的则会输出。

  • 需要注意的是,dropwhile只会过滤开始的元素,无法过滤中间的元素,它一般适用于过滤某些文件的文档注释啥的。

9.排列组合的迭代

相当于是去子序列,不过可能会存在序列中元素是否可以重复、元素是否是有序还是无序的。有序指的是抽取的子序列元素是否与原有序列中的元素顺序相一致。假设存在如下序列

item = [1, 2, 3, 4]

我们需要对序列进行去子序列操作,因此我们可以使用itertools.combinations抽取子序列,不过此方法抽取的子序列是有序、不重复的。

from itertools import combinations


data = combinations(item, 3)
print(list(data))
[(1, 2, 3), (1, 2, 4), (1, 3, 4), (2, 3, 4)]

combinations类似的还有一个combinations_with_replacement,这个抽取子序列时是可重复,有序的.

from itertools import combinations_with_replacement


data = combinations_with_replacement(item, 3)
print(list(data))
[(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 1, 4), (1, 2, 2), (1, 2, 3), (1, 2, 4), (1, 3, 3), (1, 3, 4), (1, 4, 4), (2, 2, 2), (2, 2, 3), (2, 2, 4), (2, 3, 3), (2, 3, 4), (2, 4, 4), (3, 3, 3), (3, 3, 4), (3, 4, 4), (4, 4, 4)]

除此之外,还有一个抽取子序列的方法:permutations,它抽取的子序列是不可重复、无序的,与combinations_with_replacement结果相反.

from itertools import permutations


data = permutations(item, 3)
print(list(data))
[(1, 2, 3), (1, 2, 4), (1, 3, 2), (1, 3, 4), (1, 4, 2), (1, 4, 3), (2, 1, 3), (2, 1, 4), (2, 3, 1), (2, 3, 4), (2, 4, 1), (2, 4, 3), (3, 1, 2), (3, 1, 4), (3, 2, 1), (3, 2, 4), (3, 4, 1), (3, 4, 2), (4, 1, 2), (4, 1, 3), (4, 2, 1), (4, 2, 3), (4, 3, 1), (4, 3, 2)]

10.如何序列在索引值上进行迭代?

一般情况下,我们对某个序列进行迭代时会直接使用for i in data的方式,但是在某些情况下,我们可能不仅需要迭代的值,也需要知道迭代到哪里了,就是迭代元素的索引,我们可能会直接使用一个变量去存储索引值,这种方式显得过于老套,我们可以直接使用enumarte函数嵌套需要迭代的对象即可获得索引及元素,如下:

item = [1, 2, 3, 4]

for index, value in enumerate(item):
    print(index, value)
0 1
1 2
2 3
3 4

这个能够在获取元素的同时获取到索引,这个在遍历文件时,行号的获取很有帮助。
基于enumarte函数,我们就可以对一个序列中的元素进行统计,如下获取到对应元素的所有索引值:

from collections import defaultdict

item = [1, 2, 3, 4, 2, 1, 3]

d_data = defaultdict(list)

for index, value in enumerate(item):
    d_data[value].append(index)


print(d_data)
defaultdict(<class 'list'>, {1: [0, 5], 2: [1, 4], 3: [2, 6], 4: [3]})

注意事项

使用enumarte时,如果遍历的序列中的元素不是正常的单个元素,例如是元组之类的,在for循环遍历时则需要将index后面的data换成对应的元素格式,例如(x,y)

同时迭代多个序列

可能在一些情况下,我们需要同时迭代两个序列,依赖于两个序列的结果进行后续操作,此时,我们可以使用多次循环来实现,但效率较为低下,因此python提供了zip函数方便我们进行多序列的迭代,如下:

d1 = [1, 2, 3]
d2 = [4, 5, 6]

for item in zip(d1, d2):
    print(item)
(1, 4)
(2, 5)
(3, 6)

我们可以看到多序列元素是一一对应的关系,但zip是相对于最小集的,就是相当于水桶效应,最终的值以最小元素的序列为准。
因此,itertools提供给我们了一个最大集的方法zip_longest, 它匹配元素的最大集,并将无对应匹配值的设为默认值。

from itertools import zip_longest


l1 = [1, 2, 3]
l2 = [4, 5, 6, 7]
data = zip_longest(l1, l2, fillvalue=None)
print(list(data))

[(1, 4), (2, 5), (3, 6), (None, 7)]

12.不同序列元素的批量迭代

当我们需要对不同序列中的元素采取相同的操作时,我们可能会使用多重循环去遍历对应的序列,再进行相应的操作,这样导致的代码可读性比较一般,itertools.chains()可以很好的解决这个问题,它会依次迭代序列中的元素,它类似于将序列中的元素放在了一个序列中,方便处理,如下:

from itertools import chain

l1 = [1, 2, 3]
l2 = [4, 5, 6, 7]
data = chain(l1, l2)
print(list(data))
[1, 2, 3, 4, 5, 6, 7]

13.创建数据处理管道

当我们需要进行处理大数据或者大文件时并且不希望一次性消耗太多的内存,生成器是一个好办法,生成器不会一次性产生大量的数据,只会在迭代过程中逐渐的消耗数据,非常适合内存消耗少的情况。

14.展开嵌套的序列

假设存在如下序列,我们需要将其元素进行展开为一个序列

items = [1, 2, [3, 4, [5, 6], 7], 8]

我们可以使用递归迭代的方式,但需要我们存储相应的结果,我们也可以使用生成器就可以直接输出结果,且减少了内存。

def expand(sep):
    if isinstance(sep, Iterable):
        for item in sep:
            if isinstance(item, Iterable):
                for i in expand(item):
                    yield i
            else:
                yield item
    else:
        yield item


print(list(expand(items)))

上述使用了yield生成器的方式去输出值,但使用了两次循环,我们可以使用yield from代替for循环yield输出结果。

def expand(sep):
    if isinstance(sep, Iterable):
        for item in sep:
            if isinstance(item, Iterable):
                yield from expand(item)
            else:
                yield item
    else:
        yield item

15.顺序迭代合并后的可迭代对象

假设存在两个已经排序了的序列,需要将两个序列进行合并后进行迭代。
首先我们会想到的是将序列中元素先解压出来再排序再进行迭代,如下:

a = [1, 4, 7, 10]
b = [2, 5, 6, 11]

from itertools import chain

for item in sorted(chain(a, b)):
    print(item, end=",")
1,2,4,5,6,7,10,11,

确实,我们可以实现相应的效果,但是我们可以使用python内置的堆中的函数处理相关数据,如下

from heapq import merge

for item in merge(a, b):
    print(item, end=",")

我们可以看到,效果是一样的,需要注意的是heapq.merge只针对于已排序的序列进行处理,如果不是已排序的。结果将会出现意想不到的的情况。

16.迭代器代替while循环

在之前的项目中,对于读取文件,往往可能我会使用while循环去读取每一行再输出,再根据行是否存在来判断是否需要退出while循环,如下:

with open(file_path, mode="r", encoding="utf-8") as r_f:
    while True:
        line = r_f.readline()
        if not line:
            break
        print(line)

这里我们也可以使用迭代器代替while循环,使用iter()方法, iter方法接受一个有返回值的function和一个标记值,标记值代表当前面的方法与标记值相等时停止迭代。

with open(file_path, mode="r", encoding="utf-8") as r_f:
    for line in iter(lambda: r_f.readline(), ''):
        print(line)

建议以后在文件的读取中使用iter方法来进行文件内容的迭代

参考

python3-cookbook第四章

posted @ 2022-08-16 17:56  形同陌路love  阅读(84)  评论(0编辑  收藏  举报