Course
Итераторы — это объекты, по которым можно итерироваться. Это одна из базовых возможностей языка программирования Python, лежащая в основе циклов и списковых включений. Любой объект, способный возвращать итератор, называется итерируемым объектом (iterable).
Создание итератора требует немалой работы. Например, реализация каждого объекта-итератора должна содержать методы __iter__() и __next__() . Помимо этого, реализация должна уметь отслеживать внутреннее состояние объекта и вызывать исключение StopIteration, когда больше не осталось значений для возврата. Эти правила известны как протокол итератора.
Реализовывать собственный итератор — процесс долгий и нужен не всегда. Проще использовать объект-генератор. Генераторы — это особый вид функций, которые используют ключевое слово yield, чтобы возвращать итератор, по которому можно пройтись по одному значению за раз.
Умение выбирать подходящие случаи для реализации итератора или использования генератора повысит ваши навыки как Python-разработчика. В оставшейся части этого руководства мы подчеркнем различия между этими двумя объектами, чтобы помочь вам решить, что лучше применять в разных ситуациях.
Глоссарий
|
Термин |
Определение |
|
Итерируемый объект |
Объект Python, по которому можно проходить в цикле. Примеры итерируемых объектов: списки, множества, кортежи, словари, строки и т. п. |
|
Итератор |
Объект, по которому можно итерироваться. Следовательно, итераторы содержат конечное число значений, доступных по очереди. |
|
Генератор |
Особый тип функции, которая не возвращает единичное значение: она возвращает объект-итератор с последовательностью значений. |
|
Ленивая вычисление (Lazy Evaluation) |
Стратегия вычислений, при которой некоторые объекты создаются только по мере необходимости. В разработке такую стратегию также называют «вызов по требованию» (call-by-need). |
|
Протокол итератора |
Набор правил, которым нужно следовать, чтобы определить итератор в Python. |
|
next() |
Встроенная функция, возвращающая следующий элемент итератора. |
|
iter() |
Встроенная функция, преобразующая итерируемый объект в итератор. |
|
yield() |
Ключевое слово Python, похожее на return, но |
Итераторы и итерируемые объекты в Python
Итерируемые объекты способны возвращать своих членов по одному — по ним можно итерироваться. Популярные встроенные структуры данных Python, такие как списки, кортежи и множества, являются итерируемыми. Строки и словари тоже считаются итерируемыми: строка позволяет пройти по своим символам, а по ключам словаря также можно итерироваться. Общее правило: любой объект, по которому можно пройти в цикле for, является итерируемым.
Примеры работы с итерируемыми объектами в Python
Исходя из определений, можно заключить, что все итераторы также являются итерируемыми. Однако не каждый итерируемый объект — итератор. Итерируемый объект порождает итератор лишь тогда, когда по нему начинают итерироваться.
Чтобы продемонстрировать это, создадим список (он итерируемый) и получим итератор, вызвав встроенную функцию iter() для списка.
list_instance = [1, 2, 3, 4]
print(iter(list_instance))
"""
<list_iterator object at 0x7fd946309e90>
"""
Хотя сам по себе список не является итератором, вызов функции iter() преобразует его в итератор и возвращает объект-итератор.
Чтобы показать, что не все итерируемые объекты являются итераторами, создадим тот же список и попытаемся вызвать функцию next(), которая возвращает следующий элемент итератора.
list_instance = [1, 2, 3, 4]
print(next(list_instance))
"""
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-0cb076ed2d65> in <module>()
3 print(iter(list_instance))
4
----> 5 print(next(list_instance))
TypeError: 'list' object is not an iterator
"""
В коде выше видно, что попытка вызвать next() для списка приводит к исключению TypeError — подробнее об этом читайте в материале Обработка исключений и ошибок в Python. Это произошло потому, что объект списка — итерируемый, но не итератор.
Примеры работы с итераторами в Python
Следовательно, если цель — пройти по списку, сначала нужно получить объект-итератор. И только затем можно управлять проходом по значениям списка.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# convert the list to an iterator
iterator = iter(list_instance)
# return items one at a time
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""
Python автоматически создает объект-итератор всякий раз, когда вы пытаетесь пройти по итерируемому объекту в цикле.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# loop through the list
for item in list_instance:
print(item)
"""
1
2
3
4
"""
Когда возникает исключение StopIteration, цикл завершается.
Значения из итератора можно получать только слева направо. В Python нет функции previous(), позволяющей двигаться по итератору назад.
Ленивая природа итераторов
Можно определить несколько итераторов на основе одного и того же итерируемого объекта. Каждый итератор будет хранить собственное состояние прогресса. Таким образом, создавая несколько экземпляров итератора для одного и того же итерируемого объекта, можно дойти до конца в одном экземпляре, пока другой останется в начале.
list_instance = [1, 2, 3, 4]
iterator_a = iter(list_instance)
iterator_b = iter(list_instance)
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"B: {next(iterator_b)}")
"""
A: 1
A: 2
A: 3
A: 4
B: 1
"""
Обратите внимание: iterator_b выводит первый элемент последовательности.
Следовательно, можно сказать, что итераторы имеют ленивую природу: при создании итератора элементы не выдаются, пока их не запросят. Иными словами, элементы нашего списка будут возвращаться только тогда, когда мы явно попросим об этом с помощью next(iter(list_instance)).
Однако все значения из итератора можно извлечь сразу, вызвав для объекта-итератора один из контейнеров встроенных итерируемых структур данных (например, list(), set(), tuple()), тем самым заставив итератор сгенерировать все свои элементы разом.
# instantiate iterable
list_instance = [1, 2, 3, 4]
# produce an iterator from an iterable
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""
Для больших итераторов это не рекомендуется, поскольку все элементы будут сгенерированы и удерживаться в памяти одновременно, что сводит на нет преимущества ленивых вычислений.
Когда набор данных слишком велик, чтобы комфортно помещаться в памяти, или когда вам нужна ленивая итерация без написания полноценного класса итератора, обычно лучше подойдут генераторы.
Генераторы в Python
Самая быстрая альтернатива реализации итератора — использовать генератор. Хотя генераторы внешне похожи на обычные функции Python, они отличаются. Прежде всего, объект-генератор не возвращает элементы напрямую. Вместо этого он использует ключевое слово yield, чтобы создавать элементы на лету. Таким образом, можно сказать, что генератор — это особая функция, использующая ленивые вычисления.
Генераторы не хранят свое содержимое в памяти, как это делает типичный итерируемый объект. Например, если цель — найти все делители положительного целого числа, мы обычно реализуем традиционную функцию (подробнее о функциях в Python — в этом руководстве) следующим образом:
def factors(n):
factor_list = []
for val in range(1, n+1):
if n % val == 0:
factor_list.append(val)
return factor_list
print(factors(20))
"""
[1, 2, 4, 5, 10, 20]
"""
Код выше возвращает весь список делителей. Однако посмотрите, чем все отличается, если вместо традиционной функции Python использовать генератор:
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
print(factors(20))
"""
<generator object factors at 0x7fd938271350>
"""
Поскольку мы использовали ключевое слово yield вместо return, выполнение функции не завершается сразу. По сути, мы сказали Python создать объект-генератор вместо традиционной функции, что позволяет отслеживать состояние генератора.
В результате можно вызывать функцию next() для ленивого итератора, чтобы получать элементы последовательности по одному.
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
factors_of_20 = factors(20)
print(next(factors_of_20))
"""
1
"""
Другой способ создать генератор — использовать выражение-генератор. Синтаксис у него похож на списковое включение, но вместо квадратных скобок используются круглые.
factor_gen = (val for val in range(1, 21) if 20 % val == 0)
print(list(factor_gen))
"""
[1, 2, 4, 5, 10, 20]
"""
Изучаем ключевое слово yield в Python
Ключевое слово yield управляет потоком выполнения функции-генератора. В отличие от return, которое завершает функцию, yield возвращает управление вызвавшему коду, но запоминает состояние локальных переменных.
Генератор, возвращенный из вызова yield, можно присвоить переменной и перебирать с помощью функции next() — при этом выполнение функции продолжится до первого встреченного yield. Когда встречается yield, выполнение функции приостанавливается. В этот момент состояние функции сохраняется, и мы можем возобновить выполнение по своему усмотрению.
Функция продолжит работу с места вызова yield. Например:
def yield_multiple_statements():
yield "This is the first statement"
yield "This is the second statement"
yield "This is the third statement"
yield "This is the last statement. Don't call next again!"
example = yield_multiple_statements()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statement
This is the second statement
This is the third statement
This is the last statement. Don't call next again or else!
--------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-25-4aaf9c871f91> in <module>()
11 print(next(example))
12 print(next(example))
---> 13 print(next(example))
StopIteration:
"""
В коде выше наш генератор содержит четыре вызова yield, но мы пытаемся вызвать next пять раз, что приводит к исключению StopIteration. Это происходит потому, что наш генератор не является бесконечной последовательностью, и количество вызовов превысило число доступных значений.
Итоги
Подытожим: итераторы — это объекты, по которым можно итерироваться, а генераторы — это особые функции, использующие ленивые вычисления. Реализуя собственный итератор, вы должны создать методы __iter__() и __next__(), тогда как генератор можно реализовать с помощью ключевого слова yield в функции или в выражении-генераторе.
Предпочтение пользовательскому итератору уместно, когда нужен объект со сложной логикой управления состоянием или требуется предоставить методы помимо __next__(), __iter__() и __init__(). С другой стороны, генератор может быть предпочтительнее при работе с большими наборами данных, поскольку он не хранит содержимое в памяти, либо когда нет необходимости реализовывать собственный итератор.
FAQS
В чём разница между итератором и генератором в Python?
Итератор — это любой объект, который реализует __iter__() и __next__(). Генератор — более простой способ создать итератор с помощью функции и ключевого слова yield. Все генераторы являются итераторами, но не все итераторы — генераторы.
Когда мне стоит использовать генератор вместо списка в Python?
Используйте генератор для больших или бесконечных последовательностей, а также когда важна экономия памяти. Списки хранят все элементы в памяти сразу, тогда как генераторы выдают по одному значению за раз. Для небольших наборов данных, к которым вы будете многократно обращаться, обычно подойдёт список.
Что делает ключевое слово yield в Python?
Ключевое слово yield превращает функцию в генератор. Вместо возврата и завершения yield приостанавливает функцию, возвращает значение и запоминает её состояние, чтобы выполнение можно было продолжить при следующем вызове.
Как создать генератор в Python?
Либо напишите функцию, которая использует yield вместо return, либо используйте выражение-генератор — тот же синтаксис, что и у спискового включения, но в круглых скобках, например (x * 2 for x in range(10)).
Являются ли генераторы быстрее итераторов в Python?
Не быстрее в чистой скорости, но они эффективнее по памяти, потому что выдают значения по требованию. Для больших наборов данных это часто даёт лучшую общую производительность; для небольших — разница пренебрежимо мала.
\n