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

Источники
  1. https://docs.python.org/3/library/stdtypes.html#generator-types

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

Поиск по сайту