Часто 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 в лицензированном учебном центре обучения и повышения квалификации ИТ-специалистов в Москве.