Xudong's Blog

Python3中的Iterator与Iterable

Word count: 2.1kReading time: 8 min
2019/09/07 Share

今天教Cc学python3的时候说到了for in语法只可以作用于iterable上,她说这个单词一看就是写错了。很长时间没碰python这都搞错?心想这下出糗了,赶忙查了查,python里确实有iterable。凡是可作用于for循环的对象都是Iterable类型也是没错的。但是却发现,我对iterable和iterator的概念有些模糊。

定义

Iterator

An object representing a stream of data.

Python Glossary - Iterator

我们可以从Python官方的文档中了解到,Iterator - 迭代器 这个术语在python中的意义。
Iterator - 可以表示数据流的对象。
数据流 - 一组有顺序的(线性的)、有起点和终点的数据集合。

就像在管道中流淌的水流,数据流通常都是单向的。Python允许我们通过调用一个iterator对象上的__next__()方法,来访问数据流(iterator)中的下一个元素。另一种方法是使用内置函数next(),使用next(iterator_object)来获取iterator中的下一个元素。我们可以在同一个iterator上重复进行这样的操作,最终会按顺序获取数据流中的所有对象。

当iterator中的最后一个元素已经被访问之后,我们再使用next()方法,或者调用__next__(),解释器会抛出一个StopIteration异常,告诉我们此迭代器之前就已经迭代结束了,其中已经没有“下一个”元素了。

由此,我们可以简单地认为:如果一个对象拥有__next__()方法,我们就可以视它为iterator。

Iterable

An object capable of returning its members one at a time.

Python Glossary - Iterable

Iterable - 有能力一次性返回它的成员的对象。
这里需要解释这个“成员”是个带s的复数。
这个对象有一个方法来返回它容纳的一些元素,甚至全部元素。我们就可以称它为可迭代对象iterable。

怎么做到一次返回全部元素?这里不要产生理解偏差,不是指返回一个数组,其实是特指返回一个iterator。

我们学习过的所有线性数据类型,包括list, str, tuple,一些非线性数据类型,包括dictopen()函数返回的文件对象,都是可迭代对象。它们都有一个叫做__iter__()的方法,可以返回它们各自的迭代器。我们也可以使用内置函数iter(),传入一个可迭代对象,就可以返回一个它的迭代器对象。

另外,文档中明确说明了,任何定义了__iter__()__getitem__()的对象,都是iterable。但是要求__getitem__()方法实现的是一个线性的语义。这样是为了让其在for语法下与可迭代对象表现出同样的行为,为了让其生成的迭代器与其他迭代器同样是一个数据流。(如果__getitem__()实现的并不是线性的语义,解释器并不会报错,同样可以被for语法使用。但是语义错误同样是编程错误。)

可迭代对象一定是一个线性的数据类型。大部分线性的数据类型都是一个iterable。
我们可以简单地认为,只要实现了__iter__()__getitem__()方法的类,其对象都是iterable。

for…in

1
2
3
L = ['python', 'java', 'rust', 'c']
for item in L:
print(item)

所以我们可以明白for...in的语法到底为我们做了啥。
首先for item in L,会在L这个iterable上调用__iter__(),创建一个iterator对象,然后每轮循环都在在iterator上调用__next__()并把返回值赋给item,直到出现StopIteration异常,帮我们捕获后就会结束循环。

自己动手

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

Python推崇鸭子类型,你如果实现了__iter__()__getitem__()方法,(即把对象传入iter()可以得到一个Iterator)那你就是一个Iterable。同理,你如果实现了__next__()__iter__()就是一个Iterator。有时候,鸭子类型可以让我们很方便地免去继承来实现一些interface或者说是protocol。

自己的Iterator

Iterator Types

由文档中对于迭代器的类型的描述,我们得知,迭代器类必须实现一个__next__()方法用于返回下一个迭代元素,还有一个__iter__()方法返回自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyRange:
def __init__(self, low, high):
self.current = low
self.high = high

def __iter__(self):
return self

def __next__(self):
if self.current >= self.high:
raise StopIteration
else:
self.current += 1
return self.current - 1

for i in MyRange(4,9):
print(i)

上面的代码实现了我们自己的range(),MyRange类实例化后产生的对象就是一个iterator。我们可以使用for循环对这个iterator进行迭代,等等!for…in的迭代对象不是iterable吗?为什么我们写的iterator也可以被for语法遍历呢?

写代码让我们发现了python规定迭代器类必须实现__iter__()的理由,很多时候我只是需要一个迭代器,但事实上很多地方使用的是可迭代对象协议。可迭代对象需要实现的是可以把迭代器返回出来,那么我们的迭代器自己实现__iter__()并且把自己返回出来,其实逻辑上是可以接受的。

对iterator进行迭代,是符合直觉的,因为迭代器本来就是可以迭代的。
对iterable进行迭代,也是符合直觉的,“可迭代的对象”本来也是可迭代的,每次迭代的时候在内部初始化一个iterator进行迭代。

iterator一定是一个iterable。

自己的Iterable

所以其实iterable就没有必要实现了,上面的MyRange本身就是一个iterable。

但你还记得上面定义吗,一个实现了线性语义的__getitem__()方法的对象也是一个iterable。另外还要求了__getitem__()方法的接收的参数是从0开始的整数。

object must be a collection object which supports the iteration protocol (the __iter__() method), or it must support the sequence protocol (the __getitem__() method with integer arguments starting at 0).

1
2
3
4
5
6
7
8
9
10
11
class MyRange:
def __init__(self, low, high):
self.low = low
self.high = high
self._length = high - low

def __getitem__(self, index):
if index >= self._length or index < 0:
raise IndexError("index out of range")
else:
return index + self.low

这样实现的MyRange,是一个单纯的iterable。内置函数iter(),可以神奇地把它的对象转变成一个iterator。因为可以线性遍历元素的容器,在逻辑结构上是满足迭代器的结构的。

判断可迭代对象

for语法能不能作用在一个对象上?(这个对象是不是一个iterable或者iterator)
一个对象能不能作为next()函数的第一个参数?(这个对象是不是一个iterator)

在动态类型的python中,有时必要的类型判断可以避免很多问题。

在网上搜索,很多文章给出的判断方法是:

1
2
3
>>> from collections import Iterable
>>> isinstance(['abc', 'def'], Iterable)
>>> True

然后我得到的结果是:

1
2
3
4
>>> from collections import Iterable
__main__:1: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working
>>> isinstance(['abc', 'def'], Iterable)
True

我目前使用的是Python 3.7.4,Warning中也明确说了,3.8以后我们就不能再从collections中import或者使用ABCs(Abstract Base Classes)。所以我们得改成:

1
2
3
4
5
6
7
>>> from collections.abc import Iterable, Iterator
>>> isinstance(['abc', 'def'], Iterable)
True
>>> isinstance(['abc', 'def'], Iterator)
False
>>> isinstance(iter(['abc', 'def']), Iterator)
True

Generator

生成器本身一定是Iterator,并且它也可以用于实现iterable的__iter__()方法。有两种方法可以方便地创建生成器,其中第二种方法涉及到yield语句,我不想再展开说,以后如果有时间再另写一篇关于yield语句以及和它有关的协程。

生成器表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> g = (num for num in range(4,9))
>>> type(g)
<class 'generator'>
>>> isinstance(g, collections.abc.Iterator)
True
>>> for i in g:
... print(i)
...
4
5
6
7
8

生成器函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def my_range(low, high):
while True:
if low >= high:
break
yield low
low += 1

g = my_range(4, 9)

print(type(g)) # <class 'generator'>
print(isinstance(g, collections.abc.Iterator)) # True

for i in g:
print(i)

尾巴

最后用一张来自Vincent Driessen的一篇博文Iterables vs. Iterators vs. Generators的图片来总结下:

img

很长时间没用就是会让记忆变得模糊哈,多查文档多用搜索引擎,python的文档真的很棒,希望你们都学得开心。

CATALOG
  1. 1. 定义
    1. 1.1. Iterator
    2. 1.2. Iterable
    3. 1.3. for…in
  2. 2. 自己动手
    1. 2.1. 自己的Iterator
    2. 2.2. 自己的Iterable
  3. 3. 判断可迭代对象
  4. 4. Generator
    1. 4.1. 生成器表达式
    2. 4.2. 生成器函数
  5. 5. 尾巴