Чем отличается итератор от генератора python
Перейти к содержимому

Чем отличается итератор от генератора python

  • автор:

В чем отличие итератора от генераторного выражения?

В чем существенная разница? В переменной a находится итератор списка, пройдя поэлементно мы получим определенные значения, а в переменной b находится генераторное-выражение, пройдясь поэлементно по которому мы получим те же самые значения. Так в чем же отличие?

Отслеживать
задан 13 мар 2023 в 13:18
Anonymous Wizard Anonymous Wizard
117 8 8 бронзовых знаков
в том, что генератор не занимает память на все элементы, а выдает их по одному
13 мар 2023 в 13:19

Попробуйте сделать миллиард элементов, и вы очень быстро заметите, что совсем не одинаково. «причем тут список» — при том, что вы создаёте целый список в первом варианте

13 мар 2023 в 13:25
@AnonymousWizard не должен, он считает размер не списка, а ссылки на список
13 мар 2023 в 13:26
@AnonymousWizard не экономит, пока готовый объект существует и жрёт собой память
13 мар 2023 в 13:34

Однако стоит отметить, что итерирование по списку может быть быстрее, чем итерирование по генераторному выражению (но не стоит забывать, что создание списка тоже занимает время, так что где какой способ будет работать быстрее — вопрос неоднозначный)

13 мар 2023 в 13:34

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

Итератор — любой объект, которые реализует метод __iter__ . Генератор — объект который вычисляет значение по мере необходимости, т.е. каждый раз когда его просят. Любой генератор это итератор, но не наоборот.

В вашем примере если заменить 10000 на 10**100 , то первый вариант займёт всю память и полностью остановит программу. Второй пример будет работать нормально, потому что он не будет считать все значения сразу, а только когда понадобится.

Отслеживать
ответ дан 13 мар 2023 в 13:35
19.7k 6 6 золотых знаков 22 22 серебряных знака 56 56 бронзовых знаков

  • python
  • python-3.x
  • итераторы
  • генераторы
    Важное на Мете
Похожие

Подписаться на ленту

Лента вопроса

Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.

Дизайн сайта / логотип © 2024 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2024.2.16.5008

Итераторы и генераторы

В предыдущих разделах мы встречали различные итерируемые объекты. Такие объекты можно использовать в цикле for , к ним можно применять оператор in . Разберемся в механизме итерирования. Классы стандартных контейнеров имеют метод __iter__ , возвращающий итератор:

l = [1, 2, 3] s = set(1, 2, 3) d = k: v for v, k in enumerate(['a', 'b', 'c'])> all(map(lambda x: hasattr(x, '__iter__'), [l, s, d])) # True type(l.__iter__()) # type(iter(l)) # itl = iter(l) for x in itl: print(x, end=' ') # 1 2 3 for x in itl: print(x, end=' ') # этот цикл не запустится, поскольку итератор позволяет # перебрать объекты только один раз its = iter(s) type(its) # # для получения следующего значения можно использовать функцию next. # Функция next вызывается при переборе значений в цикле for next(its) # 1 next(its) # 2 next(its) # 3 

Вызов метода __iter__ в данном случае аналогичен использованию функции iter . В обоих случаях мы получаем итератор. В итераторе должен быть определен метод __next__ , который возвращает следующее значение. Функция next вызывает метод __next__ .

Функция iter также вернет итератор, если вместо метода __iter__ реализован метод __getitem__ , позволяющий доступаться к элементам контейнера по индексу.

Чтобы сделать тип итерируемым, достаточно реализовать метод __iter__ или метод __getitem__ . Чтобы создать тип-итератор, необходимо определить в нем метод __next__ и метод __iter__ . Последний должен возвращать сам объект ( self ). Таким образом, итераторы сами являются итерируемыми объектами.

Генераторы

Другой встречавшийся нам итерируемый объект — результат вызова функции range :

import sys for x in range(5): print(x, end=' ') # 1 2 3 4 5 rng = range(10**6) type(rng) # sys.getsizeof(rng) # 48 — размер объекта в байтах l = list(rng) sys.getsizeof(l) # 9000120 

Функция range возвращает объект типа range , который занимает фиксированный объем памяти. Эффективное использование памяти достигается благодаря тому, что каждое значение вычисляется в реальном времени, позволяя избавиться от необходимости хранить все значения в памяти. Такое поведение можно реализовать с помощью генераторов. Напишем свою версию range :

def my_range(start, stop=None, step=None): if step is None: step = 1 if stop is None: start, stop = 0, start v = start while v  stop: yield v v += step myrng = my_range(5) type(myrng) # sys.getsizeof(myrng) # 128 for x in myrng: print(x, end=' ') # 0 1 2 3 4 

Размер нашего генератора больше, чем объекта range , но он также не зависит от значений аргументов функции my_range . Ключевым элементом функции-генератора my_range является строка

yield v 

При достижении этой строки генератор запоминает свое состояние, прерывает работу до следующего вызова метода __next__ и возвращает текущее значение переменной v .

Создадим более интересный генератор, который вычисляет последовательность Рекамана:

def rekaman(stop): n = 0 prev = 0 visited = set() while n  stop: if n == 0: yield 0 prev = 0 elif prev - n > 0 and prev - n not in visited: yield prev - n prev = prev - n else: yield prev + n prev = prev + n n += 1 visited.add(prev) for r in rekaman(100): print(r, end=' ') # 0 1 3 6 2 7 13 20 12 21 11 22 10 23 9 24 8 25 43 62 42 63 41 18 42 # 17 43 16 44 15 45 14 46 79 113 78 114 77 39 78 38 79 37 80 36 81 35 # 82 34 83 33 84 32 85 31 86 30 87 29 88 28 89 27 90 26 91 157 224 # 156 225 155 226 154 227 153 228 152 75 153 74 154 73 155 72 156 71 # 157 70 158 69 159 68 160 67 161 66 162 65 163 64 

Функции-генераторы являются удобным и гибким инструментом языка python. Иметь этот инструмент в арсенале очень полезно.

Альтернативный способ создания генераторов предоставляют генераторные выражения:

l1 = [x**2 for x in range(100)] # списковое включение g1 = (x**2 for x in range(100)) # генераторное выражение type(l1) # type(g1) # 

Генераторные выражения имеют синтаксис близкий к списковому включению. Отличие состоит только в использовании круглых скобок. В частности, в генераторных выражениях можно использовать условный оператор:

g = (x for x in range(20) if x % 3) for val in g: print(val, end=' ') # 1 2 4 5 7 8 10 11 13 14 16 17 19 

Если необходима более сложная логика, то следует вернуться к использованию функций-генераторов.

Функция map также возвращает итератор (генератор):

l1 = [x**2 for x in range(100)] g1 = (x**2 for x in range(100)) m1 = map(lambda x: x**2, range(100)) type(l1) # type(g1) # type(m1) # sys.getsizeof(l1) # 920 sys.getsizeof(g1) # 128 sys.getsizeof(m1) # 64 

Если преобразованную коллекцию необходимо обходить несколько раз или надо сохранить все её элементы, то генератор можно преобразовать в список:

m2 = list(map(lambda x: x**2, range(100))) 

С помощью генераторов удобно создавать итераторы. Вернемся к примеру из одного из предыдущих разделов, в котором мы реализовали класс релятивистских векторов:

from typing import NamedTuple class FourVector(NamedTuple): t: float r: list fv = FourVector(1, [0.3, 0.4, 0.0]) for x in fv: print(x, end=' ') # 1 [0.3, 0.4, 0.0] 

Объекты класса FourVector является итерируемыми, поскольку класс наследуется от типа NamedTuple . Давайте изменим правило итерирования:

import itertools class FourVector(NamedTuple): t: float r: list def __iter__(self): if isinstance(self.r, list): return itertools.chain([self.t], self.r) return (x for x in [self.t, self.r]) fv1 = FourVector(1, [0.3, 0.4, 0.0]) fv2 = FourVector(1, 0.5) for x in fv1: print(x, end=' ') # 1 0.3 0.4 0.0 for x in fv2: print(x, end=' ') # 1 0.5 

Мы не могли использовать спиское включение вместо генераторного выражения в методе __iter__ , поскольку, в отличие от типа generator , тип list не является итератором. Функция itertools.chain принимает несколько итераторов или итерируемых коллекций и создаёт генератор, который последовательно проходит по всем их элементам.

Резюме

Итерируемый объект должен иметь реализацию хотя бы одного из методов __iter__ и __getitem__ . Объект-итератор должен иметь реализацию метода __next__ .

Генераторы являются инструментом для вычисления элементов последовательностей “на лету” и позволяют избежать хранения всех элементов последовательности в памяти. Генераторные выражения позволяют создавать генераторы с простой логикой. Генераторы удобно использовать при реализации итераторов.

Источники

  • The Python Wiki: Generators
  • Как работает yield
  • How to make an iterator in Python
  • PEP 289 — Generator Expressions
  • Сопрограммы в Python

AlexKorablev.ru

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 бережет: экономим оперативную память

Часто Data Scientist и python-программист сталкиваются с задачей чтения больших объемов данных (Big Data). Чтобы при этом компьютер не зависал, помогут специальные объекты: итератор (iterator) и генератор (generator). В этой статье рассмотрим, что это такое, зачем и как их создавать, а также каким образом они берегут оперативную память.

Iterable, iterator, generator — базовые концепты Python

В предыдущей статье мы затрагивали тему итерируемых структур данных – последовательностей. На практике последовательность соответствует понятию Iterable – объекту-контейнеру, над которым можно провести итерирование. В основном, он используется в конструкции цикла for … in. Списки, словари, множества, массив байтов (bytearray), строки и прочие подобные структуры данных – все это объекты iterable.
У объекта iterable есть метод __iter__() , который возвращает Iterator. Iterator — это объект, реализующий метод __next__() , возвращающий следующий элемент контейнера. Допустим, у нас есть список чисел, и мы хотим пройтись по нему:

nums = [1, 2, 3, 4] for num in nums: print(num)

В данном цикле конструкция in nums вызывает метод __iter__() , который возвращает итератор. А num – это возвращаемый методом __next__() элемент этого итератора. Итерирование прекратится в тот момент, когда возникнет исключение StopIteration, о котором мы расскажем чуть позже.
Объект Generator – это разновидность итератора, который можно проитерировать лишь один раз. Это означает, что второй раз использовать цикл for … in для генератора уже невозможно. Чтобы получить генератор используется ключевое слово yield. Разберем все поподробней.
Что такое итератор: пример
Реализуем обратный счетчик CountDown, который ведет отчет от заданного числа до 0. Для этого нам понадобятся вышерассмотренные методы __iter__() и __next__() . Первый из них возвращает сам объект, а второй – элемент счетчика:

class CountDown: def __init__(self, start): self.count = start + 1 def __iter__(self): return self def __next__(self): self.count -= 1 if self.count < 0: raise StopIteration return self.count

Здесь в конструкторе __init__() добавляется единица, чтобы вывести еще стартовое число. Инициализируем в качестве стартового значения число 5:

>>> counter = CountDown(5) >>> for i in counter: . print(i) 5 4 3 2 1 0

После того как count станет меньше нуля, итерирование прекращается, так как возникает исключение StopIteration.

Как работает генератор: примеры кода

Как сказано в документации Python [1], генератор — это удобный способ реализовать протокол итератора, так как нет необходимости создавать классы. Представим тот же CountDown в виде генератора:

def countdown(start): count = start + 1 while count > 0: yield count count -= 1

С тем же результатом:

>>> counter = countdown(5) >>> for i in counter: . print(i) 5 4 3 2 1 0

Такая функция ведет себя как обычный итератор, а yield возвращает объект генератора. Ключевое слово yield можно сравнить с return, но yield сохраняет текущее состояние локальных переменных. Следующее обращение к генератору вызывает метод __next__() , который возобновляет работу строк, стоящих после yield, с сохранёнными локальными переменными. Работа будет выполняться до появления ключевого слова yield. В нашем примере всего один yield, находящийся в цикле.

Генераторы в классах

Подчеркнем, в классах тоже можно использовать генератор:

class Countdown: def __init__(self, start): self.count = start def __iter__(self): while self.count > -1: yield self.count self.count -= 1

Так, вместо метода __next__() используется генератор. Такая запись намного короче.
Поясним, почему генераторы и итераторы так эффективны.

В чем польза генераторов Python

Вначале статьи мы упомянули, что последовательности – это iterable; а списки – это последовательности. При этом в примерах Python-кода не создавали ни списки, ни множества. Сам yield возвращал только одно число из последовательности! Отсюда и эффективность – не нужно хранить в памяти всю последовательность, достаточно лишь текущего значения.
Как мы разбирали в прошлой статье, списки в Python можно создавать в одну строчку, используя конструкцию List comprehension. С генераторами тоже можно проделывать подобное:

counter = (i for i in range(5,-1,-1))

В отличие от List comprehension, здесь используются круглые скобки. Также можно проверить тип созданного объекта:

>>> type(counter) generator >>> for i in counter: . print(i) 5 4 3 2 1 0

Теперь вы знаете, что такое итераторы и генераторы, и как они помогают эффективно читать большие данные, включая те, что не помещаются в оперативную память. Однако, стоит помнить, что прочитать их можно только один раз.

Освоить все тонкости практической работы с большими данными на Python, помогут наши специализированные курсы по Python в лицензированном учебном центре обучения и повышения квалификации ИТ-специалистов в Москве.

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

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