Python: range не является итератором!

Spread the love

После моего выступления в Loop Better на PyGotham 2017 кто-то задал мне отличный вопрос: iterator – это lazy iterable (iterable это итерируемый объект а lazy означает отложенное действие прим. переводчика), и range – это lazy iterable в Python 3? Является ли range итератором?

К сожалению, я не помню имя человека, который задал мне этот вопрос. Я помню, что говорил что-то вроде «о, я люблю этот вопрос!»

Мне нравится этот вопрос, потому что range объекты в Python 3 (xrange в Python 2) действительно объекты с отложенным действием то есть lazy, но range объекты не являются итераторами, и это то, что, как я вижу, люди часто путают.

В прошлом году я много раз слышал, что новички в Python, опытные программисты Python и даже другие инструкторы Python ошибочно называют range объекты в Python 3 итераторами. Это особенность часто путает многих людей.

Да это сбивает с толку

Когда люди говорят об итераторах (iterators) и итерируемых объектах (iterables) в Python, вы, вероятно, слышите, как кто-то повторяет неправильное представление о том, что range является итератором. Поначалу эта ошибка может показаться несущественной, но я думаю, что на самом деле она довольно критична. Если вы считаете, что range объекты являются итераторами, ваши представления о работе итераторов в Python не верны. И range, и итераторы в некотором смысле «ленивы», но они ленивы по-разному.

В этой статье я собираюсь объяснить, как работают итераторы, как работает range, и как отличается лень этих двух типов «ленивых итераций».

Но сначала я хотел бы попросить вас не использовать приведенную ниже информацию в качестве предлога, чтобы быть высокомерным с кем либо, будь то новые ученики или опытные программисты в Python. Многие люди продуктивно используют Python в течение многих лет, не понимая в полной мере различие, которое я собираюсь объяснить. Вы можете написать много тысяч строк кода Python, не имея четкого представления как работают итераторы. И в этом нет ничего предосудительного.

Что такое итератор (iterator)?

В Python итерируемый объект (iterable) – это любой объект, предоставляющий возможность поочерёдного прохода по своим элементам, а итератор (iterator) – это то, что выполняет реальный проход.

Вы можете получить итератор из любого итерируемого объекта в Python, используя функцию iter:

>>> iter([1, 2])
<list_iterator object at 0x7f043a081da0>
>>> iter('hello')
<str_iterator object at 0x7f043a081dd8>

Если у вас есть итератор, единственное, что вы можете с ним сделать – это получить следующий элемент с помощью функции next:

>>> my_iterator = iter([1, 2])
>>> next(my_iterator)
1
>>> next(my_iterator)
2

Вы получите исключение остановки итерации StopIteration, если запросите следующий элемент, но при этом все элементы будут пройдены:

>>> next(my_iterator)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

И удобно, и несколько запутанно, все итераторы тоже итерируемы. Это означает, что вы можете получить итератор из итератора (то есть он вернет себя сам). То есть вы также можете перебирать итератор:

>>> my_iterator = iter([1, 2])
>>> [x**2 for x in my_iterator]
[1, 4]

Важно отметить, что у итераторов есть состоянием. То есть, как только вы получили элемент из итератора, он удаляется из итератора (а не из источника данных) . Поэтому после того, как вы пройдете по всем элементам один раз, итератор будет пуст:

>>> my_iterator = iter([1, 2])
>>> [x**2 for x in my_iterator]
[1, 4]
>>> [x**2 for x in my_iterator]
[]

В Python 3 функции enumerate, zip, reversed и ряд других встроенных функций возвращают итераторы:

>>> enumerate(numbers)
<enumerate object at 0x7f04384ff678>
>>> zip(numbers, numbers)
<zip object at 0x7f043a085cc8>
>>> reversed(numbers)
<list_reverseiterator object at 0x7f043a081f28>

Генераторы (будь то из функции генератора или выражения генератора generator expressions) являются одним из наиболее простых способов создания собственных итераторов:

>>> numbers = [1, 2, 3, 4, 5]
>>> squares = (n**2 for n in numbers)
>>> squares
<generator object <genexpr> at 0x7f043a0832b0>

Я часто говорю, что итераторы – это ленивые одноразовые итерации. Они «ленивы», потому что у них есть возможность вычислять элементы, только тогда когда вы проходите через них. И они «одноразовые», потому что как только вы «получили (consumed)» элемент из итератора, он исчезнет навсегда. Термин «исчерпан (exhausted)» часто используется для полностью использованного итератора.

Это было краткое изложение того, что такое итераторы. Если вы раньше не сталкивались с итераторами, я бы порекомендовал почитать о них больше, прежде чем продолжить. Я написал статью, в которой объясняются итераторы, и я выступил с докладом Loop Better, о котором я упоминал ранее, в ходе которого я углубился в итераторы.

Чем отличается range?

Хорошо, мы рассмотрели итераторы. Давайте сейчас поговорим о range.

Объект range в Python 3 (xrange в Python 2) может быть пройден как любой другой итерируемый объект:

>>> for n in range(3):
...     print(n)
...
0
1
2

И так как range является итеративным, мы можем получить итератор из него:

>>> iter(range(3))
<range_iterator object at 0x7f043a0a7f90>

Но сами объекты range не являются итераторами. Мы не можем вызвать next для объекта range:

>>> next(range(3))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'range' object is not an iterator

И в отличие от итератора, мы можем перебрать объект range без его опустошения (consuming):

>>> numbers = range(3)
>>> tuple(numbers)
(0, 1, 2)
>>> tuple(numbers)
(0, 1, 2)

Если бы мы сделали это с помощью итератора, мы не получили бы элементов при втором прохождение:

>>> numbers = iter(range(3))
>>> tuple(numbers)
(0, 1, 2)
>>> tuple(numbers)
()

В отличие от объектов zip, enumerate или generator объекты range не являются итераторами.

Так что такое range?

Объект range в некотором смысле является «ленивым» , потому что он не генерирует все числа, которые он «содержит», сразу когда мы его создаем. Вместо этого он дает нам эти числа, когда они нам нужны.

Создадим для примера объект range и генератор (который является итератором):

>>> numbers = range(1_000_000)
>>> squares = (n**2 for n in numbers)

В отличие от итераторов, объекты range имеют длину:

>>> len(numbers)
1000000
>>> len(squares)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'generator' has no len()

И они могут быть проиндексированы:

>>> numbers[-2]
999998
>>> squares[-2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable

И в отличие от итераторов, вы можете спросить их, содержат ли они объекты, не меняя их состояния:

>>> 0 in numbers
True
>>> 0 in numbers
True
>>> 0 in squares
True
>>> 0 in squares
False

Если вы ищете определение для объектов range, вы можете назвать их «ленивые последовательности (lazy sequences)». Они представляют собой последовательности Sequence (такие же как списки, кортежи и строки), но на самом деле они не содержат никакой памяти или состояния и вместо этого отвечают на вопросы в вычислительном отношении.

>>> from collections.abc import Sequence
>>> isinstance([1, 2], Sequence)
True
>>> isinstance('hello', Sequence)
True
>>> isinstance(range(3), Sequence)
True

Что это отличие означает?

Может показаться, что я придираюсь к тому, чтобы сказать, что range не является итератором, но я действительно не думаю, что это так.

Если я скажу вам, что что-то является итератором, вы будете знать, что когда вы вызовете iter для него, вы всегда получите тот же объект (по определению):

>>> iter(my_iterator) is my_iterator
True

И вы будете знать, что вы можете вызывать next на нем, потому что вы можете вызывать next на всех итераторах:

>>> next(my_iterator)
4
>>> next(my_iterator)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

И вы будете подразумевать, что элементы будут исчезать из итератора, когда вы проходить по нему. Иногда эта особенность может пригодиться для обработки итераторов определенными способами:

>>> my_iterator = iter([1, 2, 3, 4])
>>> list(zip(my_iterator, my_iterator))
[(1, 2), (3, 4)]

Поэтому, хотя может показаться, что разница между «ленивым итеративным объектом» и «итератором» невелика, эти термины действительно означают разные вещи. В то время как «ленивый итерируемый объект» является очень общим термином без конкретного значения, слово «итератор» подразумевает объект с очень специфическим набором поведений.

Когда вы сомневаетесь говорите “iterable” или “lazy iterable”

Если вы знаете, что можете пройти по списку каких либо элементов говорите что, это iterable.

Если вы знаете, что вычисление происходит только в момент прохождения то, это будет lazy iterable.

Если вы знаете, что можете использовать функцию next, то это итератор (iterator) (наиболее распространенная форма отложенных итераций lazy iterables).

Если вы можете циклически пройти несколько раз, не «исчерпывая» его, это не итератор. Если вы не можете передать что-то функции next, это не итератор. Объект range Python 3 не является итератором. Если вы учите людей объектам range, не используйте слово «итератор». Это сбивает с толку и может привести к тому, что другие тоже начнут неправильно использовать слово «итератор».

С другой стороны, если вы видите, что кто-то другой неправильно использует слово итератор, не принимайте это близко к сердцу. Вы можете указать на неправильное использование, только если это будет казаться важным, но имейте в виду, что я слышал, что даже опытные программисты Python и преподаватели Python злоупотребляли этим словом, называя итераторами range объекты. Слова важны, но язык еще сложнее.

Спасибо, что присоединились ко мне в этом краткой заметке о range и наполненном итераторами приключении!

Оригинал:Python: range is not an iterator!


Spread the love

Python: range не является итератором!: 1 комментарий

  • 05.04.2019 в 11:03
    Permalink

    Спасибо за перевод. Познавательно!

    Ответ

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *