Итераторы и итерации в Python: выполнение эффективных итераций

Оглавление

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

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

В этом учебнике вы узнаете, как:

  • Создавать итераторы с помощью протокола итераторов в Python
  • Понимать различия между итераторами и итерабельными переменными
  • Работайте с итераторами и итерабельными таблицами в коде Python
  • Использование функций генераторов и оператора yield для создания генераторов итераторов
  • Создавайте собственные iterables, используя различные техники, например, протокол iterable protocol
  • Используйте модуль asyncio и ключевые слова await и async для создания асинхронных итераторов

Прежде чем углубиться в эти темы, вы должны быть знакомы с некоторыми основными понятиями, такими как циклы и итерации, объектно-ориентированное программирование, наследование, специальные методы и асинхронное программирование в Python.

Бесплатный пример кода: Нажмите здесь, чтобы скачать бесплатный пример кода, который покажет вам, как использовать и создавать итераторы и итерабельные таблицы для более эффективной обработки данных.

Понимание итерации в Python

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

  • Повторяя целевой код столько раз, сколько вам нужно, в последовательности
  • Помещение целевого кода в цикл, который выполняется столько раз, сколько вам нужно

Первый подход имеет несколько недостатков. Самым неприятным является сам повторяющийся код, который трудно поддерживать и который не масштабируется. Например, следующий код выведет на экран приветствие три раза:

# greeting.py

print("Hello!")
print("Hello!")
print("Hello!")

Если вы запустите этот скрипт, то получите 'Hello!', напечатанный на экране три раза. Этот код работает. Однако, что если вы решите обновить свой код, чтобы печатать 'Hello, World!' вместо просто 'Hello!'? В этом случае вам придется обновлять приветствие три раза, что является бременем для обслуживания.

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

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

>>>

>>> times = 0

>>> while times < 3:
...     print("Hello!")
...     times += 1
...
Hello!
Hello!
Hello!

Этот цикл while выполняется до тех пор, пока условие завершения цикла (times < 3) остается истинным. На каждой итерации цикл печатает ваше приветствие и увеличивает управляющую переменную times. Теперь, если вы решите обновить сообщение, вам нужно будет изменить всего одну строку, что сделает ваш код гораздо более удобным для обслуживания.

Цикл whilePython поддерживает так называемую бесконечную итерацию, что означает выполнение одного и того же блока кода снова и снова, потенциально неопределенное количество раз.

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

Для запуска такой итерации обычно используется цикл for в Python:

>>>

>>> numbers = [1, 2, 3, 4, 5]

>>> for number in numbers:
...     print(number)
...
1
2
3
4
5

В этом примере numbers list представляет собой поток данных, который вы будете называть iterable, потому что вы можете выполнять итерации над ним, как вы узнаете позже в этом учебнике. Цикл перебирает каждое значение в numbers и выводит его на экран.

Когда вы используете цикл while или for, чтобы повторить часть кода несколько раз, вы фактически выполняете итерацию. Это название, данное самому процессу.

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

Знакомство с итераторами Python

Итераторы были добавлены в Python

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

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

Что такое итератор в Python?

В Python итератор - это объект, позволяющий выполнять итерацию по коллекциям данных, таким как списки, кортежи, словари и наборы.

Итераторы

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

Итераторы отвечают за два основных действия:

  1. Возврат данных из потока или контейнера по одному элементу за раз
  2. Отслеживание текущих и посещенных элементов

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

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

Что такое протокол итератора Python?

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

Method Description
.__iter__() Called to initialize the iterator. It must return an iterator object.
.__next__() Called to iterate over the iterator. It must return the next value in the data stream.

Метод .__iter__() итератора обычно возвращает self, который содержит ссылку на текущий объект: сам итератор. Этот метод прост в написании и в большинстве случаев выглядит примерно так:

def __iter__(self):
    return self

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

Метод .__next__() будет немного сложнее в зависимости от того, что вы пытаетесь сделать с вашим итератором. Этот метод должен возвращать следующий элемент из потока данных. Он также должен вызывать исключение StopIteration, когда в потоке данных больше нет элементов. Это исключение приведет к завершению итерации. Правильно. Итераторы используют исключения для управления потоком данных.

Когда использовать итератор в Python?

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

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

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

Создание различных типов итераторов

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

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

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

Примечание: Второй и третий типы итераторов могут навести на мысль о некоторых приемах, похожих на операции mapping и filtering из функционального программирования.

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

Получение исходных данных

Итак, пришло время научиться писать собственные итераторы в Python. В качестве первого примера вы напишите классический итератор SequenceIterator. Он будет принимать в качестве аргумента тип данных sequence и выдавать свои элементы по требованию.

Запустите ваш любимый редактор кода или IDE и создайте следующий файл:

# sequence_iter.py

class SequenceIterator:
    def __init__(self, sequence):
        self._sequence = sequence
        self._index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._index < len(self._sequence):
            item = self._sequence[self._index]
            self._index += 1
            return item
        else:
            raise StopIteration

Ваш SequenceIterator будет принимать последовательность значений во время инстанцирования. Класс initializer, .__init__(), позаботится о создании соответствующих атрибутов экземпляра , включая входную последовательность и атрибут ._index. Вы будете использовать этот последний атрибут как удобный способ пройтись по последовательности, используя индексы.

Примечание: Вы заметите, что атрибуты экземпляра в этом и следующем примерах являются непубличными атрибутами, имена которых начинаются с символа подчеркивания (_). Это потому, что вам не нужен прямой доступ к этим атрибутам извне класса.

Метод .__iter__() делает только одно: возвращает текущий объект, self. В данном случае self - это сам итератор, что подразумевает наличие у него метода .__next__().

Наконец, у вас есть метод .__next__(). Внутри него вы определяете условный оператор для проверки того, что текущий индекс меньше количества элементов во входной последовательности. Эта проверка позволяет остановить итерацию, когда данные закончатся, в этом случае условие else вызовет исключение StopIteration.

В пункте if вы захватываете текущий элемент из исходной входной последовательности, используя его индекс. Затем вы увеличиваете атрибут ._index с помощью присваивания augmented assignment. Это действие позволяет вам двигаться вперед в итерации, в то время как вы отслеживаете посещенные элементы. Последним шагом является возврат текущего элемента.

Вот как работает ваш итератор, когда вы используете его в цикле for:

>>>

>>> from sequence_iter import SequenceIterator

>>> for item in SequenceIterator([1, 2, 3, 4]):
...     print(item)
...
1
2
3
4

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

Примечание: Вы можете создать итератор, который не определяет метод .__iter__(), в этом случае его метод .__next__() будет работать. Однако вы должны реализовать .__iter__(), если хотите, чтобы ваш итератор работал в циклах for. Этот цикл всегда вызывает .__iter__() для инициализации итератора.

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

>>>

>>> sequence = SequenceIterator([1, 2, 3, 4])

>>> # Get an iterator over the data
>>> iterator = sequence.__iter__()
>>> while True:
...     try:
...         # Retrieve the next item
...         item = iterator.__next__()
...     except StopIteration:
...         break
...     else:
...         # The loop's code block goes here...
...         print(item)
...
1
2
3
4

После инстанцирования SequenceIterator код подготавливает объект sequence к итерации, вызывая его метод .__iter__(). Этот метод возвращает реальный объект итератора. Затем цикл многократно вызывает .__next__() на итераторе, чтобы получить из него значения.

Примечание: Не следует использовать .__iter__() и .__next__() непосредственно в коде. Вместо этого следует использовать встроенные функции iter() и next(), которые возвращаются к вызову соответствующих специальных методов.

Когда вызов .__next__() вызывает исключение StopIteration, вы выходите из цикла. В этом примере вызов print() под пунктом else блока try представляет собой блок кода в обычном цикле for. Как видите, конструкция цикла for является своего рода синтаксическим сахаром для куска кода, подобного приведенному выше.

Преобразование входных данных

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

# square_iter.py

class SquareIterator:
    def __init__(self, sequence):
        self._sequence = sequence
        self._index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._index < len(self._sequence):
            square = self._sequence[self._index] ** 2
            self._index += 1
            return square
        else:
            raise StopIteration

Первая часть этого класса SquareIterator такая же, как и у вашего класса SequenceIterator. Метод .__next__() также довольно похож. Единственное отличие заключается в том, что перед возвратом текущего элемента метод вычисляет его квадратное значение. Это вычисление выполняет преобразование для каждой точки данных.

Вот как класс будет работать на практике:

>>>

>>> from square_iter import SquareIterator

>>> for square in SquareIterator([1, 2, 3, 4, 5]):
...     print(square)
...
1
4
9
16
25

Использование экземпляра SquareIterator в цикле for позволяет перебирать квадратные значения исходных входных значений.

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

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

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

Генерирование новых данных

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

# fib_iter.py

class FibonacciIterator:
    def __init__(self, stop=10):
        self._stop = stop
        self._index = 0
        self._current = 0
        self._next = 1

    def __iter__(self):
        return self

    def __next__(self):
        if self._index < self._stop:
            self._index += 1
            fib_number = self._current
            self._current, self._next = (
                self._next,
                self._current + self._next,
            )
            return fib_number
        else:
            raise StopIteration

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

Вы начинаете этот метод с условия, которое проверяет, не достиг ли текущий индекс последовательности значения ._stop, в этом случае вы увеличиваете текущий индекс для управления процессом итерации. Затем вычисляется число Фибоначчи, соответствующее текущему индексу, и результат возвращается вызывающему .__next__().

Когда ._index вырастает до значения ._stop, вы вызываете сигнал StopIteration, который завершает процесс итерации. Обратите внимание, что вы должны указать значение stop, когда вызываете конструктор класса для создания нового экземпляра. Аргумент stop по умолчанию равен 10, что означает, что класс будет генерировать десять чисел Фибоначчи, если вы создадите экземпляр без аргументов.

Вот как вы можете использовать этот FibonacciIterator класс в своем коде:

>>>

>>> from fib_iter import FibonacciIterator

>>> for fib_number in FibonacciIterator():
...     print(fib_number)
...
0
1
1
2
3
5
8
13
21
34

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

Кодирование потенциально бесконечных итераторов

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

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

# inf_fib.py

class FibonacciInfIterator:
    def __init__(self):
        self._index = 0
        self._current = 0
        self._next = 1

    def __iter__(self):
        return self

    def __next__(self):
        self._index += 1
        self._current, self._next = (self._next, self._current + self._next)
        return self._current

Самой важной деталью в этом примере является то, что .__next__() никогда не вызывает исключения StopIteration. Этот факт превращает экземпляры данного класса в потенциально бесконечные итераторы, которые будут выдавать значения вечно, если использовать класс в for цикле.

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

If you’re working in a Python interactive REPL, then you can press the Ctrl+C key combination, which raises a KeyboardInterrupt exception and terminates the loop.

Чтобы проверить, работает ли ваш FibonacciInfIterator как ожидалось, выполните следующий цикл. Но помните, что это будет бесконечный цикл:

>>>

>>> from inf_fib import FibonacciInfIterator

>>> for fib_number in FibonacciInfIterator():
...     print(fib_number)
...
0
1
1
2
3
5
8
13
21
34
KeyboardInterrupt
Traceback (most recent call last):
    ...

When you run this loop in your Python interactive session, you’ll notice that the loop prints numbers from the Fibonacci sequence without stopping. To stop the loops, go ahead and press Ctrl+C.

Как вы убедились на этом примере, бесконечные итераторы типа FibonacciInfIterator заставят циклы for работать бесконечно. Они также заставят функции, принимающие итераторы, такие как sum(), max(), min(), map() и filter(), никогда не возвращаться. Поэтому будьте осторожны при использовании бесконечных итераторов в коде, так как вы можете привести к зависанию вашего кода.

Inheriting From collections.abc.Iterator

Модуль collections.abc включает абстрактный базовый класс (ABC) под названием Iterator. Вы можете использовать этот ABC для быстрого создания своих собственных итераторов. Этот класс предоставляет базовые реализации для метода .__iter__().

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

Посмотрите следующий пример, в котором вы изменяете свой SequenceIterator класс для использования Iterator ABC:

# sequence_iter.py

from collections.abc import Iterator

class SequenceIterator(Iterator):
    def __init__(self, sequence):
        self._sequence = sequence
        self._index = 0

    def __next__(self):
        if self._index < len(self._sequence):
            item = self._sequence[self._index]
            self._index += 1
            return item
        else:
            raise StopIteration

Если вы наследуете от Iterator, то вам не нужно писать метод .__iter__(), поскольку суперкласс уже предоставляет его стандартную реализацию. Однако вам придется написать свой собственный метод .__next__(), потому что родительский класс не предоставляет рабочей реализации.

Вот как работает этот класс:

>>>

>>> from sequence_iter import SequenceIterator

>>> for number in SequenceIterator([1, 2, 3, 4]):
...     print(number)
...
1
2
3
4

Как вы видите, эта новая версия SequenceIterator работает так же, как и ваша первоначальная версия. Однако на этот раз вам не пришлось кодировать метод .__iter__(). Ваш класс унаследовал этот метод от Iterator.

Функции, унаследованные от Iterator ABC, полезны при работе с иерархиями классов. Они избавят вас от лишней работы и головной боли.

Создание генератора итераторов

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

Примечание: В Python обычно используется термин генераторы для общего обозначения двух отдельных понятий: генераторной функции и генераторного итератора. Функция-генератор - это функция, которую вы определяете с помощью оператора yield. Итератор генератора - это то, что возвращает эта функция.

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

Создание функций генератора

Чтобы создать функцию-генератор, вы должны использовать ключевое слово yield для получения значений по одному. Вот как можно написать функцию-генератор, которая возвращает итератор, эквивалентный вашему классу SequenceIterator:

>>>

>>> def sequence_generator(sequence):
...     for item in sequence:
...         yield item
...

>>> sequence_generator([1, 2, 3, 4])
<generator object sequence_generator at 0x108cb6260>

>>> for number in sequence_generator([1, 2, 3, 4]):
...     print(number)
...
1
2
3
4

В цикле sequence_generator() вы принимаете последовательность значений в качестве аргумента. Затем вы перебираете эту последовательность с помощью цикла for. На каждой итерации цикл выдает текущий элемент с помощью ключевого слова yield. Эта логика затем упаковывается в объект генератора итератора, который автоматически поддерживает протокол итератора.

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

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

Использование выражений генератора для создания итераторов

Если вам нравятся генераторные функции, то вам понравятся генераторные выражения. Это особые типы выражений, которые возвращают итераторы генераторов. Синтаксис выражения-генератора почти такой же, как и синтаксис выражения для понимания списка. Вам нужно только превратить квадратные скобки ([]) в круглые скобки:

>>>

>>> [item for item in [1, 2, 3, 4]]  # List comprehension
[1, 2, 3, 4]

>>> (item for item in [1, 2, 3, 4])  # Generator expression
<generator object <genexpr> at 0x7f55962bef60>

>>> generator_expression = (item for item in [1, 2, 3, 4])
>>> for item in generator_expression:
...     print(item)
...
1
2
3
4

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

Изучение различных типов генераторов итераторов

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

Однако, как следует из их названия, генераторы могут генерировать потоки данных. Для этого генераторы могут принимать или не принимать входные данные. Как и итераторы на основе классов, генераторы позволяют вам:

  • Выдать входные данные как есть
  • Преобразовать входные данные и выдать поток преобразованных данных
  • Генерировать новый поток данных из известного вычисления

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

>>>

>>> def square_generator(sequence):
...     for item in sequence:
...         yield item**2
...

>>> for square in square_generator([1, 2, 3, 4, 5]):
...     print(square)
...
1
4
9
16
25

Эта функция square_generator() берет последовательность и вычисляет квадратное значение каждого из ее элементов. Оператор yield выдает квадратные элементы по запросу. Когда вы вызываете функцию, вы получаете итератор генератора, который генерирует квадратные значения из исходных входных данных.

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

>>>

>>> def fibonacci_generator(stop=10):
...     current_fib, next_fib = 0, 1
...     for _ in range(0, stop):
...         fib_number = current_fib
...         current_fib, next_fib = (
...            next_fib, current_fib + next_fib
...         )
...         yield fib_number
...

>>> list(fibonacci_generator())
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
>>> list(fibonacci_generator(15))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]

Эта функциональная версия вашего класса FibonacciIterator работает, как и ожидалось, производя числа Фибоначчи по запросу. Обратите внимание, как вы упростили код, превратив класс итератора в функцию генератора. Наконец, для отображения фактических данных вы вызвали list() с итератором в качестве аргумента. Эти вызовы неявно потребляют итераторы, возвращая списки чисел.

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

def fibonacci_generator(stop=10):
    current_fib, next_fib = 0, 1
    index = 0
    while True:
        if index == stop:
            return
        index += 1
        fib_number = current_fib
        current_fib, next_fib = next_fib, current_fib + next_fib
        yield fib_number

В этой версии fibonacci_generator() для выполнения итерации используется цикл while. Цикл проверяет индекс на каждой итерации и возвращается, когда индекс достигнет значения stop.

Вы будете использовать оператор return внутри функции генератора, чтобы явно указать, что генератор завершен. Оператор return заставит генератор выдать сообщение StopIteration.

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

Обработка данных в памяти с помощью итераторов

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

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

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

Возврат итераторов вместо контейнерных типов

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

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

>>>

>>> def square_list(sequence):
...     squares = []
...     for item in sequence:
...         squares.append(item**2)
...     return squares
...

>>> numbers = [1, 2, 3, 4, 5]
>>> square_list(numbers)
[1, 4, 9, 16, 25]

В этом примере у вас есть два объекта списка: исходная последовательность numbers и список квадратных значений, который получается в результате вызова square_list(). В этом случае входные данные достаточно малы. Однако если вы начнете с огромного списка значений в качестве входных данных, то вы будете использовать большой объем памяти для хранения исходного и результирующего списков.

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

Аналогично, выражения-генераторы более эффективны с точки зрения памяти, чем выражения-понятия. Понятия создают объекты-контейнеры, а выражения-генераторы возвращают итераторы, которые создают элементы по мере необходимости.

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

Создание конвейера обработки данных с помощью генераторов итераторов

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

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

Вот ваш набор индивидуальных функций генератора:

# math_pipeline.py

def to_square(numbers):
    return (number**2 for number in numbers)

def to_cube(numbers):
    return (number**3 for number in numbers)

def to_even(numbers):
    return (number for number in numbers if number % 2 == 0)

def to_odd(numbers):
    return (number for number in numbers if number % 2 != 0)

def to_string(numbers):
    return (str(number) for number in numbers)

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

Вот как можно комбинировать некоторые из этих функций генератора для создания различных конвейеров обработки данных:

>>>

>>> import math_pipeline as mpl

>>> sample = range(20)

>>> list(mpl.to_string(mpl.to_square(mpl.to_even(range(20)))))
['0', '4', '16', '36', '64', '100', '144', '196', '256', '324']

>>> list(mpl.to_string(mpl.to_cube(mpl.to_odd(range(20)))))
['1', '27', '125', '343', '729', '1331', '2197', '3375', '4913', '6859']

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

Понимание некоторых ограничений итераторов Python

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

  • Генерируют и выдают поток данных по требованию
  • Полностью приостанавливают итерацию до тех пор, пока не потребуется следующее значение, что делает их ленивыми
  • Экономить память, сохраняя только одно значение в памяти в данный момент времени
  • Управлять потоками данных бесконечного или неизвестного размера

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

Рассмотрим следующий код, который повторно использует ваш класс SequenceIterator:

>>>

>>> from sequence_iter import SequenceIterator

>>> numbers_iter = SequenceIterator([1, 2, 3, 4])

>>> for number in numbers_iter:
...     print(number)
...
1
2
3
4

>>> for number in numbers_iter:
...     print(number)
...

Второй цикл в этом примере ничего не выводит на экран. Проблема в том, что первый цикл потребил все элементы из вашего итератора numbers_iter. Как только вы израсходуете все элементы из итератора, этот итератор будет исчерпан.

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

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

>>>

>>> another_iter = SequenceIterator([1, 2, 3, 4])

>>> for number in another_iter:
...     print(number)
...
1
2
3
4

Здесь вы создаете совершенно новый итератор another_iter путем повторного инстанцирования SequenceIterator. Этот новый итератор позволяет вам еще раз просмотреть исходные данные.

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

>>>

>>> numbers_iter = SequenceIterator([1, 2, 3, 4, 5, 6])

>>> for number in numbers_iter:
...     if number == 4:
...         break
...     print(number)
...
1
2
3

>>> next(numbers_iter)
5
>>> next(numbers_iter)
6
>>> next(numbers_iter)
Traceback (most recent call last):
    ...
StopIteration

В этом примере используется условный оператор для прерывания цикла, когда текущее число равно 4. Теперь цикл потребляет только первые четыре числа в numbers_iter. Доступ к остальным значениям можно получить с помощью .__next__() или второго цикла. Обратите внимание, что нет способа получить доступ к потребленным значениям.

Другим ограничением итераторов является то, что они определяют только метод .__next__(), который каждый раз получает следующий элемент. Не существует метода .__previous__() или чего-либо подобного. Это означает, что вы можете двигаться только вперед по итератору. Вы не можете двигаться назад.

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

Наконец, в отличие от списков и кортежей, тераторы не допускают индексации и операций нарезки с помощью оператора [] :

>>>

>>> numbers_iter = SequenceIterator([1, 2, 3, 4, 5, 6])

>>> numbers_iter[2]
Traceback (most recent call last):
    ...
TypeError: 'SequenceIterator' object is not subscriptable

>>> numbers_iter[1:3]
Traceback (most recent call last):
    ...
TypeError: 'SequenceIterator' object is not subscriptable

В первом примере вы пытаетесь получить элемент с индексом 2 в numbers_iter, но получаете ошибку TypeError, поскольку итераторы не поддерживают индексацию. Аналогично, когда вы пытаетесь получить фрагмент данных из numbers_iter, вы тоже получаете TypeError.

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

# reusable_range.py

class ReusableRange:
    def __init__(self, start=0, stop=None, step=1):
        if stop is None:
            stop, start = start, 0
        self._range = range(start, stop, step)
        self._iter = iter(self._range)

    def __iter__(self):
        return self

    def __next__(self):
        try:
            return next(self._iter)
        except StopIteration:
            self._iter = iter(self._range)
            raise

Этот итератор ReusableRange имитирует некоторое поведение встроенного класса range. Метод .__next__() создает новый итератор над объектом range каждый раз, когда вы потребляете данные.

Вот как работает этот класс:

>>>

>>> from reusable_range import ReusableRange

>>> numbers = ReusableRange(10)

>>> list(numbers)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(numbers)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Здесь вы инстанцируете ReusableRange для создания многоразового итератора по заданному диапазону чисел. Чтобы опробовать его, вы вызываете list() несколько раз с объектом-итератором numbers в качестве аргумента. Во всех случаях вы получите новый список значений.

Использование встроенной функции next()

Встроенная функция next() позволяет получить следующий элемент из итератора. Для этого next() автоматически возвращается к вызову метода итератора .__next__(). На практике не следует вызывать специальные методы типа .__next__() непосредственно в коде, поэтому если вам нужно получить следующий элемент из итератора, то следует использовать next().

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

Обычным случаем использования next() является ситуация, когда вам нужно вручную пропустить строку заголовка в CSV файле. Объекты File также являются итераторами, которые выдают строки по запросу. Так, вы можете вызвать next() с CSV-файлом в качестве аргумента, чтобы пропустить его первую строку, а затем передать остальную часть файла в цикл for для дальнейшей обработки:

with open("sample_file.csv") as csv_file:
    next(csv_file)
    for line in csv_file:
        # Process file line by line here...
        print(line)

В этом примере вы используете оператор with для открытия файла CSV, содержащего некоторые целевые данные. Поскольку вы хотите просто обработать данные, вам нужно пропустить первую строку файла, которая содержит заголовки для каждого столбца данных, а не данные. Для этого вы вызываете команду next() с объектом файла в качестве аргумента.

Этот вызов next() возвращается к методу .__next__() объекта file, который возвращает следующую строку в файле. В данном случае следующая строка - это первая строка, потому что вы еще не начали потреблять файл.

Вы также можете передать второй и опциональный аргумент в next(). Этот аргумент называется default и позволяет указать значение по умолчанию, которое будет возвращено, когда целевой итератор вызовет исключение StopIteration. Итак, default - это способ пропустить исключение:

>>>

>>> numbers_iter = SequenceIterator([1, 2, 3])
>>> next(numbers_iter)
1
>>> next(numbers_iter)
2
>>> next(numbers_iter)
3
>>> next(numbers_iter)
Traceback (most recent call last):
    ...
StopIteration

>>> numbers_iter = SequenceIterator([1, 2, 3])
>>> next(numbers_iter, 0)
1
>>> next(numbers_iter, 0)
2
>>> next(numbers_iter, 0)
3
>>> next(numbers_iter, 0)
0
>>> next(numbers_iter, 0)
0

Если вы вызовете next() без значения по умолчанию, то ваш код завершится исключением StopIteration. С другой стороны, если вы предоставите подходящее значение по умолчанию в вызове next(), то вы получите это значение в качестве результата, когда итератор будет исчерпан.

До сих пор вы много узнали об итераторах в Python. Пришло время познакомиться с iterables, которые представляют собой несколько иные инструменты.

Знакомство с итерациями Python

Когда речь заходит об итерации в Python, часто можно услышать, что люди говорят об объектах iterable или просто iterables. Как следует из названия, iterable - это объект, над которым можно iterate. Для выполнения этой итерации обычно используется цикл for.

Чистые итерируемые объекты обычно содержат сами данные. Например, встроенные в Python типы контейнеров, такие как списки, кортежи, словари и множества, являются итерируемыми объектами. Они предоставляют поток данных, по которым можно выполнять итерации. Однако итераторы также являются итерируемыми объектами, даже если они сами не хранят данные. Подробнее об этом вы узнаете в разделе Сравнение итераторов и итерируемых объектов.

Python ожидает итерируемые объекты в нескольких различных контекстах, наиболее важным из которых являются циклы for. Итерабельные объекты также ожидаются в операциях распаковки и во встроенных функциях, таких как all(), any(), enumerate(), max(), min(), len(), zip(), sum(), map() и filter().

Другие определения итераций включают объекты, которые:

  • Реализуют протокол iterable
  • Заставляют встроенную функцию iter() возвращать итератор
  • Реализуют протокол последовательности

В следующих разделах вы узнаете об этих трех способах определения итерабельных таблиц в Python. Начнем с понимания протокола итерабельных переменных.

Протокол итерабельности

Протокол iterable состоит из одного специального метода, который вы уже знаете из раздела о протоколе iterator. Метод .__iter__() выполняет протокол iterable. Этот метод должен возвращать объект итератора, что обычно не совпадает с self, если только ваш итератор не является также итератором.

Чтобы быстро перейти к примеру работы протокола iterable, вы воспользуетесь классом SequenceIterator из предыдущих разделов. Вот реализация:

# sequence_iterable.py

from sequence_iter import SequenceIterator

class Iterable:
    def __init__(self, sequence):
        self.sequence = sequence

    def __iter__(self):
        return SequenceIterator(self.sequence)

В этом примере ваш класс Iterable принимает в качестве аргумента последовательность значений. Затем вы реализуете метод .__iter__(), который возвращает экземпляр SequenceIterator, построенный с использованием входной последовательности. Этот класс готов к итерации:

>>>

>>> from sequence_iterable import Iterable

>>> for value in Iterable([1, 2, 3, 4]):
...     print(value)
...
1
2
3
4

Метод .__iter__() - это то, что делает объект итерабельным. За кулисами цикл вызывает этот метод на итерируемом объекте, чтобы получить объект-итератор, который направляет процесс итерации через свой метод .__next__().

Обратите внимание, что итерационные таблицы сами по себе не являются итераторами. Поэтому вы не можете использовать их в качестве прямых аргументов функции next():

>>>

>>> numbers = Iterable([1, 2, 3, 4])
>>> next(numbers)
Traceback (most recent call last):
    ...
TypeError: 'Iterable' object is not an iterator

>>> letters = "ABCD"
>>> next(letters)
Traceback (most recent call last):
    ...
TypeError: 'str' object is not an iterator

>>> fruits = [
...     "apple",
...     "banana",
...     "orange",
...     "grape",
...     "lemon",
... ]
>>> next(fruits)
Traceback (most recent call last):
    ...
TypeError: 'list' object is not an iterator

Вы не можете передать итератор непосредственно в функцию next(), потому что в большинстве случаев итераторы не реализуют метод .__next__() из протокола итераторов. Это сделано намеренно. Помните, что паттерн итератора предназначен для отделения алгоритма итерации от структур данных.

Примечание: Вы можете добавить метод .__next__() к пользовательскому итератору и возвращать self из его метода .__iter__(). Это превратит вашу итерабельную таблицу в итератор самой себя. Однако это дополнение накладывает некоторые ограничения. Наиболее значимым ограничением может быть то, что вы не сможете выполнять многократную итерацию по вашей итерабельной таблице.

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

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

Если next() не работает, то как итераторы могут работать в циклах for? Что ж, циклы for всегда вызывают встроенную функцию iter(), чтобы получить итератор из целевого потока данных. Подробнее об этой функции вы узнаете в следующем разделе.

Встроенная iter() функция

С точки зрения Python, итератор - это объект, который можно передать встроенной функции iter() для получения из него итератора. Внутренне, iter() возвращается к вызову .__iter__() на целевых объектах. Поэтому, если вам нужен быстрый способ определить, является ли объект итерабельным, используйте его в качестве аргумента для iter(). Если вы получите в ответ итератор, значит, ваш объект является итерируемым. Если вы получите ошибку, значит, объект не является итерируемым:

>>>

>>> fruits = [
... "apple",
... "banana",
... "orange",
... "grape",
... "lemon",
... ]

>>> iter(fruits)
<list_iterator object at 0x105858760>

>>> iter(42)
Traceback (most recent call last):
    ...
TypeError: 'int' object is not iterable

Когда вы передаете итерируемый объект, например список, в качестве аргумента встроенной функции iter(), вы получаете итератор для этого объекта. Напротив, если вы вызываете iter() с объектом, который не является итерируемым, например, целым числом, то вы получите исключение TypeError.

Встроенная reversed() функция

Встроенная в Python функция reversed() позволяет создать итератор, который выдает значения входной итерации в обратном порядке. Ас iter() возвращается к вызову .__iter__() на базовом итераторе, reversed() делегирует специальный метод .__reverse__(), который присутствует в упорядоченных встроенных типах, таких как списки, кортежи и словари.

Другие упорядоченные типы, такие как строки, также поддерживают reversed(), даже если они не реализуют метод .__reverse__().

Вот пример того, как работает reversed():

>>>

>>> digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> reversed(digits)
<list_reverseiterator object at 0x1053ff490>

>>> list(reversed(digits))
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

В этом примере вы используете reversed() для создания объекта-итератора, который выдает значения из списка digits в обратном порядке.

Чтобы выполнить свою работу, reversed() возвращается к вызову .__reverse__() на входной итерабельной переменной. Если эта итерабельность не реализует .__reverse__(), то reversed() проверяет существование .__len__() и .__getitem___(index). Если эти методы присутствуют, то reversed() использует их для последовательного перебора данных. Если ни один из этих методов не присутствует, то вызов reversed() на таком объекте будет неудачным.

Протокол последовательности

Последовательности - это контейнерные типы данных, которые хранят элементы в последовательном порядке. Каждый элемент быстро доступен через индекс, основанный на нуле, который отражает относительное положение элемента в последовательности. Вы можете использовать этот индекс и оператор индексации ([]) для доступа к отдельным элементам последовательности:

>>>

>>> numbers = [1, 2, 3, 4]
>>> numbers[0]
1
>>> numbers[2]
3

Целочисленные индексы дают доступ к отдельным значениям в основном списке чисел. Обратите внимание, что индексы начинаются с нуля.

Все встроенные типы данных последовательности - списки, кортежи и строки - реализуют протокол последовательности, который состоит из следующих методов:

  • .__getitem__(index) принимает целочисленный индекс, начиная с нуля, и возвращает элементы с этим индексом в основной последовательности. При выходе индекса за пределы диапазона возникает исключение IndexError.
  • .__len__() возвращает длину последовательности.

Когда вы используете объект, поддерживающий эти два метода, Python внутренне вызывает .__getitem__() для последовательного получения каждого элемента и .__len__() для определения конца данных. Это означает, что вы можете использовать объект в цикле напрямую.

Примечание: Словари Python также реализуют .__getitem__() и .__len__(). Однако, они считаются отображающими типами данных, а не последовательностями, поскольку для поиска используются произвольные неизменяемые ключи, а не целочисленные индексы.

Чтобы проверить это внутреннее поведение Python, рассмотрим следующий класс, который реализует минимальную структуру данных stack, используя список для хранения фактических данных:

# stack.py

class Stack:
    def __init__(self):
        self._items = []

    def push(self, item):
        self._items.append(item)

    def pop(self):
        try:
            return self._items.pop()
        except IndexError:
            raise IndexError("pop from an empty stack") from None

    def __getitem__(self, index):
        return self._items[index]

    def __len__(self):
        return len(self._items)

Этот класс Stack предоставляет два основных метода, которые обычно встречаются в структуре данных стека. Метод push() позволяет добавлять элементы на вершину стека, а метод pop() удаляет и возвращает элементы с вершины стека. Таким образом, вы гарантируете, что ваш стек работает по принципу LIFO (last in, first out).

Класс также реализует протокол последовательности Python. Метод .__getitem__() возвращает элемент по адресу index из базового объекта списка ._items. Между тем, метод .__len__() возвращает количество элементов в стеке с помощью встроенной функции len().

Эти два метода делают ваш Stack класс итерабельным:

>>>

>>> from stack import Stack

>>> stack = Stack()
>>> stack.push(1)
>>> stack.push(2)
>>> stack.push(3)
>>> stack.push(4)

>>> iter(stack)
<iterator object at 0x104e9b460>

>>> for value in stack:
...     print(value)
...
1
2
3
4

В классе

You Stack нет метода .__iter__(). Однако Python достаточно умен, чтобы построить итератор, используя .__getitem__() и .__len__(). Итак, ваш класс поддерживает iter() и итерацию. Вы создали итератор без формальной реализации протокола итерации.

Работа с итерациями в Python

До этого момента вы узнали, что такое итерабель в Python и как создавать их с помощью различных методов. Вы узнали, что итерабельные таблицы сами содержат данные. Например, списки, кортежи, словари, строки и множества являются итерабельными.

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

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

Итерация по итерациям с помощью for циклов

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

>>>

>>> for number in [1, 2, 3, 4]:
...     print(number)
...
1
2
3
4

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

Итерация через понимания

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

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

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

>>>

>>> numbers = [1, 2, 3, 4]

>>> [number**3 for number in numbers]
[1, 8, 27, 64]

Это понимание списка строит новый список значений куба из исходных данных в numbers. Обратите внимание, что синтаксис понимания напоминает цикл for с блоком кода в начале конструкции.

Вы также можете использовать понимания для условной обработки итераций. Для этого в конце конструкции понимания можно добавить одно или несколько условий:

>>>

>>> numbers = [1, 2, 3, 4, 5, 6]

>>> [number for number in numbers if number % 2 == 0]
[2, 4, 6]

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

Понятия - популярные инструменты в Python. Они обеспечивают отличный способ быстрой и краткой обработки итерабельных данных. Для более глубокого изучения понимания списков ознакомьтесь со статьей Когда использовать понимание списков в Python.

Распаковка итераций

В Python можно использовать итеративные переменные в операциях, известных как распаковка итеративных переменных. Распаковка итерабельной переменной означает присвоение ее значений ряду переменных по очереди. Переменные должны быть представлены в виде кортежа или списка, а количество переменных должно соответствовать количеству значений в итерабле.

Распаковка итерабельного кода может помочь вам написать более лаконичный, читабельный код. Например, вы можете обнаружить, что делаете что-то вроде этого:

>>>

>>> numbers = [1, 2, 3, 4]

>>> one = numbers[0]
>>> two = numbers[1]
>>> three = numbers[2]
>>> four = numbers[3]

>>> one
1
>>> two
2
>>> three
3
>>> four
4

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

>>>

>>> numbers = [1, 2, 3, 4]

>>> one, two, three, four = numbers

>>> one
1
>>> two
2
>>> three
3
>>> four
4

В данном примере numbers - это итерабельная переменная, содержащая числовые данные. Присвоение распаковывает значения в numbers в четыре целевые переменные. Для этого Python внутренне запускает быстрый цикл над итеративной переменной в правой части, чтобы распаковать ее значения в целевые переменные

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

В приведенном выше примере показана наиболее распространенная форма распаковки итерабельных переменных в Python. Главное условие для работы примера - количество переменных должно соответствовать количеству значений в итерабле.

Исследование альтернативных способов записи .__iter__() в итерациях

Как вы узнали из предыдущих разделов, если вы хотите, чтобы объект был итерируемым, то вам придется снабдить его методом .__iter__(), который возвращает итератор. Этот итератор должен реализовать протокол итератора, для чего необходимы методы .__iter__() и .__next__().

В этом разделе вы рассмотрите несколько альтернативных способов создания итераторов с использованием стандартного протокола iterable. Другими словами, вы узнаете различные способы написания методов .__iter__() и создания итерабельных объектов. Обратите внимание, что обычно вы определяете этот метод в классах, которые работают как контейнеры данных или коллекции.

Быстрый способ создания метода .__iter__() заключается в использовании встроенной функции iter(), которая возвращает итератор из потока входных данных. В качестве примера вернитесь к классу Stack и внесите следующие изменения в код:

# stack.py

class Stack:
    def __init__(self):
        self._items = []

    def push(self, item):
        self._items.append(item)

    def pop(self):
        try:
            return self._items.pop()
        except IndexError:
            raise IndexError("pop from an empty stack") from None

    def __iter__(self):
        return iter(self._items)

В этом примере вы используете iter() для получения итератора из исходных данных, хранящихся в ._items. Это быстрый способ написать метод .__iter__(). Однако это не единственный способ сделать это.

Вы также можете превратить ваш метод .__iter__() в функцию-генератор, используя оператор yield в цикле над ._items:

# stack.py

class Stack:
    # ...

    def __iter__(self):
        for item in self._items:
            yield item

Функции генератора возвращают объект-итератор, который выдает элементы по запросу. В этом примере элементы будут поступать из атрибута ._items вашего класса, который хранит исходные данные в стеке.

Наконец, вы также можете использовать синтаксис yield from <iterable>, который был представлен в PEP 380 как быстрый способ создания генераторов итераторов:

# stack.py

class Stack:
    # ...

    def __iter__(self):
        yield from self._items

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

Сравнение итераторов и итерабельных таблиц

До этого момента вы многое узнали об итераторах и итерационных переменных в Python. Когда вы только начинаете изучать Python, часто возникают ошибки, потому что вы путаете итераторы и итераторы. Итераторы имеют метод .__iter__(), который производит элементы по запросу. Итераторы реализуют метод .__iter__(), который обычно возвращает self, и метод .__next__(), который возвращает элемент при каждом вызове.

По этой внутренней структуре можно заключить, что все итераторы являются итераторами, поскольку они соответствуют протоколу итераторов. Однако не все итераторы являются итераторами - только те, которые реализуют метод .__next__().

Непосредственным следствием этого различия является то, что вы не можете использовать чистые итерации в качестве аргументов функции next():

>>>

>>> fruits = [
...     "apple",
...     "banana",
...     "orange",
...     "grape",
...     "lemon",
... ]
>>> next(fruits)
Traceback (most recent call last):
    ...
TypeError: 'list' object is not an iterator

>>> hello = "Hello, World!"
>>> next(hello)
Traceback (most recent call last):
    ...
TypeError: 'str' object is not an iterator

Когда вы вызываете next() с итерабельной переменной в качестве аргумента, вы получаете TypeError. Это происходит потому, что чистые итерабельные переменные не предоставляют .__next__() специального метода, который функция next() может внутренне вызвать для получения следующего элемента данных.

В приведенных выше примерах вы вызываете next() со списком и строковым объектом соответственно. Эти типы данных являются итераторами, но не итераторами, поэтому вы получаете ошибки.

Важно отметить, что .__iter__() семантически отличается для итерабельных и итераторов. В итераторах метод возвращает сам итератор, который должен реализовать метод .__next__(). В итераторах метод должен возвращать элементы по требованию.

Вы можете почувствовать искушение добавить метод .__next__() к пользовательской итерабельной переменной. Это добавление сделает его итерабельным и итератором одновременно. Однако такая практика не рекомендуется, поскольку она не позволяет выполнять несколько итераций над базовыми данными. Чтобы поддерживать множественные итерации по данным, вы должны иметь возможность получить от них несколько независимых итераторов.

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

Еще одно различие связано с базовыми данными. Чистые итераторы обычно сами хранят данные. В отличие от них, итераторы не хранят данные, а выдают их по одному элементу за раз, в зависимости от запроса вызывающей стороны. Поэтому итераторы более эффективны, чем итерационные таблицы, с точки зрения потребления памяти.

Вот краткое описание вышеуказанных и других различий между итераторами и итерабельными таблицами в Python:

Feature Iterators Iterables
Can be used in for loops directly
Can be iterated over many times
Support the iter() function
Support the next() function
Keep information about the state of iteration
Optimize memory use

Первая возможность в этом списке возможна потому, что циклы Python for всегда вызывают iter() для получения итератора из целевых данных. Если этот вызов успешен, то цикл выполняется. В противном случае выдается ошибка.

В общем, при работе с огромными наборами данных следует использовать преимущества итераторов и писать код, экономящий память. Напротив, если вы разрабатываете пользовательские классы контейнеров или коллекций, то предоставьте им протокол iterable, чтобы вы могли использовать их позже в циклах for.

Кодирование асинхронных итераторов

Валютность и параллелизм являются обширными темами в современных вычислениях. Python предпринял множество усилий в этом направлении. Начиная с Python 3.7, в язык включены ключевые слова async и await и полный стандартно-библиотечный фреймворк asyncio для асинхронного программирования.

Примечание: Валютность и параллелизм - две популярные темы в современном компьютерном программировании. Параллелизм заключается в выполнении нескольких операций или задач одновременно, используя преимущества нескольких ядер процессора. параллелизм предполагает, что несколько задач имеют возможность выполняться в перекрывающемся режиме.

Наряду с другими возможностями async, вы обнаружите, что теперь вы можете писать асинхронные for циклы и вычисления , а также асинхронные итераторы .

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

  • .__aiter__() возвращает асинхронный итератор, обычно self.
  • .__anext__() должен возвращать awaitable объект из потока. Он должен вызывать исключение StopAsyncIteration, когда итератор исчерпан.

Обратите внимание, что эти методы очень похожи на методы, используемые в обычных итераторах. Метод .__aiter__() заменяет .__iter__(), а .__anext__() заменяет .__next__(). Ведущий символ a в их именах означает, что данный итератор является асинхронным.

Другая деталь заключается в том, что .__anext__() должен поднимать StopAsyncIteration вместо StopIteration в конце, чтобы сигнализировать, что данные закончились, и итерация должна завершиться.

В качестве примера асинхронного итератора рассмотрим следующий класс, который производит случайные целые числа:

# async_rand.py

import asyncio
from random import randint

class AsyncIterable:
    def __init__(self, stop):
        self._stop = stop
        self._index = 0

    def __aiter__(self):
        return self

    async def __anext__(self):
        if self._index >= self._stop:
            raise StopAsyncIteration
        await asyncio.sleep(value := randint(1, 3))
        self._index += 1
        return value

Этот класс принимает значение stop во время инстанцирования. Это значение определяет количество случайных целых чисел, которые необходимо получить. Метод .__aiter__() возвращает self, стандартная практика в итераторах. Нет необходимости в том, чтобы этот метод был асинхронным. Поэтому не нужно использовать ключевые слова async def в определении метода.

Метод .__anext__() должен быть асинхронным coroutine, поэтому для его определения необходимо использовать ключевые слова async def. Этот метод должен возвращать ожидаемый объект, то есть объект, который вы можете использовать в асинхронной операции, например, в цикле async for.

В этом примере .__anext__() вызывает StopAsyncIteration, когда атрибут ._index достигает значения в ._stop. Затем метод запускает выражение await, которое вычисляет случайное целое число, обернутое в вызов asyncio.sleep() для имитации ожидаемой операции. Наконец, метод возвращает вычисленное случайное число.

Вот как можно использовать этот итератор в цикле async for:

>>>

>>> import asyncio
>>> from async_rand import AsyncIterable

>>> async def main():
...     async for number in AsyncIterable(4):
...         print(number)
...

>>> asyncio.run(main())
3
3
2
1

Этот код выдаст вам другой результат, поскольку он работает со случайными числами. Важно помнить, что асинхронные итераторы, циклы async for и асинхронные понимания не делают процесс итерации параллельным. Они просто позволяют итерации передать управление циклу событий asyncio для выполнения какой-либо другой короутины.

В приведенном выше примере цикл событий asyncio выполняется, когда вы вызываете функцию asyncio.run() с вашей функцией main() в качестве аргумента.

Заключение

Вы многое узнали о Python iterators и iterables. Теперь вы знаете, что это такое и в чем их основные различия. Вы узнали, как создавать различные типы итераторов в соответствии с их специфическим поведением относительно входных и выходных данных.

Вы изучили генераторы итераторов и научились создавать их в Python. Кроме того, вы узнали, как создавать собственные итераторы, используя различные техники. Наконец, вы коснулись асинхронных итераторов и асинхронных циклов в Python.

В этом уроке вы узнали, как:

  • Создавайте собственные итераторы, используя протокол итераторов в Python
  • отличатьитераторы от итераций и использовать их в коде
  • Используйте функции-генераторы и оператор yield для создания итераторов-генераторов
  • Создавайте пользовательские итераторы, используя различные техники, такие как iterable protocol
  • Записывать асинхронные итераторы с помощью модуля asyncio и ключевых слов await и async

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

Вернуться на верх