14. 再谈函数的高级用法
14.1 迭代

基本概念

在前面的章节中,我们已经学习了 Python 提供的for语句和while语句。这两种语句可以帮助开发人员以循环的方式逐个访问序列中的元素。

举个例子:

lst = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for elem in lst:
    print(elem)

在这个例子中,我们通过for语句,以循环的方式,实现了对lst序列中所有元素的遍历。

通过“循环”这个概念,我们可以进一步引申出“迭代”的概念。

循环,众所周知,就是复执行一段代码一定次数或直到满足特定条件;相对应的,任何实现这种重复执行代码的机制,就叫做迭代(Iteration)。

换句话说,在编程中,迭代就是重复一组指令或操作的总体概念

相对而言,迭代是一个比循环更广泛的概念,它涵盖了与编程中的重复相关的各种技术和过程。迭代不仅仅限于循环;相反,循环只是迭代的一种具体实现。也就是说,我们可以将循环视为迭代的一个子集或一种具体类型。

在 Python 中,如果我们希望重复执行一组指令--也就是实现迭代,我们可以采用循环的方式,也可以采用诸如递归、列表推导、生成器等其他方式。

在本节中,我们就将深入地探究一下迭代在 Python 中的应用。

Python 中的迭代和可迭代对象

我们想学习迭代在 Python 中的应用,就离不开一个概念:可迭代对象(Iterables),因为 Python 中的迭代通常是基于可迭代对象的。

那么,什么是可迭代对象呢?

可迭代对象,首先是 Python 中的对象;其次,外部代码可以遍历其包含的每个元素。像我们之前学习的列表、元组、字符串、字典、集合等,都是 Python 中的可迭代对象,因为我们可以逐一访问它们包含的元素。

简而言之,我们可以把可迭代对象看作是迭代过程的数据源。

在 Python 中,像迭代这种重复执行代码的过程,通常都是基于可迭代对象进行的;换言之,在 Python 中,如果没有可迭代对象,就无法进行迭代。

在 Python 中,我们常见的可迭代对象包括:

  • 序列(如列表、元组、字符串)
  • 集合(如集合、字典)
  • 文件对象

这些可迭代对象与其他数据类型对象有什么区别呢?

  1. 迭代协议(Iteration Protocol)
    可迭代对象遵循迭代协议,这意味着它们可以与迭代结构(如for循环和while循环)一起使用,从而允许外部代码可以逐个访问其元素;相比之下,其他数据类型对象则不需遵循迭代协议,外部代码也无法遍历其内部元素。
  2. __iter__()方法
    可迭代对象具有一个__iter__()方法,该方法返回一个迭代器对象。迭代器负责跟踪迭代的当前状态,并在请求时使用__next__()方法提供下一个项目。
  3. __next__()方法
    可迭代对象返回的迭代器具有__next__()方法,该方法产生序列中的下一个项目,或在没有更多项目可迭代时触发StopIteration异常。这就允许外部代码可以逐个遍历可迭代对象的元素。
  4. 序列或集合
    可迭代对象通常是数据的序列或集合,例如列表、元组、字符串、字典、集合等等。这些数据结构设计用于保存多个项目,并且可以通过迭代来访问其元素。
  5. 延迟求值
    一些可迭代对象,如生成器和生成器表达式,使用延迟求值。也就是说,当创建这些对象时,它们并不会马上生成所有值;相反,它们会在外部代码遍历它们时,才即时生成值。这对于处理大型数据集可以节省内存。

正是由于可迭代对象拥有这些特点,我们才可以在类似for循环或者while循环中进行元素遍历;相对应的,我们在普通的数据类型对象中,是无法做到这一点的。

那么,除了 Python 内置的诸多可迭代对象外,我们可以创建自定义的可迭代对象吗?

答案是:可以!

下面就让我们来学习一下如何在 Python 中创建自定义的可迭代对象。

自定义可迭代对象

在 Python 中,如果我们想要创建自定义的可迭代对象,我们需要通过遵循迭代协议来实现。一般而言,要使一个类可迭代,我们需要定义两个特殊方法:__iter__()__next__()

  1. __iter__()方法
    __iter__()方法负责返回一个迭代器对象。迭代器是一个对象,它跟踪迭代的当前状态,并知道在请求时如何生成序列中的下一个项目。
  2. __next__()方法
    __next__()方法负责在每次迭代期间生成序列中的下一个项目。它确定下一个要返回的值,并推进迭代器的内部状态。

当我们在for循环或其他迭代结构中使用可迭代对象时,Python 的调用逻辑是这样的:

  1. Python 调用可迭代对象上的__iter__()方法以获取一个迭代器。这是迭代的初始化步骤。
  2. Python 重复调用迭代器上的__next__()方法以获取序列中的下一个项目。每次循环时,都会调用__next__()方法,并且迭代器会前进到下一个项目。
  3. 如果__next__()方法触发了StopIteration异常,则表示迭代结束,循环终止。

下面让我们看看如何在具体开发时,定义这两个方法。

假如,我们已经创建了一个自定义类。

class MyIterable:
    def __init__(self, data):
        self.data = data

为了让这个类可迭代,我们需要在类中实现__iter__()方法。

class MyIterable:
    def __init__(self, data):
        self.data = data
 
    def __iter__(self):
        self.index = 0
        return self

__iter__()方法中,我们初始化一个index属性,用于在迭代期间跟踪当前位置,并返回self作为迭代器。

接下来,我们需要实现__next__()方法。__next__()方法负责在每次迭代期间返回序列中的下一个项目。如果没有更多的项目可迭代,它应该出发StopIteration异常。

class MyIterable:
    def __init__(self, data):
        self.data = data
 
    def __iter__(self):
        self.index = 0
        return self
 
    def __next__(self):
        if self.index < len(self.data):
            result = self.data[self.index]
            self.index += 1
            return result
        else:
            raise StopIteration

这样,我们就创建了一个自定义可迭代类,我们可以在for循环或任何其他迭代结构中使用它,就像使用任何 Python 内置的可迭代对象一样。

my_data = [1, 2, 3, 4, 5]
my_iterable = MyIterable(my_data)
 
for item in my_iterable:
    print(item)

这段代码使用我们新创建的自定义可迭代对象,然后逐个遍历my_data中的元素。

迭代器与next()方法

我们在前面讲过,Python 中的列表、元组、集合和字符串等数据类型都是可迭代对象。这意味着我们可以使用for循环或其他迭代结构来遍历它们的元素;但这并不意味着所有可迭代对象都支持next()方法。

这里需要澄清的是,当我们在使用for循环进行迭代时,我们无需使用next()方法,因为 Python 在幕后为我们处理迭代过程。例如:

my_list = [1, 2, 3, 4]
for item in my_list:
    print(item)

在这种情况下,Python 会自动遍历my_list的元素。

当然,我们也可以显式地为这些数据类型创建一个迭代器,然后使用next()函数逐个访问它们的元素。要实现这一点,我们需要使用iter()函数获取可迭代对象的迭代器,然后在该迭代器上调用next()

例如:

my_list = [1, 2, 3, 4]
my_iterator = iter(my_list)
 
while True:
    try:
        item = next(my_iterator)
        print(item)
    except StopIteration:
        break

在这个例子中,我们使用iter()显式地为my_list创建了一个迭代器,然后在while循环中使用next()函数逐个访问其元素。这种方法可以让开发人员更精细地控制迭代过程。

在 Python 中,几乎所有的序列类型对象都可以通过iter() 函数转换成迭代器。

例如,我们可以使用以下代码将列表 [1, 2, 3, 4, 5] 转换成迭代器:

lst = [1, 2, 3, 4, 5]
x = iter(lst)
print(x)  # 输出:<list_iterator object at 0x10bf717b8>

我们也可以使用以下代码将元组 (1, 2, 3, 4, 5) 转换成迭代器:

t = (1, 2, 3, 4, 5)
x = iter(t)
print(x)  # 输出:<tuple_iterator object at 0x10bf717b8>

我们还可以使用以下代码将字符串 'hello' 转换成迭代器:

s = 'hello'
x = iter(s)
print(x)  # 输出:<str_iterator object at 0x10bf717b8>

可能有人会问,既然我们可以使用循环来遍历序列中的元素,为什么还要使用迭代器呢?

使用迭代器而不是传统循环来访问列表中的每个元素,其实会给我们带来很多好处,最主要的一个是:内存效率。

如前所述,迭代器提供了一种延迟求值的形式。也就是说,迭代器并不会在内存中预先计算和存储所有值,相反,它会在我们遍历序列时即时生成值。显而易见,这大幅降低了内存消耗,也避免了内存耗尽,因此迭代器在处理大型或无限数据集时,特别有帮助。

同时,由于迭代器不会在内存中预先存储所有值,所以,我们可以用迭代器来表示无限序列;而由于内存空间的有限性,我们是不可能使用传统的列表方式来存储无限序列的。

总结

迭代是 Python 中的一个基本概念,它允许开发人员高效地处理数据集合和执行重复任务。

在 Python 中,迭代是基于可迭代对象的。这些可迭代对象的目的是允许外部代码可以顺序访问其元素。

我们可以使用诸如for循环的语句来遍历内置的可迭代对象,如列表和字符串;还可以通过定义带有 __iter__()__next__() 方法的类来创建自定义可迭代对象。