Разница между генераторами и итераторами в Python
В чем разница между итераторами и генераторами? Приведите примеры, когда целесообразно использовать каждый из этих подходов.
5 ответ(ов)
Итератор
— это более общее понятие: любой объект, чей класс имеет метод __next__
(или next
в Python 2) и метод __iter__
, который возвращает self
.
Каждый генератор является итератором, но не наоборот. Генератор создается с помощью вызова функции, содержащей одно или несколько выражений yield
(в Python 2.5 и ранее — заявлений yield
) и является объектом, соответствующим определению итератора из предыдущего абзаца.
Вам может понадобиться использовать пользовательский итератор, а не генератор, когда вам требуется класс с довольно сложным поведением, поддерживающим состояние, или если вы хотите предоставить другие методы помимо __next__
(и __iter__
, и __init__
). Чаще всего генератора (иногда, для достаточно простых нужд, и генераторное выражение) будет достаточно, и его легче реализовать, потому что управление состоянием (в разумных пределах) в основном "выполняется за вас" благодаря приостановке и возобновлению фрейма.
Например, генератор, такой как:
def squares(start, stop):
for i in range(start, stop):
yield i * i
generator = squares(a, b)
или эквивалентное генераторное выражение (genexp):
generator = (i*i for i in range(a, b))
потребует гораздо больше кода для создания в качестве пользовательского итератора:
class Squares(object):
def __init__(self, start, stop):
self.start = start
self.stop = stop
def __iter__(self):
return self
def __next__(self): # next в Python 2
if self.start >= self.stop:
raise StopIteration
current = self.start * self.start
self.start += 1
return current
iterator = Squares(a, b)
Но, конечно, с классом Squares
вы могли бы легко предложить дополнительные методы, например:
def current(self):
return self.start
если в вашем приложении есть реальная необходимость в такой дополнительной функциональности.
Итераторы — это объекты, которые используют метод next()
для получения следующих значений последовательности.
Генераторы — это функции, которые создают или "отдают" последовательность значений, используя ключевое слово yield
.
Каждый вызов метода next()
на объекте генератора (например, на f
ниже), возвращённом функцией-генератором (например, foo()
ниже), генерирует следующее значение в последовательности.
Когда функция-генератор вызывается, она возвращает объект генератора, не начиная выполнение функции. При первом вызове метода next()
функция начинает выполняться до тех пор, пока не достигнет оператора yield
, который возвращает выдаваемое значение. yield
продолжает отслеживать, что происходило ранее, т.е. он запоминает последнее выполнение. При следующем вызове next()
выполнение продолжается с последнего значения.
Следующий пример демонстрирует взаимодействие между yield
и вызовом метода next
на объекте генератора.
>>> def foo():
... print("begin")
... for i in range(3):
... print("before yield", i)
... yield i
... print("after yield", i)
... print("end")
...
>>> f = foo()
>>> next(f)
begin
before yield 0 # Управление находится в цикле for
0
>>> next(f)
after yield 0
before yield 1 # Продолжаем выполнение цикла for
1
>>> next(f)
after yield 1
before yield 2
2
>>> next(f)
after yield 2
end
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Все уже дали действительно замечательные и подробные ответы с примерами, и я их очень ценю. Я просто хотел дать краткий ответ для тех, кто все еще не совсем ясно понимает концепцию:
Если вы создаете свой собственный итератор, это немного более сложно — вам нужно создать класс и реализовать хотя бы методы __iter__
и __next__
. Но что, если вы не хотите заниматься этой морокой и хотите быстро создать итератор? К счастью, Python предоставляет упрощенный способ определения итератора. Все, что вам нужно сделать, — это определить функцию с хотя бы одним вызовом yield
, и теперь, когда вы вызываете эту функцию, она возвращает "что-то", что будет вести себя как итератор (вы сможете вызывать метод next
и использовать его в цикле for
). Это что-то в Python называется Генератором.
Надеюсь, это немного прояснит ситуацию.
Предыдущие ответы упустили важное дополнение: генераторы имеют метод close
, в то время как обычные итераторы этого не делают. Метод close
вызывает исключение StopIteration
в генераторе, которое может быть перехвачено в блоке finally
этого итератора, что дает возможность выполнить некоторые операции очистки. Эта абстракция делает генераторы более удобными в использовании, чем простые итераторы. Вы можете закрыть генератор так же, как закрываете файл, не беспокоясь о его внутреннем устройстве.
Скажем так, мой личный ответ на первый вопрос был бы таким: у итерируемых объектов есть только метод __iter__
, у обычных итераторов есть только метод __next__
, а у генераторов — и __iter__
, и __next__
, а также дополнительный метод close
.
Что касается второго вопроса, мой личный ответ был бы таков: в публичном интерфейсе я предпочитаю генераторы, так как они более устойчивы: метод close
и большая компоновка с yield from
. Локально я могу использовать итераторы, но только если это простая и плоская структура (итераторы плохо компонуются) и если есть основания полагать, что последовательность относительно короткая, особенно если ее можно остановить до достижения конца. Я склонен рассматривать итераторы как примитив низкого уровня, за исключением литералов.
Что касается управления потоком, генераторы являются столь же важной концепцией, как и промисы: оба понятия абстрактны и компонуемы.
Вот краткое руководство по генераторам в Python в формате ответов на StackOverflow:
Генераторная функция — это функция, содержащая оператор
yield
.Генераторное выражение — это аналог спискового включения, использующее круглые скобки
()
вместо квадратных[]
.Генераторный объект (часто называемый просто "генератор") возвращается как из генераторной функции, так и из генераторного выражения.
Генератор также является подтипом итератора.
Как пройтись по двум спискам параллельно?
Как перебрать файлы в указанной директории?
Получить первый элемент из итерируемого объекта, соответствующий условию
Что делает yield без значения в контекстном менеджере?
Как клонировать список, чтобы он не изменялся неожиданно после присваивания?