Итераторы Python

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

 

mytuple = ("яблоко", "банан", "вишня")

myit = iter(mytuple)

print(next(myit))

print(next(myit))

print(next(myit))

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

Кстати, итерируемым объектом являются не только массивы, но и строки.

Давайте приведем пример кода. который раскладывает строки на буквы. Каждый следующий символ строки – это отдельный элемент в понимании Python. Поэтому если использовать итератор для любого слова (например, «банан»), то можно получить последовательность букв, из которых оно состоит. 

mystr = "банан"

myit =  iter(mystr)

print(next(myit))

print(next(myit))

print(next(myit))

print(next(myit))

print(next(myit))

Вывод: 

б

а

н

а

н

Цикл-итератор

Самое частое использование цикла for – итерация по итерируемому объекту. Приведем пример, как он используется. 

mytuple = ("яблоко", "банан", "вишня") 

for x in mytuple:

    print(x)

Если выполнить код, то получим такой результат.

яблоко

банан

вишня

Причем он будет одинаковым во всех случаях: как в прошлом, так и нынешнем. 

И точно так же, как и в прошлом случае, цикл for позволяет итерировать символы строки. 

mystr = "банан"

for x in mystr:

    print(x)

Этот код разложит слово «банан» на буквы точно таким же образом, как и в предыдущем случае. То есть, результат идентичный. Цикл for используется для создания объекта итератора и для каждого цикла выполняет метод next(). Благодаря тому, что он более сжатый, он и используется чаще.

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

Как реализовать итератор в объекте?

Для этого необходимо реализовать два метода в объекте:

  1. __iter__().
  2. __next__().

Каждый класс имеет также функцию __init__(), используемую для инициализации объекта. Метод __iter__() очень похож на него, но при этом имеет одно отличие. Он всегда должен возвращать объект итератора. А с помощью метода __next __() вы можете получать следующий элемент из набора значений.

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

class MyNumbers:

    def __iter__(self):

        self.a = 1

        return self




    def __next__(self):

        x = self.a

        self.a += 1

        return x




myclass = MyNumbers()  

myiter = iter(myclass)  

print(next(myiter))  

print(next(myiter))  

print(next(myiter))  

print(next(myiter))  

print(next(myiter))

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

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

StopIteration

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

Чтобы сделать это, используем оператор StopIteration.

Условие окончания перебора элементов объекта задается в методе __next __(). Работает он следующим образом: проверяется количество раз, которое прошел итератор. Если это число достигает определенного значения, то цикл останавливается. 

class MyNumbers:

    def __iter__(self):

        self.a = 1

        return self

    

    def __next__(self):

        if self.a <= 20:  

            x = self.a

            self.a += 1

            return x

        else:

            raise StopIteration




myclass = MyNumbers()

myiter = iter(myclass)

for x in myiter:

    print(x)

После того, как мы запустим код, получим следующий вывод.

1  

2  

3  

18

19

20

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

Итератор, итерируемый объект и генератор: в чем отличия

Начинающие разработчики Python часто путают значения понятий «итерируемый объект», «итератор» и «генератор», хотя они отличаются. Соотношение между ними следующее – итератор – это некая копия итерируемого объекта, если можно так выразиться. В свою очередь, генератор – это разновидность итератора. Причем в некоторых случаях последний – гораздо удобнее и функциональнее, ничуть не уступая объявлению итератора через класс.

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

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

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

>>> a.__next__()

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

AttributeError: 'list' object has 

no attribute '__next__'

Важно: при использовании метода __next__() итератор опустошается. То есть, каждый раз, когда мы его используем, в нем уже не содержится никаких данных. 

>>> b.__next__()

1

>>> b.__next__()

2

>>> b.__next__()

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

StopIteration

Остановка итерации происходит автоматически, когда метод перебирает все элементы. 

А вот что общего у итераторов и итерируемых объектов – так это метод __iter__(). Но им возвращается сам итератор.  

>>> a = [1, 2]

>>> a = "hi"

>>> b = a.__iter__()

>>> c = b.__iter__()

>>> a

'hi'

>>> b

<str_iterator object at 0x7f7e24c1ad30>

>>> c

<str_iterator object at 0x7f7e24c1ad30>

>>> b.__next__()

'h'

>>> c.__next__()

'i'

>>> b.__next__()

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

StopIteration

Обратите внимание на то, что в этом фрагменте кода переменные b и c ссылаются на тот же самый объект. 

О том, что итерируемые объекты и итераторы – это разные понятия, станет понятно, когда мы приведем примеры того и другого:

  1. Итерируемые объекты – списки, словари, строки, коллекции, возвращаемый функцией range() объект.
  2. Итераторы – файловые объекты, генераторы, итераторы, созданные на основе итерируемых объектов.

Функции iter() и next() вызывают одноименные методы тех объектов, которые передаются в качестве аргумента (но которые имеют нижние подчеркивания перед и после названия).  

>>> a = {1: 'a', 2: 'b'}

>>> b = iter(a)

>>> b

<dict_keyiterator object at 0x7f7e24c17778>

>>> next(b)

1

Причем цикл for, когда он вызывается, сперва создает итератор и вызывает его метод __iter__()

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

Чтобы было проще понять, просто сравните эти два фрагмента кода. Первый – это пример использования класса для создания итератора. 

from random import random




class RandomIncrease:

    def __init__(self, quantity):

        self.qty = quantity

        self.cur = 0




    def __iter__(self):

        return self




    def __next__(self):

        if self.qty > 0:

            self.cur += random()

            self.qty -= 1

            return round(self.cur, 2)

        else:

            raise StopIteration




iterator = RandomIncrease(5)

for i in iterator:

    print(i)

А второй – это использование генератора.

def random_increase(quantity):

    cur = 0

    while quantity > 0:

        cur += random()

        quantity -= 1

        yield round(cur, 2)




generator = random_increase(5)

for i in generator:

    print(i)

Обратите внимание: этот вариант гораздо более компактный, но выполняет ту же функцию.

А в некоторых случаях этот код можно свести до еще более простого. 

g = (round(random()+i, 2) for i in range(5))




for i in g:

    print(i)

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

Выводы

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

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

ОфисГуру
Adblock
detector