Обзор генераторов и итераторов в Python

Введение:

Привет Путешественник,

Вы когда-нибудь обнаруживали, что вам нужно что-то делать снова, и снова, и снова, и снова...? Оно может очень быстро стать повторяющимся!

Хотя такое поведение может со временем привести к повторяющимся стрессовым травмам или даже к психическим расстройствам у среднего человека, компьютеры гораздо лучше подходят для выполнения повторяющихся задач! Парадигмы, с помощью которых программы имеют тенденцию обрабатывать повторяющееся выполнение (также известное как итерация), как правило, делятся на две отдельные категории:

  1. Определенная итерация — количество исполнений блока кода известно при инициализации цикла.
  2. Неопределенная итерация — количество выполнений блока кода не известно при инициализации цикла и продолжается до тех пор, пока не будет выполнено заданное условие.

Эти две классификации могут показаться вам более знакомыми как циклы for и while (соответственно).

Различия между этими двумя типами итераций присутствуют во многих языках программирования, от Perl до Ruby и C++ (и многих других!), но сегодня я хотел бы сосредоточить наше внимание на главном Объектно-ориентированный язык программирования, выпущенный в 1991 году, Python.

Как и многие другие языки программирования, Python предоставляет доступ к повторяющимся функциям. Однако, чтобы лучше понять, как это работает «за кулисами», нам нужно углубиться и узнать о…

Итераторы и метод __iter__

Так что же такое есть итератор?

Ну, согласно документации python3 итератор — это не что иное, как:

Объект, представляющий поток данных.

Достаточно легко, верно?

Суть в том, что итераторы — это объекты, как и большинство всего остального в Python.

Итераторы, используемые в сочетании с итерируемыми объектами (такими как списки, строки, кортежи, словари и т. д.), обеспечивают доступ к значениям в коллекции!

Хотя итераторы могут обращаться к элементам упорядоченных данных, стоит отметить, что итераторы Python также могут обращаться к элементам неупорядоченных данных (таких как наборы)! Это обеспечивает значительную степень свободы, что, пожалуй, лучше всего демонстрирует тот факт, что итераторы Python могут даже перебирать открытые файлы и читать строки файлов!

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

Как минимум, итераторы должны содержать два разных метода:

  1. __iter__() — возвращает сам объект итератора. Обычно это вызывается, когда вы пытаетесь использовать итератор для определенной итерации — в цикле for.
  2. __next__() — возвращает следующий элемент последовательности.

Включение этих двух методов в объект итератора фактически делает итератор самим итерируемым!

Вот пример того, как может выглядеть базовый итератор, если вы создадите его с нуля!

class exampleIterator:
    def __init__(self, start, end):
        self.present = start
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.present <= self.end:
            value = self.present
            self.present += 1
            return value
        else:
            raise StopIteration

В приведенном выше блоке кода класс exampleIterator принимает начальное и конечное значения. После того, как этот итератор пройдет через всю итерацию, к которой он был вызван, он вызовет исключение StopIteration.

Ниже приведен пример того же самого итератора при его вызове!

ex_iter = exampleIterator(1, 10)

for i in ex_iter:
    print(i)

# => 1,2,3,4,5,6,7,8,9,10

Итераторы — это очень круто, но есть еще кое-что, чем можно восхищаться!

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

Однако в их конструкции заложено несколько ограничений. Например, итераторы могут двигаться только в одном направлении, вперед, и теряют доступ к значениям, хранившимся ранее в его состоянии.

Хотя мы можем определять итераторы с нуля, существует специальный класс функций, который позволяет нам получить доступ к той же самой функциональности с гораздо большей простотой использования! Я, конечно, о…

Генераторы

Генераторы — это подмножество итераторов, которые объединяют большую часть работы по созданию пользовательского класса итератора! Хотя нельзя оспаривать тот факт, что вышеупомянутые пользовательские классы являются более гибкими в отношении поддержания состояния, генераторы имеют синтаксис, который обеспечивает гораздо большую ясность и простоту вашего кода!

Давайте посмотрим, как мы можем реорганизовать наш вышеприведенный класс exampleIterator с помощью функции-генератора!

def ex_generator(start, end):
    current = start
    while current < end:
        yield current
        current += 1

Вероятно, легко увидеть, что это очень похоже на обычную функцию Python, но в ней отсутствует ключевое слово return! Это, дорогой читатель, задумано!

Функции генератора при вызове создают итератор/объект генератора, который выполняет блок кода до тех пор, пока не будет выполнено ключевое слово yield. В этот момент значение ключевого слова yield «возвращается» до тех пор, пока объект генератора не будет вызван снова, после чего выполнение продолжается до тех пор, пока не будет выполнено ключевое слово yield!

Для сравнения: Обычные функции будут выполнять кодовый блок, возвращать значение, а затем избавляться от всех переменных внутри этой функции при выходе из кодового блока (за исключением замыкания, конечно). Функции-генераторы, однако, будут хранить ход выполнения итерации в состоянии, и к ним можно будет получить доступ после завершения выполнения функции-генератора с помощью встроенного метода next(), как показано ниже!

ex_gen = ex_generator(1, 10)

print(next(ex_gen)) #=> 1
print(next(ex_gen)) #=> 2
print(next(ex_gen)) #=> 3

Если это было недостаточно круто, функции-генераторы также более эффективны с точки зрения памяти, чем другие типы функций, по тем же причинам, по которым итераторы эффективны с точки зрения памяти! А именно, что они не хранят сразу все сгенерированные значения в памяти. Вместо этого они генерируют и возвращают значения по частям в соответствии с запросом ключевого слова next(), аналогично концепции итератора, рассмотренной выше!

В заключение…

Итераторы и генераторы обеспечивают чистый и всеобъемлющий способ взаимодействия с большими или маленькими наборами данных! Несмотря на то, что при создании пользовательского класса итератора для больших наборов данных возникают некоторые сложные синтаксические препятствия, эффективность использования памяти, которую обеспечивает даже мелкомасштабная реализация, более чем компенсирует любые препятствия, которые могут возникнуть на этом пути! Я надеюсь, что я победил некоторых из вас в любом случае.

Оставьте мне комментарий здесь или свяжитесь со мной в Твиттере, если я что-то пропустил!

и наконец,

Оставайся в безопасности, Путешественник.

Ресурсы:

Определенная итерация
Неопределенная итерация
Perl
C++
Ruby
Python
документация python3
методы dunder
__next__()
шаблон проектирования итераторов
наборы
генераторы
ленивая загрузка