详细解析Python可迭代对象与迭代器

迭代是数据处理的基石:程序将计算应用于数据序列。如果数据太大,在内存中放不下,则需要采用“惰性”的方式获取数据,即只按需获取和处理数据项的一部分,而不是一次性加载整个数据集。这就是迭代器的作用。

在Python中,所有容器类型都是可迭代对象。Python使用可迭代对象提供的迭代器支持以下操作:

  1. for循环
  2. 列表,字典,集合推导式
  3. 拆包赋值
  4. 构造容器实例

单词序列

首先我们通过下面代码开启可迭代对象之旅。

import re
import reprlib

RE_WORD = re.compile(r'\w+')


class Sentence:
    def __init__(self, text: str):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __len__(self):
        return len(self.words)

    def __getitem__(self, index):
        return self.words[index]

    def __repr__(self):
        # reprlib.repr这个实用函数用于生成大型模型结构的简略字符串表现形式
        return f"Sentence({reprlib.repr(self.text)})"

我们测试一下上面的代码:

s = Sentence(text="静夜思:唐【李白】 床前明月光,疑是地上霜。举头望明月,低头思故乡。")
print(s)   # 输出:Sentence('静夜思:唐【李白】 床前...。举头望明月,低头思故乡。')
for i in s:
    print(i)
print(s[0]) # 静夜思
print(s[-1]) # 低头思故乡
print(list(s))  # 输出:['静夜思', '唐', '李白', '床前明月光', '疑是地上霜', '举头望明月', '低头思故乡']

上面代码中传入一个字符串,创建一个Sentence实例。reprlib.repr函数默认情况下生成的字符串最多有30个字符,所以多余的字符串使用...替代。Sentence实例可以迭代,因为可以迭代,使用Sentence对象可用于构建列表和其他可迭代类型。

Python开发者都知道,序列可以迭代,为什么呢?

序列可以迭代的原因:iter函数

需要迭代对象x时,Python会自动调用iter(x)。

内置函数iter执行以下操作:

  1. 检查对象是否实现了__iter__方法,如果实现了就调用它,获取一个迭代器。
  2. 如果没有实现__iter__方法,但是实现了__getitem__方法,那么iter()创建一个迭代器,尝试索引(从0开始)获取项。
  3. 然后尝试失败,则Python抛出TypeError异常,通常会提示“'C' object is not iterable”,其中C是目标对象所属的类。

所有Python序列可迭代的原因是,按照定义,序列都实现了__getitem__方法(不懂的话可以去看看前面的《深入剖析序列的特殊方法》一文)。其实,标准的序列也都实现了__iter__方法。因此不仅实现了特殊方法__iter__的对象被视为可迭代对象,实现了__getitem__方法的对象也被视为可迭代对象。

from collections import abc
class Demo:
    def __getitem__(self, item):
        pass

class Demo1:
    def __iter__(self):
        pass

print(iter(Demo()))  # <iterator object at 0x000001E61560C610>
print(isinstance(Demo1(),abc.Iterable)) #True

值得一提的是,在Python3.10开始,检查对象x是否迭代,最准确的方法是使用iter(x)函数,如果不可迭代,则处理TypeError异常。这比isinstence(x,abc.Iterable)更准确,因为iter(x)函数还会考虑过时的__getitem__方法,而抽象基类Iterable则不考虑。

内置的iter()更常被Python自身使用,我们自己很少用到。iter()函数还有另一种用法,不过鲜为人知。

使用iter处理可调用对象

调用iter()时,传入两个参数可以为函数或任何可迭代对象创建迭代器。对于这种用法,第一个参数必须是一个可迭代对象,重复调用产生值;第二个参数是哨符,即一种标记值,如果可调用对象返回哨符,则迭代器抛出StopIteration,而不产出哨符。

例如下面示例:使用iter函数掷一个6面骰子,直到点数为1.

import random

def d6():
    return random.randint(1, 6)

d6_iter = iter(d6, 1)
print(d6_iter)
for i in d6_iter:
    print(i)
输出:
<callable_iterator object at 0x0000016A13F82C80>
4
3
5
6
2
5
3
2
4

上面的iter函数返回的是一个callable_iterator。这个示例中的for循环可能会运行很长时间,但是绝对不会产生1,因为1是哨符。

可迭代对象和迭代器

由上面内容可知,可迭代对象:使用内置函数iter可以获取迭代器的对象。如果对象实现了能返回迭代器的__iter__方法,那么对象就是可迭代的。序列都可以迭代。实现了__getitem__方法,而且接受从0开始的索引,这种对象也可以迭代。

可迭代对象和迭代器之间的关系是:Python从可迭代对象中获取迭代器。

Python中字符串是可以迭代的。表面上看不出来,但是背后有一个迭代器。

s = 'ABC'
for char in s:
    print(char)

假如没有for语句,我们不得不使用while循环模拟:

s='ABC'
s_iter = iter(s)
while True:
    try:
        print(next(s_iter))
    except StopIteration:
        del s_iter # 释放s_iter的引用,即废弃迭代器对象
        break

StopIteration表明迭代器已耗尽。内置函数iter()在内部自行处理for循环和其他迭代上下文(列表推导式,可迭代对象拆包等)的StopIteration异常。

Python标准的迭代器接口有以下两个方法:

  1. __next__:返回序列的下一项,如果没有项了,抛出StopIteration异常
  2. __iter__:返回self,以便在预期可迭代对象的地方使用迭代器。例如for循环中

回到上面的Sentence类:

s = Sentence(text="静夜思:唐【李白】 床前明月光,疑是地上霜。举头望明月,低头思故乡。")
it = iter(s)
print(it)  # <iterator object at 0x0000026DBBCD3460>
print(next(it))  # 静夜思
print(next(it))  # 唐
print(list(it))  # ['李白', '床前明月光', '疑是地上霜', '举头望明月', '低头思故乡']

可以清楚的看出iter()是如何构建迭代器的,以及next()是如何使用迭代器的,以及无法重置已经耗尽的迭代器。

为Sentence类实现__iter__方法

下面实现经典迭代器设计模式。虽然不符合Python的习惯做法,后续会重构。但是通过下面代码,可以明确可迭代容器和迭代器之间的关系。

import re
import reprlib

RE_WORD = re.compile(r'\w+')


class Sentence:
    def __init__(self, text: str):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __len__(self):
        return len(self.words)

    def __repr__(self):
        # reprlib.repr这个实用函数用于生成大型模型结构的简略字符串表现形式
        return f"Sentence({reprlib.repr(self.text)})"

    def __iter__(self):
        return SentenceIterator(self.words)


class SentenceIterator:
    def __init__(self, words):
        self.words = words
        self.index = 0

    def __next__(self):
        try:
            word = self.words[self.index]
        except IndexError:
            raise StopIteration()
        self.index += 1
        return word

    def __iter__(self):
        return self

与前一版相比,这里只多了一个__iter__方法,没有了__getitem__方法,为的是明确表明这个类之所以可以迭代,是因为实现了__iter__方法。根据可迭代协议,__iter__方法实例化并返回一个迭代器(SentenceIterator)。

注意:对于上面的SentenceIterator迭代器示例来说,没必要在类中实现__iter__方法,不过这样做是对的,因为迭代器应该实现__iter__和__next__这两个方法,而且这样做能让迭代器通过issubclass(SentenceIterator,abc.Iterator)测试。

不要把可迭代对象变成迭代器

构建可迭代对象和迭代器时经常会出现错误,原因是混淆了二者。要知道,可迭代对象有个__iter__方法,每次都实例化一个新的迭代器;而迭代器要实现__next__方法,返回单个元素,此外还要实现__iter__方法,返回本身。

因此迭代器是可迭代对象,但是可迭代对象不是迭代器。

 

 

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值