zl程序教程

您现在的位置是:首页 >  Python

当前栏目

Python 中的迭代器

2023-03-14 22:55:29 时间

9.6 迭代器

自从第4章4.2.6节出现了“可迭代的”(Iterable)一词之后,就不断遇到具有此特征的对象,比如列表、字符串、字典等。判断一个对象是否是可迭代的,主要看它是否具有 __iter__() 方法。

>>> hasattr(list, '__iter__')
True
>>> hasattr(int, '__iter__')
False

此外,在第4章4.3.3节还首次遇到了“迭代器”这个名词,此后它也反复出现,例如:

>>> map(lambda x, y: x*y, [1, 2, 3], [4, 5, 6])
<map object at 0x7f86d1969c70>

函数 map() 所返回的对象就是迭代器(Iterator)。

显然,“迭代器”一定是“可迭代的”,但“可迭代的”对象,不一定是“迭代器”。

定义迭代器的一种最简单的方式是用内置函数 iter()

>>> lst = [1, 2, 3, 4]
>>> iter_lst = iter(lst)
>>> iter_lst
<list_iterator object at 0x7f86d194ccd0>

从返回结果中可以看出,iter_lst 引用的是迭代器对象。那么,iter_lstlst 有什么异同?

>>> hasattr(lst, '__iter__')
True
>>> hasattr(iter_lst, '__iter__')
True

首先,它们都有 __iter__() 方法,即它们都是可迭代的。但差异也是明显的:

>>> hasattr(lst, '__next__')
False
>>> hasattr(iter_lst, '__next__')
True

有无 __next__() 方法是 iter_lstlst 两个对象的区别,迭代器必须有 __next__() 方法,它的作用是什么?

>>> iter_lst.__next__()
1
>>> iter_lst.__next__()
2
>>> iter_lst.__next__()
3
>>> iter_lst.__next__()
4
>>> iter_lst.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

每执行一次 __next__() 方法,就将 iter_lst 中的一个成员呈现出来,也就是将该成员读入到内存——这是迭代器的特点,也是与列表、字典等可迭代对象的不同之处。迭代器 iter_lst 比列表 lst 节省内存。对于迭代器对象,内存中虽然已经有了它,但对象的成员没有占用内存空间。而列表一经创建之后,其所有成员已经被读入了内存。

要想使用迭代器的成员,必须执行迭代器的 __next__() 方法,且需要多少个成员就执行多少次(按照从左向右的方向排序),当读取到最后一个成员之后,到了迭代器结束位置,再执行 __next__() 方法,就抛出 StopIteration 异常。

一个一个地执行 __next__() 方法太麻烦,替代方法就是使用循环语句。如接续上述操作(这句话很重要,必须是接续上面的操作,才能有下面操作的效果),继续执行:

>>> for i in iter_lst: print(i)
...

没有任何打印结果——如果打印出了 iter_lst 的成员,说明你一定搞错了或者没有按照前面的要求做。

为什么会没有任何结果?甚至连异常信息都没有。

这又是 iter_lstlst 的另一项区别。在迭代器中,有一个“指针”(注意,这里加了引号),它指到哪个成员,在执行 __next__() 方法时就将该成员读入内存,“指针”随后指向下一个成员。当最后一个成员被读入内存后,“指针”也移动到了迭代器的最后。但是,这个“指针”不会自动“掉头”回到最开始的位置,犹如“过河的卒子”。

再来看前面的操作,在执行若干次 iter_lst.__next__() 后,“指针”已经移动到了 iter_lst 最后。若用 for 循环再次试图从 iter_lst 读取成员,但“指针”不走回头路,所以什么也读不到。

另外,没有抛出异常,其原因在于 for 循环会自动捕获 StopIteration 异常信息,并进行处理——后面会看到这个效果。

>>> iter_lst = iter(lst)         # (1)
>>> for i in iter_lst: print(i)  # (2)
...
1
2
3
4
>>> for i in iter_lst: print(i)  # (3)
...

注释(1)重新生成一个迭代器,然后用注释(2)的 for 循环读取其成员。如果再次循环(如注释(3)所示),也没有读到内容,原因同前。

列表 lst 则不然,可以反复多次使用循环语句读取其成员,每次都“不走空”。

>>> for i in lst: print(i, end=",")
...
1,2,3,4,>>>
>>> for i in lst: print(i, end=",")
...
1,2,3,4,>>>

使用迭代器的 __next__() 方法能够将迭代器成员读入内存,在 Python 中还有一个内置函数也实现此功能,即 next() 函数。以下是接续注释(3)操作:

>>> next(iter_lst)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

此处抛出异常的原因与执行 iter_lst.__next__() 抛出异常一样。

>>> iter_lst = iter(lst)
>>> next(iter_lst)
1
>>> next(iter_lst)
2
>>> iter_lst.__next__()
3

内置函数 next() 就是迭代器的 __next__() 方法的函数实现,如同内置函数 len() 实现了 __len__() 方法一样(参阅9.3节)。

从第6章6.3节学习了 for 循环之后,它就经常出现在程序中,现在要基于对迭代器的理解,从更深层次研究 for 循环。

>>> lst
[1, 2, 3, 4]
>>> import dis
>>> dis.dis("for i in lst: pass")
  1           0 LOAD_NAME                0 (lst)
              2 GET_ITER
        >>    4 FOR_ITER                 4 (to 10)
              6 STORE_NAME               1 (i)
              8 JUMP_ABSOLUTE            4
        >>   10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

此处使用的 dis 模块是 Python 标准库一员,其作用是将 Python 代码反汇编成字节码,字节码类似汇编指令,一个 Python 语句会对应若干字节码指令,虚拟机一条一条执行字节码指令,从而完成程序执行。

通过 dis.dis("for i in lst: pass") 所得到的就是执行 for 循环的字节码。主要看其中的两处:

  • GET_ITER ,其作用等同于 iter(lst)
  • FOR_ITER ,其相当于使用了 next() ,依次获取每个成员。

这说明 for 循环会先将被循环对象转化为迭代器,然后调用 __next__() 方法(即使用 next() 函数)依次获取每个成员。

前面操作中使用的迭代器是用 iter() 函数生成,注意该函数的参数必须是可迭代对象,或者说这个函数只能将可迭代对象转化为迭代器。“任何对象都可以自定义”,这是我们从第8章以来已经逐步确立的观念。那么如何自定义一个迭代器?

#coding:utf-8
'''
filename: iterator.py
'''
class MyRange:
    def __init__(self, n):
        self.i = 1
        self.n = n
    
    def __iter__(self):  
        return self

    def __next__(self): 
        if self.i <= self.n:
            i = self.i
            self.i += 1
            return i
        else:
            raise StopIteration()

if __name__ == "__main__":
    print(f"range(7): {list(range(7))}")
    print(f"MyRange(7): {[i for i in MyRange(7)]}")

执行结果:

% python iterator.py
range(7): [0, 1, 2, 3, 4, 5, 6]
MyRange(7): [1, 2, 3, 4, 5, 6, 7]

此处定义的类 MyRange 的实例对象有点类似 range() 函数返回的对象,但二者也有差别,通过比较执行结果很容易看出来。造成此区别的操作之一是在类 MyRange 的初始化方法中以 self.i = 1 确定以整数 1 作为计数起点,而不是 0

另外,在 __next__() 方法中以 self.i <= self.n 作为判断条件(注意等号),从而将实例化参数值也包含在了迭代器返回值范围。

再观察类 MyRange 内的方法,__iter__()__next__() 是迭代器的标志,在类中定义了这两个方法,就得到了能生成迭代器的类。

在第7章7.1.2节曾经写过斐波那契数列函数。这里再次编写一个实现斐波那契数的程序,只不过是用迭代器实现。

参考代码如下:

#coding: utf-8
'''
filename: fibsiterator.py
'''
class Fibs:
    def __init__(self, max):
        self.max = max
        self.a = 0
        self.b = 1

    def __iter__(self):
        return self

    def __next__(self):
        fib = self.a
        if fib > self.max:
            raise StopIteration
        self.a, self.b = self.b, self.a + self.b
        return fib

if __name__ == "__main__":
    fibs = Fibs(100000)  # (4)
    lst = [ fibs.__next__() for i in range(10)]
    print(lst)

运行结果如下:

% python fibsiterator.py 
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

注释(4)利用定义的类 Fibs 创建的斐波那契数列对象,包含了很多项(读者还可以设置更大的参数)。但是,因为迭代器的特点,那些斐波那契数在执行后面的列表解析之前没有一个被读入内存。只有当执行了后面的列表解析,才有指定数量的数字被读入内存,并组合成了一个列表。

在 Python 标准库中,还有一个与迭代器密切相关的模块 itertools ,在此也简要给予介绍。

>>> import itertools
>>> counter = itertools.count(start = 7)

变量 counter 所引用的对象是从整数 7 开始,步长为 1 的迭代器对象,即通过它可以得到从 7 开始的整数。

>>> next(counter)
7
>>> next(counter)
8
>>> next(counter)
9

其实,counter 是生成器(参阅9.7节),读者可以通过帮助文档查阅其详细说明。

除了能“线性迭代”之外,还能创建“循环迭代”的迭代器。

>>> colors = itertools.cycle(["red", "yellow", "green"])
>>> next(colors)
'red'
>>> next(colors)
'yellow'
>>> next(colors)
'green'
>>> next(colors)
'red'
>>> next(colors)
'yellow'

现在得到的 colors 迭代器就能实现“循环迭代”。

自学建议 随着掌握的知识越来越多,可能有一种跃跃欲试的感觉,急切地盼望着开发一个“改变世界”的程序。 心情可以理解,行动还要谨慎。如果有了一个创意,特别建议先用自己所熟悉的自然语言写出来,并用严格的逻辑描述解决问题的过程。写完之后,还要反复斟酌,检查自己的逻辑是否有问题。这是编程的基础。