AlexKorablev.ru
Александр Кораблев о разработке ПО, ИТ-индустрии и Python.
В чем разница между итератором и генератором?
Опубликовано 29 February 2016 в Python
Как ни странно, вопрос о разнице между генераторами и итераторами в Python — довольно частый вопрос. В общем-то эти сущности сильно связаны (любой генератор — это итератор), их довольно часто путают, что иногда приводит к недопониманиям.
Итератор — более общая концепция. Это объект, у которого определены два метода __next__ и __iter__.
С другой стороны, генератор — это итератор. Но не наоборот. Генератор может получаться использованием yield в теле функции.
def squares(start, stop): for i in range(start, stop): yield i * i generator = squares(a, b)
Либо использованием так называемых generator expression. На вроде такого:
generator = (i*i for i in range(a, b))
Если посмотреть dir(generator) и в первом и во втором случае, то обнаружим, что оба варианта — итераторы (определены функции __next__ и __iter__).
По большому счету генераторы — это синтаксический сахар. И без них можно писать такие же эффективные программы используя только итераторы. Вот только читаемость и простота кода сильно снизятся. Так что, если не нужно от итератора ничего сверх естественного, то стоит подумать над использованием генератора.
В примерах и объяснениях я использовал Python 3. Для второй версии языка есть незначительные отличия.
—
Возник вопрос? Мне всегда можно написать в Twitter: avkorablev
Понравилась статья? Поделись с друзьями!
Итерируемый объект, итератор и генератор в Python
В Python итерируемый объект (iterable или iterable object), итератор (iterator или iterator object) и генератор (generator или generator object) — разные понятия, а не синонимы одного и того же. От итерируемого объекта можно получить его «копию»-итератор; генератор является разновидностью итератора.
В некоторых источниках итератор рассматривается как частный случай итерируемого объекта, поскольку оба поддерживают операцию итерации, то есть обход циклом for . Однако for работает только с итераторами. Переданный на обработку объект должен иметь метод __iter__() , который for неявно вызывает перед обходом. Метод __iter__() должен возвращать итератор.
У итерируемого объекта, то есть объекта, который можно «превратить» в итератор, должен быть метод __iter__() , который возвращает соответствующий объект-итератор.
>>> a = [1, 2] >>> b = a.__iter__() >>> a [1, 2] >>> b >>> type(a) >>> type(b)
У итерируемого объекта нет метода __next__() , который используется при итерации:
>>> a.__next__() Traceback (most recent call last): File "", line 1, in AttributeError: 'list' object has no attribute '__next__'
У итератора есть метод __next__() , который извлекает из итератора очередной элемент. При этом этот элемент уже не содержится в итераторе. Таким образом, итератор в конечном итоге опустошается:
>>> b.__next__() 1 >>> b.__next__() 2 >>> b.__next__() Traceback (most recent call last): File "", line 1, in StopIteration
Метод __next__() исчерпанного итератора возбуждает исключение StopIteration .
У итераторов, также как у итерируемых объектов, есть метод __iter__() . Однако в данном случае он возвращает сам объект-итератор:
>>> a = [1, 2] >>> a = "hi" >>> b = a.__iter__() >>> c = b.__iter__() >>> a 'hi' >>> b >>> c >>> b.__next__() 'h' >>> c.__next__() 'i' >>> b.__next__() Traceback (most recent call last): File "", line 1, in StopIteration
Здесь переменные b и c указывают на один и тот же объект.
Примеры итерируемых объектов в Python — список, словарь, строка и другие контейнерные типы (они же коллекции), тип, возвращаемый функцией range() .
Примеры итераторов — файловые объекты, генераторы, итераторы созданные на основе списка, строки, объекта типа range и т. д.
В Python есть встроенные функции iter() и next() , которые соответственно вызывают методы __iter__() и __next__() объектов, переданных в качестве аргумента.
>>> a = >>> b = iter(a) >>> b >>> next(b) 1
Внутренний механизм цикла for сначала вызывает метод __iter__() объекта. Так что, если передан итерируемый объект, создается итератор. После этого применяется метод __next__() до тех пор, пока не будет возбуждено исключение StopIteration .
Поскольку метод __iter__() итератора возвращает сам итератор, то после перебора циклом for объект исчерпывается. То есть получить данные из итератора можно только один раз. В случае с коллекциями это не так. Здесь создается другой объект — итератор. Он, а не итерируемый объект, отдается на обработку циклу for .
>>> a = range(2) >>> b = iter(a) >>> type(a) >>> type(b) >>> for i in a: . print(i) . 0 1 >>> for i in a: . print(i) . 0 1 >>> for i in b: . print(i) . 0 1 >>> for i in b: . print(i) . >>>
Отличительной особенностью генераторов является то, что они создаются не на основе классов, а путем вызова функции, содержащей инструкцию yield, или специальным генераторным выражением по синтаксису похожим на генератор списка. Отметим, генератор списка, который является особым выражением, к генераторам, которые являются разновидностью объектов-итераторов, отношения не имеет. Подробнее можно почитать здесь.
Другими словами, если потребуется создать свой итератор, может оказаться проще определить функцию с yield или воспользоваться выражением, чем создавать класс с методами __next__() и __iter__() .
Рассмотрим пример. Определим сначала собственный класс-итератор:
from random import random class RandomIncrease: def __init__(self, quantity): self.qty = quantity self.cur = 0 def __iter__(self): return self def __next__(self): if self.qty > 0: self.cur += random() self.qty -= 1 return round(self.cur, 2) else: raise StopIteration iterator = RandomIncrease(5) for i in iterator: print(i)
0.65 1.17 1.19 1.45 2.11
Наш итератор выдает числа по нарастающей. При этом каждое следующее число больше предыдущего на случайную величину.
Здесь же отметим преимущество итераторов как таковых перед контейнерными типами вроде списков. В памяти компьютера не хранятся все элементы итератора, в основном лишь описание, как получить следующий элемент. Если представить, что нужны тысячи чисел или надо генерировать сложные объекты, выгода существенна.
В случае с функцией, создающей генератор, приведенный выше пример может выглядеть так:
def random_increase(quantity): cur = 0 while quantity > 0: cur += random() quantity -= 1 yield round(cur, 2) generator = random_increase(5) for i in generator: print(i)
Нам незачем самим определять методы __iter__() и __next__() , так как они неявно присутствуют у генератора.
Если логика генератора проста, вместо функции можно использовать выражение, создающее генератор:
g = (round(random()+i, 2) for i in range(5)) for i in g: print(i)
Данный пример не идентичен приведенным выше функции и классу. Здесь целая часть каждого следующего числа больше чем у предыдущего на единицу.
Генераторное выражение и функция-генератор возвращают объект одного и того же типа — generator .
Итераторы и генераторы
Итератор – это интерфейс доступа к элементам коллекций и потоков данных. Он требует реализации единственного метода – «дай мне следующий элемент». Если вы пишите свой итератор на Python 3 вам нужно реализовать в классе метод __next__. Если элементы исчерпаны итератор возбудит исключение StopIteration.
Пример. Итератор счетчик – выдает числа от low до high:
class Counter: def __init__(self, low, high): self.current = low self.high = high def __iter__(self): return self def __next__(self): if self.current > self.high: raise StopIteration else: self.current += 1 return self.current - 1
Генератор – это итератор
Генератор – это итератор, но не наоборот. Не любой итератор является генератором.
Есть два способа получить генератор:
1. Генераторное выражение (что-то типа list comprehension, но возвращает генератор, а не список). Используются круглые скобки:
>>> g = (2 * i for i in range(5)) >>> type(g) >>> next(g) 0 >>> next(g) 2
2. Генераторные функции. Это функции, где есть хотя бы одно выражение yield. Когда мы запускаем генератор, функция выполняет до первого выражения yield. То, что мы передали в yield будет возвращено наружу. Генератор при этом встанет «на паузу» до следующей итерации. При следующей итерации выполнение генератора продолжится до очередного yield.
Генераторы можно прочитать только 1 раз, потому что обычно генераторы не хранят значения в памяти, а генерируют их налету (отсюда и название).
Пример. Генератор чисел Фибоначчи (бесконечный):
def fib(): a, b = 0, 1 while 1: yield a a, b = b, a + b >>> fib_g = fib() >>> next(fib_g) 0 >>> next(fib_g) 1 >>> next(fib_g) 1 >>> next(fib_g) 2 >>> next(fib_g) 3 >>> next(fib_g) 5
Вызвав генераторную функцию fib() мы получили генератор. Затем мы итерируем этот генератор функцией next().
Остановка генератора
Если генератор «закончился» (т.е. просто вышли из функции генератора в конце его кода или по return), то автоматически возбуждается исключение StopIteration. Это не ошибка, это нормально, просто принятый способ обработки конца итератора.
def gen(): yield 1 yield 5 # и все, код кончился, вышли for x in gen(): print(x) # 1, 5
for in сам ловит исключение StopIteration и просто завершает итерировать этот генератор.
Передача данных в генератор
У генераторов есть дополнительные методы, которые позволяют передавать внутрь генератора данные или возбуждать внутри него исключения. Это еще одно отличие от простых итераторов.
send() – отправить данные в генератор. Переданное значение вернется из той конструкции yield, на которой возникла последняя пауза генератора. При этом генератор будет прокручен на один шаг, как если бы мы вызвали next:
val = yield i # генератор вернет i, но внутри получит val из аргумента метода send
Пример. Этот генератор просто выдает числа от 0 и далее, при этом печатает в поток вывода все, что мы ему отправляем.
def my_gen(): i = 0 while True: val = yield i print('Got inside generator:', val) i += 1 >>> g = my_gen() >>> next(g) 0 >>> g.send("hello") Got inside generator: hello 1 >>> g.send("world") Got inside generator: world 2
Обратите внимание, что первый раз нельзя посылать в генератор данные, пока мы не прокрутили его до первого yield. Нужно либо взывать next(g) или g.send(None) – это одно и тоже.
Не будет ошибкой отправлять данные генератору, который не получает их (нет использования значения конструкции yield). Например, нашему генератору fib() можно отравить все, что угодно, он просто проигнорирует.
throw() – бросить исключение внутри генератора. Исключение будет возбуждено из того выражение yield, где генератор последний раз остановился.
>>> g = my_gen() # my_gen из прошлого примера >>> g.throw(TypeError, 'my error') Traceback (most recent call last): File "", line 1, in File "", line 1, in my_gen TypeError: my error
close() – закрыть генератор. Бросает внутри генератора особое исключение GeneratorExit. Это исключение, даже если оно не обработано, не распространится в код, вызвавший close(). Но, если мы поймали это исключение внутри генератора, то после закрытия генератора нельзя уже делать yield, рискуя получить RuntimeError. Остальные виды исключений будут распространяться из генератора в код, его вызывающий. Попытка итерировать закрытый итератор приведет к исключению StopIteration (закрытый генератор – пустой итератор).
>>> g = my_gen() >>> next(g) 0 >>> next(g) Got inside generator: None 1 >>> g.close() >>> next(g) Traceback (most recent call last): File "", line 1, in StopIteration
Бонус
Как взять из итератора (в том числе из генератора) N первых значений?
Можно, конечно, написать свою функцию. Но зачем, если она уже есть в стандартном модуле itertools. Этот модуль содержит множество вспомогательных функций для работы с итераторами. Нам понадобится itertools.islice. Первый аргумент – итератор (ну или генератор), остальные три – как в range.
>>> list(itertools.islice(fib(), 10)) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] >>> list(itertools.islice(fib(), 10, 20, 2)) [55, 144, 377, 987, 2584]
В первом примере мы передаем в функцию itertools.islice наш генератор чисел Фибоначчи и число чисел, которые надо вычислить (в нашем случае – 10).
Мы также применяем функцию list, чтобы посмотреть список значений, потому что itertools.islice возвращает не спикок, а именно новый итератор, в котором будут только интересные нам значений из исходного итератора.
Во втором примеры аргументов 4 штуки. В этом случае второй аргумент – начальный номер = 10, третий – конечный номер = 20 – (не включительно), и четвертый – шаг = 2. (Очень похоже на range, не так ли?)
В чем отличие итератора от генераторного выражения?
В чем существенная разница? В переменной a находится итератор списка, пройдя поэлементно мы получим определенные значения, а в переменной b находится генераторное-выражение, пройдясь поэлементно по которому мы получим те же самые значения. Так в чем же отличие?
Отслеживать
задан 13 мар в 13:18
Anonymous Wizard Anonymous Wizard
115 7 7 бронзовых знаков
в том, что генератор не занимает память на все элементы, а выдает их по одному
13 мар в 13:19
Попробуйте сделать миллиард элементов, и вы очень быстро заметите, что совсем не одинаково. «причем тут список» — при том, что вы создаёте целый список в первом варианте
13 мар в 13:25
@AnonymousWizard не должен, он считает размер не списка, а ссылки на список
13 мар в 13:26
@AnonymousWizard не экономит, пока готовый объект существует и жрёт собой память
13 мар в 13:34
Однако стоит отметить, что итерирование по списку может быть быстрее, чем итерирование по генераторному выражению (но не стоит забывать, что создание списка тоже занимает время, так что где какой способ будет работать быстрее — вопрос неоднозначный)
13 мар в 13:34
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Итератор — любой объект, которые реализует метод __iter__ . Генератор — объект который вычисляет значение по мере необходимости, т.е. каждый раз когда его просят. Любой генератор это итератор, но не наоборот.
В вашем примере если заменить 10000 на 10**100 , то первый вариант займёт всю память и полностью остановит программу. Второй пример будет работать нормально, потому что он не будет считать все значения сразу, а только когда понадобится.
Отслеживать
ответ дан 13 мар в 13:35
19.6k 6 6 золотых знаков 22 22 серебряных знака 56 56 бронзовых знаков
- python
- python-3.x
- итераторы
- генераторы
-
Важное на Мете
Похожие
Подписаться на ленту
Лента вопроса
Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.
Дизайн сайта / логотип © 2023 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2023.11.15.1019
Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.
