Python中的迭代协议

Python 小记 2019-02-08 3780 字 2665 浏览 点赞

前言

Python的特色之一是基于协议实现功能。比如改变一个加号(+)的行为,在C++中需要操作符重载,在Python中则是重写__add__方法。为了描述可迭代对象和迭代器,Python提供了两个魔法方法,分别是__iter____next__。又为了支持for...in...行为,牵扯进了__getitem__

我们先从可迭代对象说起。

Iterable

Python在collections.abc模板中提供了Iterable,用于判断对象是否可迭代,还提供了Iterator,判断一个对象是否是迭代器。这里拿熟知的列表类型试水:

lyst = ["t", "y"]

print(isinstance(lyst, Iterable))  # 输出:True

print(isinstance(lyst, Iterator))  # 输出:False

可见list只是一个可迭代对象,而非迭代器。

让一个对象成为可迭代对象只需要添加__iter__方法。

class Students(object):
    def __iter__(self):
        pass

if __name__ == "__main__":
    stu = Students()
    print(isinstance(stu, Iterable))  # 输出:True

尽管上面看来其有些欺诈——如果真的将它迭代会发现解释器报错TypeError。但不可否认的是,此时解释器认为stu是一个可迭代对象了(涉及到isinstance()的处理机制,可见《Python中的abc模块》),所以isinstance()返回出True。

Iterator

与可迭代对象最大的区别在于,可以对迭代器使用next()方法。认为,迭代器是特殊的可迭代对象,它需要__iter____next__两个协议支持。

用之前的欺骗手法做个测试:

class Students(object):
    def __iter__(self):
        pass

    def __next__(self):
        pass

if __name__ == "__main__":
    stu = Students()
    print(isinstance(stu, Iterable))  # 输出:True
    print(isinstance(stu, Iterator))  # 输出:True

对象stu是可迭代对象,也是迭代器。打印结果证实了前边说法。

for...in...

直接在协议函数下面写pass,让其充装可迭代对象、迭代器,这是利用了Python鸭子类型的特点。然而,如果希望对象可以正常的被for...in...遍历,有些逻辑需要我们手动实现。

先说__iter__方法,它只需要完成一个任务——返回一个迭代器。如果我们不返回迭代器,将引发报错:

class Students(object):
    def __iter__(self):
        return [0, 1, 2]  # 列表类型是可迭代对象,而不是迭代器

if __name__ == "__main__":
    stu = Students()

    for s in stu:
        print(s)

# 输出:
Traceback (most recent call last):
  File "xxx", line 50, in <module>
    for s in stu:
TypeError: iter() returned non-iterator of type 'list'

现在试着返回一个生成器(生成器是特殊的迭代器):

class Students(object):
    def __iter__(self):
        return (i for i in range(3))

if __name__ == "__main__":
    stu = Students()

    for s in stu:
        print(s)

# 输出:
0
1
2

事实上,在for...in...过程中,Python会自动调用iter()函数,也就是说for i in x被转换成了for i in iter(x)处理。想要继续运行下去,得先保证iter()函数不会抛错。


基于前面结论,__iter__不负责逻辑处理,那么处理逻辑就得写在__next__方法里面。像下面这样:

class Students(object):
    def __init__(self):
        self.items = [1, 2, 3, 4]
        self.index = 0

    def __next__(self):
        if self.index >= len(self.items):
            raise StopIteration
        result = self.items[self.index]
        self.index += 1
        return result

此时还不可以对Students的实例对象使用for遍历,但可以通过next()访问内部元素:

print(next(stu))
print(next(stu))
print(next(stu))

# 输出:
1
2
3

之前也说过了,迭代器需要实现__iter____next__两个方法,所以我们得添加__iter__。又因为__iter__的任务是返回一个迭代器,拥有了__iter____next__方法的Students的对象本身就是一个迭代器(似乎有点绕),所以返回自身(self)即可:

class Students(object):
    # ...
    def __iter__(self):
        return self
    # ...

另外需要说明的是,在for...in...遍历过程中,需要一个终止信号,这个信号就是StopIteration。for..in..遍历的时候等同于:

while True:
    try:
        print(next(stu))
    except StopIteration:
        break

getitem

__getitem__的作用可见《Python中的切片》for...in...运作的完整流程需要用到这个方法。从问第一个问题开始:

  • 问题1:对象中有__iter__方法吗?

    • 如果有,调用iter(对象);(此时StopIteration是终止信号,for..in..会自动处理StopIteration)
    • 如果没有,进入问题2。
  • 问题2:对象中有__getitem__方法吗?

    • 如果有,从0开始传入索引值访问内部元素,之后索引值依次累加:1,2,3,...(此时IndexError是终止信号,for..in..会自动处理IndexError)
    • 如果没有,抛出异常TypeError

感谢



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论