更深入的理解 Python 中的迭代

从我们作为 Python 程序员的角度来看,你可以使用迭代器来做的唯一有用的事情是将其传递给内置的 next 函数,或者对其进行循环遍历:

>>> next(iterator)

1

>>> list(iterator)

[2, 3, 5, 7]

如果我们第二次循环遍历迭代器,我们将一无所获:

>>> list(iterator)

[]

你可以把迭代器看作是惰性迭代器,它们是一次性使用,这意味着它们只能循环遍历一次。

正如你在下面的真值表中所看到的,可迭代对象并不总是迭代器,但是迭代器总是可迭代的:

对象可迭代?迭代器?
可迭代对象V?
迭代器VV
生成器VV
列表VX

全部的迭代器协议

让我们从 Python 的角度来定义迭代器是如何工作的。

可迭代对象可以被传递给 iter 函数,以便为它们获得迭代器。

迭代器:

  • 可以传递给 next 函数,它将给出下一项,如果没有下一项,那么它将会引发 StopIteration 异常

  • 可以传递给 iter 函数,它会返回一个自身的迭代器

这些语句反过来也是正确的:

  • 任何可以在不引发 TypeError 异常的情况下传递给 iter 的东西都是可迭代的

  • 任何可以在不引发 TypeError 异常的情况下传递给 next 的东西都是一个迭代器

  • 当传递给 iter 时,任何返回自身的东西都是一个迭代器

这就是 Python 中的迭代器协议。

迭代器的惰性

迭代器允许我们一起工作,创建惰性可迭代对象,即在我们要求它们提供下一项之前,它们不做任何事情。因为可以创建惰性迭代器,所以我们可以创建无限长的迭代器。我们可以创建对系统资源比较保守的迭代器,可以节省我们的内存,节省 CPU 时间。

迭代器无处不在

你已经在 Python 中看到过许多迭代器,我也提到过生成器是迭代器。Python 的许多内置类型也是迭代器。例如,Python 的 enumerate 和 reversed 对象就是迭代器。

>>> letters = ['a', 'b', 'c']

>>> e = enumerate(letters)

>>> e

>>> next(e)

(0, 'a')

在 Python 3 中,zip, map 和 filter 也是迭代器。

>>> numbers = [1, 2, 3, 5, 7]

>>> letters = ['a', 'b', 'c']

>>> z = zip(numbers, letters)

>>> z

>>> next(z)

(1, 'a')

Python 中的文件对象也是迭代器。

>>> next(open('hello.txt'))

'hello world\n'

在 Python 标准库和第三方库中内置了大量的迭代器。这些迭代器首先惰性迭代器一样,延迟工作直到你请求它们下一项。

创建你自己的迭代器

知道你已经在使用迭代器是很有用的,但是我希望你也知道,你可以创建自己的迭代器和你自己的惰性迭代器。

下面这个类构造了一个迭代器接受一个可迭代的数字,并在循环结束时提供每个数字的平方。

class square_all:

def __init__(self, numbers):

self.numbers = iter(numbers)

def __next__(self):

return next(self.numbers) * 2

def __iter__(self):

return self

但是在我们开始对该类的实例进行循环遍历之前,没有任何工作要做。

这里,我们有一个无限长的可迭代对象 count,你可以看到 square_all 接受 count 而不用完全循环遍历这个无限长的迭代:

>>> from itertools import count

>>> numbers = count(5)

>>> squares = square_all(numbers)

>>> next(squares)

25

>>> next(squares)

36

这个迭代器类是有效的,但我们通常不会这样做。通常,当我们想要做一个定制的迭代器时,我们会生成一个生成器函数:

def square_all(numbers):

for n in numbers:

yield n**2

这个生成器函数等价于我们上面所做的类,它的工作原理是一样的。

这种 yield 语句似乎很神奇,但它非常强大:yield 允许我们在调用 next 函数之间暂停生成器函数。yield 语句是将生成器函数与常规函数分离的东西。

另一种实现相同迭代器的方法是使用生成器表达式。

def square_all(numbers):

return (n**2 for n in numbers)

这和我们的生成器函数确实是一样的,但是它使用的语法看起来 像是一个列表推导一样 。如果你需要在代码中使用惰性迭代,请考虑迭代器,并考虑使用生成器函数或生成器表达式。

迭代器如何改进你的代码

一旦你已经接受了在代码中使用惰性迭代器的想法,你就会发现有很多可能来发现或创建辅助函数,以此来帮助你循环遍历和处理数据。

惰性求和

这是一个 for 循环,它对 Django queryset 中的所有工作时间求和:

hours_worked = 0

for event in events:

if event.is_billable():

hours_worked += event.duration

下面是使用生成器表达式进行惰性评估的代码:

billable_times = (

event.duration

for event in events

if event.is_billable()

)

hours_worked = sum(billable_times)

请注意,我们代码的形状发生了巨大变化。

将我们的计算工作时间变成一个惰性迭代器允许我们能够命名以前未命名(billable_times)的东西。这也允许我们使用 sum 函数,我们以前不能使用 sum 函数是因为我们甚至没有一个可迭代对象传递给它。迭代器允许你从根本上改变你组织代码的方式。

惰性和打破循环

这段代码打印出日志文件的前 10 行:

for i, line in enumerate(log_file):

if i >= 10:

break

print(line)

这段代码做了同样的事情,但是我们使用的是 itertools.islice 函数来惰性地抓取文件中的前 10 行:

from itertools import islice

first_ten_lines = islice(log_file, 10)

for line in first_ten_lines:

print(line)

我们定义的 first_ten_lines 变量是迭代器,同样,使用迭代器允许我们给以前未命名的东西命名(first_ten_lines)。命名事物可以使我们的代码更具描述性,更具可读性。

作为奖励,我们还消除了在循环中使用 break 语句的需要,因为 islice 实用函数为我们处理了中断。

你可以在标准库中的 itertools 中找到更多的迭代辅助函数,以及诸如 boltons 和 more-itertools 之类的第三方库。

创建自己的迭代辅助函数

你可以在标准库和第三方库中找到用于循环的辅助函数,但你也可以自己创建!

这段代码列出了序列中连续值之间的差值列表。

current = readings[0]

for next_item in readings[1:]:

differences.append(next_item - current)

current = next_item

请注意,这段代码中有一个额外的变量,我们每次循环时都要指定它。还要注意,这段代码只适用于我们可以切片的东西,比如序列。如果 readings 是一个生成器,一个 zip 对象或其他任何类型的迭代器,那么这段代码就会失败。

让我们编写一个辅助函数来修复代码。

这是一个生成器函数,它为给定的迭代中的每个项目提供了当前项和下一项:

def with_next(iterable):

"""Yield (current, next_item) tuples for each item in iterable."""

iterator = iter(iterable)

current = next(iterator)

for next_item in iterator:

yield current, next_item

current = next_item

我们从可迭代对象中手动获取一个迭代器,在它上面调用 next 来获取第一项,然后循环遍历迭代器获取后续所有的项目,跟踪后一个项目。这个函数不仅适用于序列,而且适用于任何类型迭代。

这段代码和以前代码是一样的,但是我们使用的是辅助函数而不是手动跟踪 next_item:

differences = []

for current, next_item in with_next(readings):

differences.append(next_item - current)

请注意,这段代码不会挂在我们循环周围的 next_item 上,with_next 生成器函数处理跟踪 next_item 的工作。

还要注意,这段代码已足够紧凑,如果我们愿意,我们甚至可以 将方法复制到列表推导中来 。

differences = [

(next_item - current)

for current, next_item in with_next(readings)

]

再次回顾循环问题

现在我们准备回到之前看到的那些奇怪的例子并试着找出到底发生了什么。

问题 1:耗尽的迭代器

这里我们有一个生成器对象 squares:

>>> numbers = [1, 2, 3, 5, 7]

>>> squares = (n**2 for n in numbers)

如果我们把这个生成器传递给 tuple 构造函数,我们将会得到它的一个元组:

>>> numbers = [1, 2, 3, 5, 7]

>>> squares = (n**2 for n in numbers)

>>> tuple(squares)

(1, 4, 9, 25, 49)

如果我们试着计算这个生成器中数字的和,使用 sum,我们就会得到 0:

>>> sum(squares)

0

这个生成器现在是空的:我们已经把它耗尽了。如果我们试着再次创建一个元组,我们会得到一个空元组:

>>> tuple(squares)

()

生成器是迭代器,迭代器是一次性的。它们就像 Hello Kitty Pez 分配器那样不能重新加载。

问题 2:部分消耗一个迭代器

再次使用那个生成器对象 squares:

>>> numbers = [1, 2, 3, 5, 7]

>>> squares = (n**2 for n in numbers)

如果我们询问 9 是否在 squares 生成器中,我们会得到 True:

>>> 9 in squares

True

但是我们再次询问相同的问题,我们会得到 False:

>>> 9 in squares

False

当我们询问 9 是否在迭代器中时,Python 必须对这个生成器进行循环遍历来找到 9。如果我们在检查了 9 之后继续循环遍历,我们只会得到最后两个数字,因为我们已经在找到 9 之前消耗了这些数字:

>>> numbers = [1, 2, 3, 5, 7]

>>> squares = (n**2 for n in numbers)

>>> 9 in squares

True

>>> list(squares)

[25, 49]

询问迭代器中是否包含某些东西将会部分地消耗迭代器。如果没有循环遍历迭代器,那么是没有办法知道某个东西是否在迭代器中。

问题 3:拆包是迭代

当你在字典上循环时,你会得到键:

>>> counts = {'apples': 2, 'oranges': 1}

>>> for key in counts:

... print(key)

...

apples

oranges

当你对一个字典进行拆包时,你也会得到键:

>>> x, y = counts

>>> x, y

('apples', 'oranges')

循环依赖于迭代器协议,可迭代对象拆包也依赖于有迭代器协议。拆包一个字典与在字典上循环遍历是一样的,两者都使用迭代器协议,所以在这两种情况下都得到相同的结果。

回顾

序列是迭代器,但是不是所有的迭代器都是序列。当有人说“迭代器”这个词时,你只能假设他们的意思是“你可以迭代的东西”。不要假设迭代器可以被循环遍历两次、询问它们的长度或者索引。

迭代器是 Python 中最基本的可迭代形式。如果你想在代码中做一个惰性迭代,请考虑迭代器,并考虑使用生成器函数或生成器表达式。

最后,请记住,Python 中的每一种迭代都依赖于迭代器协议,因此理解迭代器协议是理解 Python 中的循环的关键。

这里有一些我推荐的相关文章和视频:

  • Loop Like a Native , Ned Batchelder 在 PyCon 2013 的讲演

  • Loop Better ,这篇文章是基于这个讲演的

  • The Iterator Protocol: How For Loops Work ,我写的关于迭代器协议的短文

  • Comprehensible Comprehensions ,关于推导和迭代器表达器的讲演

  • Python: Range is Not an Iterator ,我关于范围和迭代器的文章

  • Looping Like a Pro in Python ,DB 的 PyCon 2017 讲演

本文是基于作者去年在 DjangoCon AU 、 PyGotham 和 North Bay Python 中发表的 Loop Better 演讲。有关更多内容,请参加将于 2018 年 5 月 9 日至 17 日在 Columbus, Ohio 举办的 PYCON 。


via: https://opensource.com/article/18/3/loop-better-deeper-look-iteration-python


分享到:


相關文章: