Работа с итерируемыми объектами – важная составляющая работы в 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 после других языков.
Как реализовать итератор в объекте?
Для этого необходимо реализовать два метода в объекте:
- __iter__().
- __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 ссылаются на тот же самый объект.
О том, что итерируемые объекты и итераторы – это разные понятия, станет понятно, когда мы приведем примеры того и другого:
- Итерируемые объекты – списки, словари, строки, коллекции, возвращаемый функцией range() объект.
- Итераторы – файловые объекты, генераторы, итераторы, созданные на основе итерируемых объектов.
Функции 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. С итерируемыми объектами приходится иметь дело постоянно, поскольку они могут хранить большое количество элементов, а также расширяться (за исключением кортежей).
Вы теперь умеете и самостоятельно создавать итерируемые объекты, которые будут выполнять те задачи, которые вы перед ними поставите. Рекомендуем потренироваться на практике, создавая собственные итераторы и итерируемые объекты. Попробуйте попрактиковаться в создании разных программ. Это поможет лучше ориентироваться в реальных рабочих задачах. Успехов.