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

Сегодня мы узнаем всё про итераторы и генераторы в Python. Поговорим о том, чем итераторы отличаются от итерируемых объектов и генераторов. Также разберем, как их создать с помощью __iter__, __next__ и itertools.

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

Это означает, что, если вы когда-либо использовали циклы для итерации или прогона значений в контейнере, вы использовали итератор.

В данном руководстве вы узнаете больше об итераторах в Python. Если говорить более конкретно, то:

  • Сначала мы подробно рассмотрим итераторы, чтобы понять, для чего они нужны и когда их следует использовать
  • Затем мы рассмотрим итерируемые объекты и узнаем, чем они отличаются от итераторов
  • Далее познакомимся с контейнерами и посмотрим, как они используют концепцию итераторов
  • Затем мы посмотрим на Itertools в действии
  • И наконец, мы разберем генераторы и генераторные выражения

Что ж, давайте приступать!

Итераторы

Итератор — это объект, реализующий протокол итератора (без паники!). Протокол итератора — это не что иное, как определенный класс в Python, который также имеет метод __next()__. Это означает, что каждый раз, когда вы запрашиваете следующее значение, итератор знает, как его вычислить. Он хранит информацию о текущем состоянии итерируемого объекта, над которым он работает.

Итератор вызывает следующее значение, когда вы вызываете для него метод next(). Объект, использующий метод __next__(), в конечном счете является итератором.

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

В Python есть несколько встроенных объектов, которые реализуют протокол итератора. Вы, должно быть, видели некоторые из них раньше: списки, кортежи, строки, словари и даже файлы. В Python также много итераторов, все функции itertools возвращают итераторы. Что такое itertools, мы рассмотрим чуть позже.

[python_ad_block]

Итерируемые объекты

По словам Винсента Дриссена из nvie.com, «итерируемый объект — это любой объект (не обязательно структура данных), способный возвращать итератор». Его основная цель – вернуть все его элементы. Итерируемые объекты могут представлять как конечный, так и бесконечный источник данных. Они прямо или косвенно определяют два метода:

  • __iter__(), который должен возвращать объект итератора,
  • __next()__ с помощью вызываемого им итератора.

Примечание. Часто итерируемые классы реализуют как __iter__(), так и __next__() в одном классе. При этом __iter__() возвращает себя, что делает класс _iterable_ одновременно итерируемым объектом и собственным итератором. Однако совершенно нормально возвращать другой объект в качестве итератора.

Между итераторами и итерируемыми объектами есть большая разница. Вот пример:

a_set = {1, 2, 3}
b_iterator = iter(a_set)
next(b_iterator)
type(a_set)
type(b_iterator)

В примере a_set — это итерируемый объект (множество), а b_iterator — итератор. Оба они являются разными типами данных в Python.

Хотите познакомиться со внутренней работой итератора и узнать, как он создает следующую последовательность? Давайте создадим итератор, который возвращает серию чисел. К примеру, это можно сделать так:

class Series(object):
    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

n_list = Series(1,10)    
print(list(n_list))

__iter__ возвращает сам объект итератора, а метод __next__ возвращает следующее значение из итератора. Если больше нет элементов для возврата, возникает исключение StopIteration.

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

Контейнеры

Контейнеры — это объекты, содержащие значения данных. Они поддерживают тесты на членство, что означает, что вы можете проверить, существует ли значение в контейнере. Кроме того, контейнеры являются итерируемыми объектами. Списки, множества, словари, кортежи и строки — все это контейнеры. Но есть и другие итерируемые объекты, такие как открытые файлы и открытые сокеты. К примеру, вы можете выполнить тесты на членство в контейнерах следующим образом:

if 1 in [1,2,3]:
    print('List')

if 4 not in {1,2,3}:
    print('Tuple')

if 'apple' in 'pineapple':
    print('String') #string contains all its substrings
# List
# Tuple
# String

Модуль Itertools

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

Давайте рассмотрим некоторые интересные вещи, которые вы можете сделать с функцией count из модуля itertools. К примеру, можно делать следующее:

from itertools import count
sequence = count(start=0, step=1)
while(next(sequence) <= 10):
    print(next(sequence))
# 1
# 3
# 5
# 7
# 9
# 11
from itertools import cycle
dessert = cycle(['Icecream','Cake'])
count = 0
while(count != 4):
    print('Q. What do we have for dessert? A: ' + next(dessert))
    count+=1
# Q. What do we have for dessert? A: Icecream
# Q. What do we have for dessert? A: Cake
# Q. What do we have for dessert? A: Icecream
# Q. What do we have for dessert? A: Cake

Вы можете узнать больше об itertools в документации.

Генераторы

Генератор — элегантный брат итератора. Он позволяет вам создавать итераторы с гораздо более простым синтаксисом, где вам не нужно писать классы с методами __iter__() и __next__().

Помните пример с итератором, который мы рассматривали ранее? Попробуем переписать код, используя концепцию генераторов:

def series_generator(low, high):
    while low <= high:
       yield low
       low += 1

n_list = []
for num in series_generator(1,10):
    n_list.append(num)

print(n_list)
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Волшебное слово у генераторов — yield. В функции series_generator нет оператора возврата return. Возвращаемое значение функции на самом деле будет генератором.

Внутри цикла while, когда выполнение достигает оператора yield, возвращается значение low и работа генератора приостанавливается. Во время второго следующего вызова генератор возобновляет работу со значения, на котором он остановился ранее, и увеличивает это значение на единицу. Он продолжает цикл while и снова приходит к оператору yield.

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

Подробнее об yield и return можно прочитать в статье «Сравнение операторов yield и return в Python (с примерами)».

Совет. За генераторами и итераторами стоит концепция ленивой фабрики. Это означает, что, пока вы не запросите у них значение, они простаивают. Когда вы запрашиваете значение, они вам его выдают, после чего снова становятся бездействующим. Это хороший подход для работы с большим количеством данных. Если вам не нужны все данные сразу и, следовательно, нет необходимости загружать все данные в память, вы можете использовать генератор или итератор, который будет передавать вам по фрагменту данных за раз.

Типы генераторов

В Python генераторы могут быть двух разных типов: функции-генераторы и генераторные выражения.

Генераторная функция — это функция, в теле которой появляется ключевое слово yield. Вы уже видели пример с функцией series_generator. Это означает, что появления ключевого слова yield достаточно, чтобы сделать функцию функцией-генератором.

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

Давайте посмотрим, что это означает:

squares = (x * x for x in range(1,10))
print(type(squares))
print(list(squares))
# <class 'generator'>
# [1, 4, 9, 16, 25, 36, 49, 64, 81]

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

Где вы можете вставить генераторы в свой код?

Совет. Найдите места в коде, где вы делаете, к примеру, следующее:

def some_function():
    result = []
    for ... in ...:
        result.append(x)
    return result

И измените код:

def iterate_over():
    for ... in ...:
        yield x

Заключение

Итераторы — мощный и полезный инструмент в Python. Однако не все хорошо знакомы с его мельчайшими подробностями. В этой статье мы разобрали, что собой представляют итераторы и генераторы в Python, чем они отличаются и как используются. Надеемся данная статья была вам полезна и теперь вы понимаете концепцию итераторов немного лучше! Успехов в написании кода!

Перевод статьи «Python Iterator Tutorial».