文章目录

  • 1. 可迭代对象概念首窥
  • 2. 判断对象是否是可迭代的
  • 3. 自定义类创建对象使用for循环
  • 3.1 自定义类实现`__iter__`方法
  • 3.1.1 迭代器
  • 3.1.2 自定义迭代器类
  • 3.1.3 迭代器使用for循环
  • 3.2 自定义类实现`__getitem__`方法
  • 3.3 for循环运行机制小结
  • 4. 迭代器应用
  • 4.1 利用迭代器实现数据生成的方式
  • 4.2 作为参数传入其他接收可迭代对象处



Python中的for循环语法十分简洁,那么其背后的运行机制你是否了解。实际上,要了解其背后的机制,需要先充分了解Python中另外两个重要概念:可迭代对象、迭代器。

一句话先总结可迭代对象和迭代器的区别与联系:可迭代对象的概念比迭代器的概念要广很多,可迭代对象包括迭代器和生成器,迭代器和生成器在很多地方均可以作为函数或方法的参数,此时为了说明参数的性质,一般生命接收的参数为可迭代对象

1. 可迭代对象概念首窥

下面首先通过for循环来引出可迭代对象的概念。如下述代码:

def test_list():

    for each in [1, 2, 3]:
        print(each)

    print()


def test_tuple():

    for each in (11, 22, 33):
        print(each)

    print()


def test_dict():

    for key, value in {"C": "Cheng", "Q": "Qing", "S": "Song"}.items():
        print(key, "=", value)

    print()


def test_str():

    for each in "Pythonic":
        print(each)

    print()


def test_int():

    for each in 100:
        print(each)

    print()


def main():
    test_list()
    test_tuple()
    test_dict()
    test_str()
    test_int()


if __name__ == "__main__":
    main()

上述代码的运行结果为:

1
2
3


11
22
33


C = Cheng
Q = Qing
S = Song


P
y
t
h
o
n
i
c


TypeError: ‘int’ object is not iterable

由上述运行结果可知,列表、元组、字典、字符串都可以使用for循环遍历,而int数值不可以,且编译器提示TypeError: 'int' object is not iterable

根据上述结果以及错误信息,可以反推:

既然数值不可以通过for循环遍历,且编译器报错为“数值不可以迭代”,那么列表、元组、字典、字符串必然都是可迭代的,即对象可以使用for循环的必要条件是该对象是可迭代的

由此,我们引出了可迭代可迭代对象的概念,进一步地,在Python的官方文档中,对于iterable给出的定义为:

  • An object capable of returning its members one at a time.
  • 可迭代对象是一类对象,即这类对象可以一次返回一个内部成员(联系for循环的现象)。
  • Examples of iterables include all sequence types (such as list, str, and tuple) and some non-sequence types like dict, file objects, and objects of any classes you define with an __iter__ method or with a __getitem__ method that implements Sequence semantics.
  • 可迭代对象的例子包括所有序列类型(如:列表、字符串、元组)以及部分非序列类型,如:字典、文件对象,以及任何定义了__iter__(所谓“支持迭代协议(iteration protocol)”)或__getitem__(所谓“支持序列协议(sequence protocol)”)方法的类所创建的对象

2. 判断对象是否是可迭代的

在Python中可以通过內置函数isinstance(object, classinfo)来方便地判断一个对象是否为可迭代的,即:函数的第一个参数传入待判断对象,第二个参数为包collections.abcabc为Abstract Base Class的缩写)中Iterable类,如果函数返回值是True,则待判断对象是可迭代的,否则不是。如:

In [1]: from collections.abc import Iterable

In [2]: isinstance([1, 2], Iterable)
Out[2]: True

In [3]: isinstance((11, 22), Iterable)
Out[3]: True

In [4]: isinstance("CodingGuru", Iterable)
Out[4]: True

In [5]: isinstance({"P": "Python"}, Iterable)
Out[5]: True

In [6]: isinstance(100, Iterable)
Out[6]: False

3. 自定义类创建对象使用for循环

通过前面的讨论,我们知道:

  • 可以使用for循环的必要条件是该对象是可迭代的
  • 任何对象想要成为可迭代对象的一个充分条件是创建该对象的类实现了 __iter____getitem__方法

3.1 自定义类实现__iter__方法

那么,自然地,我们会想,是否通过任何实现__iter____getitem__方法的类所创建的对象都可以使用for循环呢?下面通过代码验证:

from collections.abc import Iterable


class ScandinavianGod(object):
    def __init__(self):
        self.names = list()

    def add_name(self, name):
        self.names.append(name)

    def __iter__(self):
        pass


def main():
    scandinavian_god = ScandinavianGod()

    scandinavian_god.add_name("奥丁")
    scandinavian_god.add_name("托尔")
    scandinavian_god.add_name("洛基")

    print("判断scandinavian_god是否为可迭代对象:", isinstance(scandinavian_god, Iterable))

    for name in scandinavian_god:
        print(name)


if __name__ == "__main__":
    main()

上述代码的运行结果为:

判断scandinavian_god是否为可迭代对象: True
TypeError: iter() returned non-iterator of type ‘NoneType’

通过上述代码的运行结果可知,对象scandinavian_god虽然是可迭代的,但是不能使用for循环,即:

一个对象是可迭代的是一个对象可使用for循环的必要不充分条件

因此,我们将目光再次转向Python官方文档关于__iter__解释的部分:

  • This method is called when an iterator is required for a container.
  • 当一个容器需要一个迭代器时,该方法被调用。
  • This method should return a new iterator object that can iterate over all the objects in the container.
  • 该方法应当返回一个新的迭代器对象,该迭代器对象可以迭代容器中的所有对象。
  • Iterator objects also need to implement this method; they are required to return themselves.
  • 迭代器对象也需要实现该方法,因为迭代器对象需要返回其自身

即上述问题出现在,我们并没有正确实现__iter__方法,即没有让该方法返回一个迭代器对象,再回看上述代码的错误信息也是指向这一点,即:TypeError: iter() returned non-iterator of type 'NoneType'

3.1.1 迭代器

虽然找到了问题,但我们首先需要知道__iter__方法要返回的迭代器是什么,Python官方文档中,对于迭代器的定义为:

  • An object representing a stream of data.
  • 迭代器是一个对象,该对象代表了一个数据流。
  • Repeated calls to the iterator’s __next__ method (or passing it to the built-in function next()) return successive items in the stream.
  • 重复调用迭代器的__next__方法(或将迭代器对象当作参数传入內置函数next()中)将依次返回数据流中的元素。
  • When no more data are available a StopIteration exception is raised instead.
  • 当数据流中无可返回元素时,则抛出StopIteration异常。
  • Iterators are required to have an __iter__ method that returns the iterator object itself so every iterator is also iterable and may be used in most places where other iterables are accepted.
  • 迭代器必须拥有__iter__方法,该方法返回迭代器对象自身,因此,每一个迭代器都是可迭代的,并且可以用于大多数可迭代对象的使用场合(其他场合请见第5部分——迭代器应用)。

3.1.2 自定义迭代器类

通过上述的研究,我们知道,自定义类创建的对象要想使用for循环的一个充分条件是:

  • 该对象需要实现__iter__方法,且
  • __iter__方法返回一个迭代器对象。

而对于迭代器对象,Python官方文档的要求为:

  • 迭代器必须拥有__iter__方法,该方法返回迭代器对象自身;
  • 迭代器还需要拥有__next__方法,而重复调用迭代器的__next__方法(或将迭代器对象当作参数传入內置函数next()中)将依次返回数据流中的元素。

基于上述结论,下面代码:

  • 实现一个自定义迭代器类ScandinavianGodIterator
  • 该类中拥有一个__iter__方法,该方法返回迭代器对象自身;
  • 该类中还拥有一个__next__方法,该方法依次返回数据流中的元素;
  • ScandinavianGod类中的__iter__方法返回迭代器类ScandinavianGodIterator所创建的对象。

基于上述分析,有下面代码:

class ScandinavianGod(object):
    def __init__(self):
        self.names = list()

    def add_name(self, name):
        self.names.append(name)

    def __iter__(self):
        # 将通过ScandinavianGod类创建的对象引用传递至迭代器初始化方法,
        # 使得:通过类ScandinavianGodIterator创建的对象后,
        # 该对象的__next__()方法可以获取实例属性names后进行数据依次取出
        return ScandinavianGodIterator(self)


class ScandinavianGodIterator(object):

    def __init__(self, obj):
        # 定义一个实例属性,用于接收传递过来的ScandinavianGod类创建的对象引用
        self.obj = obj
        self.current_index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.current_index < len(self.obj.names):
            ret = self.obj.names[self.current_index]
            self.current_index += 1
            return ret
        else:
            # 遍历完self.obj.names实例属性后,抛出该异常,该异常将由for循环捕捉处理
            raise StopIteration


def main():
    scandinavian_god = ScandinavianGod()

    scandinavian_god.add_name("奥丁")
    scandinavian_god.add_name("托尔")
    scandinavian_god.add_name("洛基")

    for name in scandinavian_god:
        if name == "奥丁":
            print("主神:%s" % name)
        elif name == "托尔":
            print("雷神:%s" % name)
        elif name == "洛基":
            print("恶作剧与毁灭之神:%s" % name)
        else:
            print("其他神:%s" % name)


if __name__ == "__main__":
    main()

运行上述代码,输出结果为:

主神:奥丁
雷神:托尔
恶作剧与毁灭之神:洛基

即成功实现了自定义类创建对象使用for循环。

3.1.3 迭代器使用for循环

由上述讨论可知,如果想要对自定义类创建的对象使用for循环,需要使用两个类,此举不仅复杂还不直观,是否可以仅使用一个类呢?答案是肯定的。

首先,有上述讨论可知:迭代器一定是可迭代对象(因为其实现了__iter__方法),但可迭代对象不一定是迭代器(因为可迭代对象可能未实现__next__方法)。

因此,可迭代对象要想成为一个迭代器,其中一个充分条件是其要实现__next__方法

故:

  • ScandinavianGod类中实现__next__方法使之通过其创建的对象都是迭代器;
  • __iter__方法处返回self

因此,有下列代码:

class ScandinavianGod(object):
    def __init__(self):
        self.names = list()
        self.current_index = 0

    def add_name(self, name):
        self.names.append(name)

    def __iter__(self):
        return self

    def __next__(self):
        if self.current_index < len(self.names):
            ret = self.names[self.current_index]
            self.current_index += 1
            return ret
        else:
            raise StopIteration


def main():
    scandinavian_god = ScandinavianGod()

    scandinavian_god.add_name("奥丁")
    scandinavian_god.add_name("托尔")
    scandinavian_god.add_name("洛基")

    for name in scandinavian_god:
        print(name)


if __name__ == "__main__":
    main()

3.2 自定义类实现__getitem__方法

class ScandinavianGod(object):
    def __init__(self):
        self.names = list()

    def add_name(self, name):
        self.names.append(name)

    def __getitem__(self, key):
        if (type(key) is int) and (key < len(self.names)):
            print("key = ", key)
            return self.names[key]
        elif type(key) is not int:
            raise TypeError
        else:
            raise IndexError


def main():
    scandinavian_god = ScandinavianGod()

    scandinavian_god.add_name("奥丁")
    scandinavian_god.add_name("托尔")
    scandinavian_god.add_name("洛基")

    # for循环方式遍历
    print("=" * 10, "for循环方式遍历:", "=" * 10)
    for name in scandinavian_god:
        if name == "奥丁":
            print("主神:%s" % name)
        elif name == "托尔":
            print("雷神:%s" % name)
        elif name == "洛基":
            print("恶作剧与毁灭之神:%s" % name)
        else:
            print("其他神:%s" % name)

    # 索引方式遍历
    print("=" * 10, "索引方式遍历:", "=" * 10)
    print(scandinavian_god[0])
    print(scandinavian_god[1])
    print(scandinavian_god[2])


if __name__ == "__main__":
    main()

3.3 for循环运行机制小结

通过上述分析,我们知道,对于自定义类实现__iter__方法,使用for循环遍历一个对象的流程应该是:

  • 判断该对象是否为可迭代的:检查该对象是否有__iter__方法;
  • 判断__iter__方法是否有正确类型返回值:检查返回值是否为迭代器引用;
  • 依次顺序获取待遍历元素:调用迭代器的__next__方法;
  • 判断是否遍历完所有元素:捕获处理StopIteration异常。

4. 迭代器应用

在上述讨论中,我们明确了可迭代对象、迭代器的概念,并且基于这两个概念,实现了对于自定义类创建的对象使用for循环。

那么,是否上述所有的讨论都仅仅只能实现对自定义类创建对象使用for循环呢?答案是否定的,这样一种情形下,迭代器可以大大提升程序(内存方面)的效率:即希望利用程序生成大量数据供程序使用,如:生成一个很长的斐波那契数列。

针对上述需求,现有两种实现思路:

  • 定义一个迭代器类,实现生成斐波那契数列的方法
  • 定义一函数,利用一个容器(如:列表)存储生成的斐波那契数列的每一项

4.1 利用迭代器实现数据生成的方式

下面代码利用迭代器实现数据生成的方式,并使用for循环遍历数列:

import time


class Fibonacci(object):
    def __init__(self, len_of_fib):
        self._current_fib_num = 0
        self._next_fib_num = 1
        self._len_of_fib = len_of_fib
        self._current_index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._current_index < self._len_of_fib:
            ret = self._current_fib_num
            self._current_fib_num, self._next_fib_num = \
                self._next_fib_num, self._current_fib_num + self._next_fib_num
            self._current_index += 1
            return ret
        else:
            raise StopIteration


def main():
    t_start = time.time()

    for each in Fibonacci(100000):
        pass

    t_stop = time.time()

    elapsed_time = t_stop - t_start

    print(elapsed_time)


if __name__ == "__main__":
    main()

为了进行对比,下面代码利用容器先存储生成的数据,然后使用for循环遍历数列::

import time


def fibonacci(len_of_fib):

    fib_sequence = list()

    current_fib_num = 0
    next_fib_num = 1
    current_index = 0

    while True:
        if current_index < len_of_fib:
            fib_sequence.append(current_fib_num)
            current_fib_num, next_fib_num = next_fib_num, \
                current_fib_num + next_fib_num
            current_index += 1
        else:
            break

    return fib_sequence


def main():
    t_start = time.time()

    # fibonacci(1000000)
    for each in fibonacci(100000):
        pass

    t_stop = time.time()

    elapsed_time = t_stop - t_start

    print(elapsed_time)


if __name__ == "__main__":
    main()

运行上述代码,结果分别为:

  • 利用迭代器实现数据生成方式

程序执行完成所需的时间为0.109802485秒…

  • 利用容器存储生成的数据:

程序执行完成所需的时间为0.226054192秒…

我们知道:

第一种方式程序执行所需时间仅为第二种方式的一半;
实际上,如果再增加数列项数的数量级,第一种方式所需时间虽然会增加,但第二种方式会很快导致计算机无法执行程序。

究其缘由,在于第二种方式是一次性生成大量数据,然后将其保存在列表中,内存的开销相比于第一种大得多得多,第一种方式只是实现了获取所需数据的方式,故内存开销只有运算和实例属性存储时所需的内存。

实际上,上述两种实现策略分别相当于Python 2.7中xrange()range()的实现策略

4.2 作为参数传入其他接收可迭代对象处

在Python中,可以很方便地使用tuple将一个列表转换为元组,使用list实现相反操作。实际上,tuplelist的形参处所接收的都是可迭代对象,如下述代码:

class Fibonacci(object):
    def __init__(self, len_of_fib):
        self._current_fib_num = 0
        self._next_fib_num = 1
        self._len_of_fib = len_of_fib
        self._current_index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._current_index < self._len_of_fib:
            ret = self._current_fib_num
            self._current_fib_num, self._next_fib_num = \
                self._next_fib_num, self._current_fib_num + self._next_fib_num
            self._current_index += 1
            return ret
        else:
            raise StopIteration


def main():
    fib_tuple = tuple(Fibonacci(10))
    print(fib_tuple)

    fib_list = list(Fibonacci(10))
    print(fib_list)


if __name__ == "__main__":
    main()

其运行结果为:

(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]