Python基础学习之二迭代器

Python  for 循环不会像其他语言中的 for 循环那样工作。在这篇文章中,我们将深入探讨 Python  for 循环来看看它们在底层如何工作,以及为什么它们会按照它们的方式工作。

循环的问题

我们将通过看一些“陷阱”开始我们的旅程,在我们了解循环如何在 Python 中工作之后,我们将再次看看这些问题并解释发生了什么。

问题 1:循环两次

假设我们有一个数字列表和一个生成器,生成器会返回这些数字的平方:


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

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

我们可以将生成器对象传递给 tuple 构造器,从而使其变为一个元组:

>>> tuple(squares)

(1, 4, 9, 25, 49)

如果我们使用相同的生成器对象并将其传给 sum 函数,我们可能会期望得到这些数的和,即 88

>>> sum(squares)

0

但是我们得到了 0

问题 2:包含的检查

让我们使用相同的数字列表和相同的生成器对象:

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

 

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

如果我们询问 9 是否在 squares 生成器中,Python 将会告诉我们 9  squares 中。但是如果我们再次询问相同的问题,Python 会告诉我们 9 不在 squares 中。

>>> 9 in squares

True

>>> 9 in squares

False


我们询问相同的问题两次,Python 给了两个不同的答案。

问题 3 :拆包

这个字典有两个键值对:

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

让我们使用多个变量来对这个字典进行拆包:

>>> x, y = counts

你可能会期望当我们对这个字典进行拆包时,我们会得到键值对或者得到一个错误。

但是解包字典不会引发错误,也不会返回键值对。当你解包一个字典时,你会得到键:

>>> x

'apples'


回顾:Python for 循环

在我们了解一些关于这些 Python 片段的逻辑之后,我们将回到这些问题。

Python 没有传统的 for 循环。为了解释我的意思,让我们看一看另一种编程语言的 for 循环。

这是一种传统 C 风格的 for 循环,用 JavaScript 编写:


let numbers = [1, 2, 3, 5, 7];

for (let i = 0; i < numbers.length; i += 1) {

    print(numbers[i])

}

JavaScript C C++ Java PHP 和一大堆其他编程语言都有这种风格的 for 循环,但是 Python 确实没有。

Python 确实没有 传统 C 风格的 for 循环。在 Python 中确实有一些我们称之为 for 循环的东西,但是它的工作方式类似于foreach循环。

这是 Python  for 循环的风格:

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

for n in numbers:

    print(n)

与传统 C 风格的 for 循环不同,Python  for 循环没有索引变量,没有索引变量初始化,边界检查,或者索引递增。Python  for 循环完成了对我们的 numbers 列表进行遍历的所有工作。

因此,当我们在 Python 中确实有 for 循环时,我们没有传统 C 风格的 for 循环。我们称之为 for 循环的东西的工作机制与之相比有很大的不同。

定义:可迭代和序列

既然我们已经解决了 Python 世界中无索引的 for 循环,那么让我们在此之外来看一些定义。

可迭代是任何你可以用 Python 中的 for 循环遍历的东西。可迭代意味着可以遍历,任何可以遍历的东西都是可迭代的。

for item in some_iterable:

    print(item)

序列是一种非常常见的可迭代类型,列表,元组和字符串都是序列。

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

>>> coordinates = (4, 5, 7)

>>> words = "hello there"

序列是可迭代的,它有一些特定的特征集。它们可以从 0 开始索引,以小于序列的长度结束,它们有一个长度并且它们可以被切分。列表,元组,字符串和其他所有序列都是这样工作的。

>>> numbers[0]

1

>>> coordinates[2]

7

>>> words[4]

'o'

Python 中很多东西都是可迭代的,但不是所有可迭代的东西都是序列。集合、字典、文件和生成器都是可迭代的,但是它们都不是序列。

>>> my_set = {1, 2, 3}

>>> my_dict = {'k1': 'v1', 'k2': 'v2'}

>>> my_file = open('some_file.txt')

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

因此,任何可以用 for 循环遍历的东西都是可迭代的,序列只是一种可迭代的类型,但是 Python 也有许多其他种类的迭代器。

Python for 循环不使用索引

你可能认为,Python  for 循环在底层使用了索引进行循环。在这里我们使用 while 循环和索引手动遍历:

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

i = 0

while i < len(numbers):

    print(numbers[i])

    i += 1

这适用于列表,但它不会对所有东西都起作用。这种循环方式只适用于序列。

如果我们尝试用索引去手动遍历一个集合,我们会得到一个错误:

>>> fruits = {'lemon', 'apple', 'orange', 'watermelon'}

>>> i = 0

>>> while i < len(fruits):

...     print(fruits[i])

...     i += 1

...

Traceback (most recent call last):

File "<stdin>", line 2, in <module>

TypeError: 'set' object does not support indexing

集合不是序列,所以它们不支持索引。

我们不能使用索引手动对 Python 中的每一个迭代对象进行遍历。对于那些不是序列的迭代器来说,这是行不通的。

迭代器驱动 for 循环

因此,我们已经看到,Python  for 循环在底层不使用索引。相反,Python  for 循环使用迭代器。

迭代器就是可以驱动可迭代对象的东西。你可以从任何可迭代对象中获得迭代器,你也可以使用迭代器来手动对它的迭代进行遍历。

让我们来看看它是如何工作的。

这里有三个可迭代对象:一个集合,一个元组和一个字符串。

>>> numbers = {1, 2, 3, 5, 7}

>>> coordinates = (4, 5, 7)

>>> words = "hello there"

我们可以使用 Python 的内置 iter 函数来访问这些迭代器,将一个迭代器传递给 iter 函数总会给我们返回一个迭代器,无论我们正在使用哪种类型的迭代器。

>>> iter(numbers)

<set_iterator object at 0x7f2b9271c860>

>>> iter(coordinates)

<tuple_iterator object at 0x7f2b9271ce80>

>>> iter(words)

<str_iterator object at 0x7f2b9271c860>

一旦我们有了迭代器,我们可以做的事情就是通过将它传递给内置的 next 函数来获取它的下一项。

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

>>> my_iterator = iter(numbers)

>>> next(my_iterator)

1

>>> next(my_iterator)

2

迭代器是有状态的,这意味着一旦你从它们中消耗了一项,它就消失了。

如果你从迭代器中请求 next 项,但是其中没有更多的项了,你将得到一个 StopIteration 异常:

>>> next(my_iterator)

3

>>> next(my_iterator)

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

StopIteration

所以你可以从每个迭代中获得一个迭代器,迭代器唯一能做的事情就是用 next 函数请求它们的下一项。如果你将它们传递给 next,但它们没有下一项了,那么就会引发 StopIteration 异常。

你可以将迭代器想象成 Pez 分配器(LCTT 译注:Pez 是一个结合玩具的独特复合式糖果),不能重新分配。你可以把 Pez 拿出去,但是一旦 Pez 被移走,它就不能被放回去,一旦分配器空了,它就没用了。

没有 for 的循环

既然我们已经了解了迭代器和 iter 以及 next 函数,我们将尝试在不使用 for 循环的情况下手动遍历迭代器。

我们将通过尝试将这个 for 循环变为 while 循环:

def funky_for_loop(iterable, action_to_do):

    for item in iterable:

        action_to_do(item)

为了做到这点,我们需要:

1.从给定的可迭代对象中获得迭代器

2.反复从迭代器中获得下一项

3.如果我们成功获得下一项,就执行 for 循环的主体

4.如果我们在获得下一项时得到了一个 StopIteration 异常,那么就停止循环

def funky_for_loop(iterable, action_to_do):

    iterator = iter(iterable)

    done_looping = False

    while not done_looping:

        try:

            item = next(iterator)

        except StopIteration:

            done_looping = True

        else:

            action_to_do(item)

我们只是通过使用 while 循环和迭代器重新定义了 for 循环。

上面的代码基本上定义了 Python 在底层循环的工作方式。如果你理解内置的 iter  next函数的遍历循环的工作方式,那么你就会理解 Python  for 循环是如何工作的。

事实上,你不仅仅会理解 for 循环在 Python 中是如何工作的,所有形式的遍历一个可迭代对象都是这样工作的。

迭代器协议iterator protocol 是一种很好表示 “在 Python 中遍历迭代器是如何工作的”的方式。它本质上是对 iter  next 函数在 Python 中是如何工作的定义。Python 中所有形式的迭代都是由迭代器协议驱动的。

迭代器协议被 for 循环使用(正如我们已经看到的那样):


for n in numbers:

    print(n)

多重赋值也使用迭代器协议:

x, y, z = coordinates

星型表达式也是用迭代器协议:

a, b, *rest = numbers

print(*numbers)

许多内置函数依赖于迭代器协议:

unique_numbers = set(numbers)

Python 中任何与迭代器一起工作的东西都可能以某种方式使用迭代器协议。每当你在 Python 中遍历一个可迭代对象时,你将依赖于迭代器协议。

生成器是迭代器

所以你可能会想:迭代器看起来很酷,但它们看起来像一个实现细节,我们作为 Python 的使用者,可能不需要关心它们。

我有消息告诉你:在 Python 中直接使用迭代器是很常见的。

这里的 squares 对象是一个生成器:

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

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

生成器是迭代器,这意味着你可以在生成器上调用 next 来获得它的下一项:

>>> next(squares)

1

>>> next(squares)

4

但是如果你以前用过生成器,你可能也知道可以循环遍历生成器:

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

>>> for n in squares:

...     print(n)

...

1

4

9

如果你可以在 Python 中循环遍历某些东西,那么它就是可迭代的。

所以生成器是迭代器,但是生成器也是可迭代的,这又是怎么回事呢?

我欺骗了你

所以在我之前解释迭代器如何工作时,我跳过了它们的某些重要的细节。

生成器是可迭代的

我再说一遍:Python 中的每一个迭代器都是可迭代的,意味着你可以循环遍历迭代器。

因为迭代器也是可迭代的,所以你可以使用内置 next 函数从可迭代对象中获得迭代器:

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

>>> iterator1 = iter(numbers)

>>> iterator2 = iter(iterator1)

请记住,当我们在可迭代对象上调用 iter 时,它会给我们返回一个迭代器。

当我们在迭代器上调用 iter 时,它会给我们返回它自己:

>>> iterator1 is iterator2

True

迭代器是可迭代的,所有的迭代器都是它们自己的迭代器。

def is_iterator(iterable):

    return iter(iterable) is iterable

迷惑了吗?

让我们回顾一些这些措辞。

  • 一个可迭代对象是你可以迭代的东西
  • 一个迭代对象器是一种实际上遍历可迭代对象的代理

此外,在 Python 中迭代器也是可迭代的,它们充当它们自己的迭代器。

所以迭代器是可迭代的,但是它们没有一些可迭代对象拥有的各种特性。

迭代器没有长度,它们不能被索引:

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

>>> iterator = iter(numbers)

>>> len(iterator)

TypeError: object of type 'list_iterator' has no len()

>>> iterator[0]

TypeError: 'list_iterator' object is not subscriptable



猜你喜欢

转载自blog.csdn.net/jack_chen3/article/details/80549531