AsyncIO 从入门到放弃 #01

迭代器


by DavyCloud, 2022

目录

迭代器的接口

可迭代对象

先记住一个简单的结论:在 Python 中,凡是能跟在 for ... in ... 后面的都是可迭代对象。

既然这些不同类型的对象都能使用同样的方式来迭代,那么它们一定存在共同之处。

基于 Python 强大的内省能力,我们可以亲手找出这个共同的属性。

可迭代对象的接口

看名字 __iter__ 就是我们要找的特殊属性,但是为什么还有另外 2 个属性呢?

这是因为这里列举的可迭代对象都是容器类的数据结构,它们都有获取大小(__len__)和判断是否包含(__contains__)的方法。

除了这些容器类对象,Python 内置对象中还有其它可迭代对象,比如文件对象,StringIO 对象等。

「可迭代对象」唯一的共同接口:__iter__

__iter__ 方法,对应的调用方法就是内置函数 iter()

输出了各种 xxx_iterator 类型的对象,这就是我们要找的迭代器!

只有最新加入的 StringIO 的结果显得有点格格不入,后面会介绍为什么。

迭代器的接口

既然所有的可迭代对象唯一的共同之处__iter__ 方法,那么显然在迭代的时候,该方法是必然会被调用的。

则得到的迭代器对象,必然也有共同的地方,只需故技重施,找找它们之间的共同属性。

「迭代器」有两个接口:__iter____next__

有点出乎意料,迭代器的共同属性竟然有两个。

新出现的 __next__,从名字不难猜出它实现了迭代器的核心功能:返回下一个值

该特殊方法同样有一个内置的函数:next()

重复出现的 __iter__ 则让人有点疑惑,经常被人一笔带过,其实它的作用也至关重要!

迭代器的用法

当列表没有遍历完的时候,每次调用 next() 都返回了列表中的对象,当迭代到最后没有东西可以返回的时候,则抛出了 StopIteration 异常。

从名字不难看出,StopIteration 异常就是专门用来提示迭代器已经到头了,

这其实不是错误,而是通过异常向迭代器的使用者(例如 for 语句)发送一个信号:迭代停止了。迭代器的使用者有责任捕获并妥善处理。

至此我们已经知道了迭代的三个关键步骤:

迭代器的 __iter__ 方法作用在哪里呢❓

自定义迭代器

设计一个迭代器

迭代器的基本功能:

再添加一点额外的逻辑:

代码本身很简单。

正如前面的分析:

注意:

此时这个迭代器只能使用啰嗦的 while 循环来访问,还不能在 for 循环中使用:

因为 for 循环会首先调用 __iter__ 方法,而现在这个迭代器并没有定义该方法。

我们可以继续自定义一个可迭代对象的类,通过它来构建并返回自定义的迭代器:

上面的两个自定义的类看上去还是符合我们截至目前的认知的:迭代器作为可迭代对象的辅助,由 for 循环隐藏在背后使用。

但是明显可以看出,如果说 GoodActionIterator 还有那么一丝作用,后面的 GoodActions 类完全是多余的。

此时的矛盾点在于:

我们并不是想要一个额外的可迭代对象,只是不想使用它自带的迭代器而已。

解决办法其实也很简单,就让迭代器自食其力,自己实现 __iter__ 方法就好了。

既然此时自定义迭代器已经构建出来了,在 __iter__ 方法中只需要 return self 即可。

现在,直接把迭代器对象放在 for ... in 后面即可。

迭代器协议

在 Python 文档中明确指出了,迭代器必须同时实现 __next____iter__ 两个方法,这称之为「迭代器协议」。

根据这个协议,迭代器必须是可迭代的,换言之,「迭代器」是一种「可迭代对象」

缺少了 __iter__ 方法的迭代器是不完整的,不符合迭代器协议的要求。

所有迭代器的 __iter__ 方法都只要千篇一律的 return self 即可。

迭代器的意义

浅层的意义

按照这个思路,我们很容易形成这样的认知:

迭代器就是为了让数据结构能够快捷地遍历而定义的辅助对象。

如果只看常见的可迭代对象,这个结论确实不错。这些常见类型的迭代器都是默默地发挥作用。

但是别忘了,如果只是在 for 循环中使用,是无需用到迭代器的 __iter__ 方法的:

深层的意义

为什么说迭代器的 __iter__ 方法是点睛之笔!

现在有两种可迭代对象:

如果迭代器没有 __iter__ 方法,就不能直接用在 for 循环中:

此种情况下,只有可迭代对象在前台露脸。

而迭代器是在背后使用默认的方式“悄悄”构建的,没有存在感,并且生命周期是和循环操作本身绑定在一起的。

而一旦迭代器实现了 __iter__ 方法:

现在整个迭代过程只需要迭代器就够了!

迭代器不光是从后台走向了前台,而且直接让可迭代对象远离了循环!

现在,迭代器的构建是在明面上单独完成的,和当前循环操作解耦了。

于是乎:

于是,催生了迭代器的两个重要的应用场景。

数据管道

如果迭代器不可迭代:

「for 循环」<-「迭代器(隐藏)」<-「可迭代对象」

现在,迭代器可以任意的嵌套连接:

「for 循环」<-「迭代器」<-「迭代器」<- ... <-「迭代器」<-「迭代器」<-「可迭代对象」

数据生成器

数据流再长总归要有源头,也就是产生数据的地方。

这些数据一定要存储在某个数据结构(如字典列表等)中吗?

这个迭代器不但不需要存储数据,甚至连 StopIteration 都不用管。

它可以无穷尽地迭代下去,每次数据都是实时产生的,并不占用内存空间。

有经验的应该能看出来了,这不就是「生成器」嘛!

虽然该迭代器是名副其实的数据生成器,但是生成器(Generator) 在 Python 中特指包含 yield 的函数对象,这是下一章的主要内容。

附录:没有定义 __iter__ 的可迭代对象

并不是所有的可迭代对象都必须定义有 __iter__ 方法。

如果一个对象没有 __iter__ 方法,但是定义了 __getitem__ 方法,同样是可以迭代的。

因此,不能通过检查 __iter__ 方法来判断一个对象是否是可迭代的,而是应该直接使用 iter() 函数,如果不可迭代,则会抛出 TypeError 异常。

这里的做法更多的是为了兼容性考虑。

实际我们在编写自己的数据类型时,如果希望它是可迭代的,最好的办法还是实现 __iter__ 方法。