Что такое итераторы и генераторы, чем они отличаются?
Итератор – это интерфейс, позволяющий перебирать элементы последовательности. Он используется, например, в цикле for … in … , но этот механизм скрыт от глаз разработчика. При желании итератор можно получить «в сыром виде», воспользовавшись функцией iter() .
Чтобы получить следующий элемент коллекции или строки, нужно передать итератор функции next() .
Под капотом функциональность реализуется в методах __iter__ и __next__ .

Пример простого итератора:
class SimpleIterator: def __iter__(self): return self def __init__(self, limit): self.limit = limit self.counter = 0 def __next__(self): if self.counter < self.limit: self.counter += 1 return 1 else: raise StopIteration simple_iter = SimpleIterator(5) for i in simple_iter: print(i) # 1 # 1 # 1 # 1 # 1
На базе итераторов в языке появились новые элементы синтаксического сахара: выражения-генераторы и генераторы коллекций. Они позволяют устанавливать условия для отбора.
numbers = range(10) squared = [n ** 2 for n in numbers if n % 2 == 0] print(squared) # [0, 4, 16, 36, 64]
В этом примере из списка чисел отбираются четные числа, а в финальную коллекцию вносятся их квадраты.
Выражения-генераторы не создают целый список заданной длины сразу, а добавляют элементы по мере необходимости.
Очевидно, что генераторы могут выполнять работу функций map и filter . Более того, они справляются с этой задачей эффективнее.
Что такое итераторы и зачем они нужны
Смотрите. Пускай у вас есть контейнер. Неважно какой: map , vector , set . Он содержит набор элементов. И вы хотите сослаться не на весь контейнер, а на какое-то место в этом наборе элементов. Так чтобы от этого места можно было перейти вперёд/назад, и что-то в этом месте сделать: изменить элемент, вставить элемент, удалить элемент.
Как это сделать? Для массива вы в таких случаях пользуетесь индексом. set внутри является красно-чёрным деревом, так что вам понадобится ссылка на узел этого дерева. unordered_map использует хэш-таблицу, так что вам нужна пара из хэш-индекса и указателя на элемент внутри bucket'а.
Чтобы не приходилось писать алгоритмы, специфические для каждого контейнера, и были придуманы итераторы.
Итератор — структура данных, которая «указывает» на некоторый элемент контейнера, и (для некоторых контейнеров) умеет переходить к предыдущему/следующему элементу.
Если вы хотите реализовать итератор, помните, что существуют разные типы итераторов, в зависимости от операций, которые они предоставляют. Вот список возможных типов.
Если вы, например, хотите реализовать RandomAccessIterator , вам придётся определить конструктор копирования, оператор присваивания, деструктор, операции == , != , * , -> , конструктор без аргументов, ++ , -- , += , + (2 шт.), -= , - (2 шт.), < , >, = . (Другие типы итераторов попроще.)
Итераторы в Python для самых маленьких

«Напиши, пожалуйста, кастомный итератор,» — такое задание довольно часто дают на собеседованиях, и я раз за разом вижу обреченные глаза кандидата, когда он сталкивается с подобной просьбой. Опыт участия в собеседованиях показал мне, что большинство начинающих разработчиков бегут от этой темы, потому что она кажется слишком запутанной и непонятной. А ведь ничего сложного в ней нет, если подобраться к ней правильным образом — в чём я и постараюсь помочь дорогим читателям.
Наше путешествие мы начнем с того, что вообще такое итератор. Итератор — это некий объект, который в себе реализует интерфейс перебора чего-либо. А говоря рабоче-крестьянским языком — это такая штука, которая в себе описывает правило, по которому мы будем перебирать содержимое той или иной коробки.
Давайте представим, что у нас есть тумбочка. В этой тумбочке лежат несколько предметов: ножницы, карандаш, яблоко и книга. И перед нами, как перед разработчиками ПО, поставили задачу: описать с помощью кода тумбочку, которая могла бы принимать на хранение некие объекты и выдавать содержимое по требованию.
Конечно же, мы можем хранить наше содержимое тумбочки в любой удобной коллекции, например, в списке:
tumb = ["ножницы", "карандаш", "яблоко", "книга"]
И решать задачу добавления объектов посредством методов списка (через append , например), а задачу перебора — с помощью цикла for :
for obj in tumb: print(obj)
«Ну и при чём тут какой-то там итератор?» — спросите меня вы. А что, если я скажу вам, что цикл for работает не совсем так, как вы думаете?
Во многих статьях и книгах пишут про то, что цикл for позволяет перебирать объекты коллекций. Это правда. Однако часто из внимания упускают то, как именно он это делает. А ведь это фундаментально важно для понимания нашей темы.
На самом деле цикл for взаимодействует не с самим целевым объектом перебора, а с его итератором! В нашем случае — с итератором списка. То есть он как бы говорит: «Эй, объект! Я хочу тебя перебрать, поэтому дай мне то, что описывает правило твоего перебора!» Если объект сможет ответить на это «Вот, держи!» и вернет циклу for некий объект, то объект называется итерируемым и его можно перебирать. Если же он отвечает что-то вроде «Я не понимаю, о чём ты», то программа выдаст ошибку, и это будет означать, что мы попытались перебрать объект, который для этого не предназначен.
Весь фокус в том, что итерируемый объект в случае успеха в качестве ответа на запрос «Дай правило итерации!» возвращает объект итератор.
Как цикл for получает объект-итератор от целевого итерируемого объекта? С помощью неявного вызова встроенной функции iter , в которую в качестве аргумента он передаёт как раз этот самый целевой итерируемый объект. И если в результате будет получен некий объект, то дальнейшая работа будет производиться уже с ним. Давайте посмотрим, что мы получим в качестве результата, если передадим наш список в метод iter :
>>> print(iter(tumb))
Видите? Мы получили объект типа list_iterator , инкапсулирующий в себе то самое правило перебора, которое сейчас будет применяться.
После успешного получения итератора цикл for начинает взаимодействовать с ним тупым нажиманием кнопки «давай следующее значение» до тех пор, пока эти значения не будут исчерпаны. Представьте себе, что вам дали в руки пульт с нопкой и сказали нажимать на неё до тех пор, пока вы не получите на экране сообщение «Хватит!»
Как цикл for нажимет эту воображаемую кнопку у итератора? С помощью ещё одной встроенной функции next , аргументом которой является объект-итератор, полученный на предыдущем шаге. Результатом этого в штатном случае будет получение очередного значения. Такая процедура будет повторяться многократно до тех пор, пока цикл for не получит сообщение о том, что все значения уже закончились. Что это за сообщение? Это raise ошибки StopIteration .
А теперь давайте взглянем на аналог цикла for , написанный через while :
tumb = ["ножницы", "карандаш", "яблоко", "книга"] # получаем итератор для итерируемого объекта it = iter(tumb) try: while True: next_val = next(it) print("Очередное значение:", next_val) except StopIteration: # явно напечатаем сообщение об окончании итерации, # хотя цикл for этого не делает и ошибка просто подавляется print("Итерация закончена") print("Программа завершена")
То есть ответственность за перебор лежит не на цикле for (он просто запрашивает итератор и жмëт в нëм кнопку) и не на самом итерируемом объекте (он лишь должен отдавать свой объект-итератор по запросу), а на итераторе!
Хорошо, с этим понятно, но что там с каким-то кастомным итератором? Это про что вообще история? А эта история про ситуации, когда вам необходимо, чтобы объекты ваших самописных классов тоже можно было итерировать. Давайте представим, что нам пришлось написать для тумбочки отдельный класс, чтобы навесить там много разных дополнительных методов, суть которых нам не важна в описываемом контексте, но важно то, что мы хотели бы иметь возможность перебирать нашу тумбочку, как если бы это был простой список.
Пускай у нашей тумбочки предметы хранятся в составе списковых полей, каждое из которых будет соответствовать тому или иному ящику. У тумбочки будут методы добавления и удаления элементов в ящики. Наш класс будет выглядеть как-то так:
class Tumbochka: """Волшебная тумбочка с тремя ящиками для чего угодно""" def __init__(self): self.boxes = < 1: [], 2: [], 3: [] >def add_to_box(self, obj, box_num): if box_num not in : print("Вы ввели неправильный номер ящика!") else: self.boxes[box_num].append(obj) def remove_from_box(self, box_num): if box_num not in : print("Вы ввели неправильный номер ящика!") else: return self.boxes[box_num].pop() def __str__(self): boxes_items = self.boxes[1] + self.boxes[2] + self.boxes[3] return ", ".join(boxes_items)
Создадим тумбочку, нагрузим её предметами и выведем информацию на экран:
tumb = Tumbochka() tumb.add_to_box("ножницы", 1) tumb.add_to_box("карандаш", 2) tumb.add_to_box("яблоко", 3) tumb.add_to_box("книга", 1) print(tumb)
А теперь вопрос: как нам сделать так, чтобы нашу тумбочку можно было итерировать? Можно, конечно, взять и сделать что-то вроде нового списка, который будет хранить в себе сумму элементов трех ящиков: tumb.boxes[1] + tumb.boxes[2] + tumb.boxes[3] и итерировать его, но это не очень хороший подход. Почему? Давайте представим себе, что у нас есть список с несколькими итерируемыми объектами: списком, множеством и строкой.
my_shiny_list = [ ["Это", "список", "внутри", "списка"], , "Это строка внутри списка", ]
И у нас вознкает необходимость добавить к этим товарищам ещё и нашу тумбочку (а может даже и не одну):
my_shiny_list = [ ["Это", "список", "внутри", "списка"], , "Это строка внутри списка", tumb, ]
Согласитесь, что писать сумму списков внутри вложенного списка — не самое изящное решение, да и сам класс в процессе развития нашего проекта может измениться: например, уберутся или добавятся новые ящики. Придется помнить про все подобные места и вносить правки в них.
Теперь давайте представим, что по логике работы нашей программы у нас предполагается последовательное итерирование каждого из элементов нашего списка:
for some_collection in my_shiny_list: for el in some_collection: print(el)
В какой-то момент мы получим ошибку TypeError: 'Tumbochka' object is not iterable . Python говорит нам, что мы попытались проитерировать объект, который на приказ «Дай мне свой итератор!» отвечает что-то вроде «Я не понимаю, о чëм ты!».
Почему так произошло? Дело в том, что наш класс тумбочки понятия не имеет, кто отвечает за правило перебора элементов в ней.
Как это устранить? Нужно сделать так, чтобы встроенная функция iter получала от нашего объекта тумбочки её итератор. Для этого нам потребуется дописать в классе тумбочки магический метод __iter__ , назначение которого как раз и состоит в том, чтобы создавать и возвращать в результате своей работы некий объект-итератор.
Но давайте посмотрим повнимательнее — для решения нашей задачи достаточно, чтобы в качестве итератора тумбочки выступал итератор списка суммы трëх ящиков. Для его получения нам нужно будет просто передать эту сумму во встроенную функцию iter и уже результат по работы вернуть в качестве результата магического метода __iter__ ! Вот как это будет выглядеть:
def __iter__(self): # получаем сумму предметов всех ящиков boxes_items = self.boxes[1] + self.boxes[2] + self.boxes[3] # получаем итератор от списка и возвращаем его it = iter(boxes_items) return it
Теперь наша тумбочка без проблем сможет быть перебранной наравне с известными встроенными коллекциями и по праву будет являться итерируемым объектом.
И что, это и есть кастомный итератор? Нет! В вышеупомянутом примере мы воспользовались итератором списка в качестве итератора нашей тумбочки. То есть сам итератор не имеет ни малейшего понятия, что его вернули как результат работы какой-то там тумбочки; его задача состоит лишь в том, чтобы перебирать.
А теперь давайте с вами представим, что мы хотим, чтобы наша тумбочка при итерации возвращала не просто объекты, а ещё и их адреса в памяти. Тут возникает проблема: итератор списка нам уже не поможет. Нужно что-то помощнее.
Можно обратиться за помощью к генераторам (это такая разновидность итератора, про которую вы можете прочесть замечательную статью моего товарища, а также посмотреть серию его видео по теме). И реализация будет выглядеть примерно так:
def __iter__(self): # получаем сумму предметов всех ящиков boxes_items = self.boxes[1] + self.boxes[2] + self.boxes[3] # возвращаем очередное значение # (пару "объект в ящике тумбочки + адрес в памяти") с помощью yield for el in boxes_items: yield el, id(el)
Результатом работы магического метода __iter__ будет generator_object , из которого по запросу мы сможем получать очередные пары значений.
Ну, а теперь это кастомный итератор? Всë ещë нет! Необходимость написания кастомного итератора возникает тогда, когда мы хотим тонко управлять процессом перебора. Например, иметь возможность при каком-то событии начать итерацию с самого начала или установить значение указателя перебора на определённый элемент. То есть не перебирать всё подряд, как в цикле for , а управлять вручную нашим процессом через непосредственное взаимодействие с итератором.
Как решить эту задачу? Объект-генератор нам не подходит, использовать итераторы коллекций — тоже. Придëтся писать что-то своё 🙂 Это самое «что-то своё» и будет называться кастомным итератором. Это отдельный класс, объект которого будет возвращаться в качестве результата работы метода __iter__ . Давайте напишем простой класс:
class TumbochkaIterator: pass
А в классе тумбочки поменяем магический метод __iter__ на вот такую реализацию:
def __iter__(self): return TumbochkaIterator()
И попробуем получить итератор тумбочки через встроенную функцию iter :
Мы увидим ошибку TypeError: iter() returned non-iterator of type 'TumbochkaIterator' , которая любезно сообщает о том, что тот объект, который мы вернули, итератором на самом деле не является.
А что является, спросите вы? А является итератором то, что обладает специальным магическим методом, способным возвращать очередное значение. Именно возвращать очередное значение! Таким магическим методом является метод __next__ . Этот метод будет отрабатывать каждый раз, когда объект итератора будет передаваться во встроенную функцию next .
Давайте добавим пустой метод __next__ в наш класс-итератор:
class TumbochkaIterator: def __next__(self): pass
Теперь посмотрим на результат, который выведет этот код:
>>> print(iter(tumb))
Видите? Мы теперь получаем в качестве результата работы функции iter от нашей тумбочки самый настоящий объект итератор! При этом он не имеет никакого понятия из чего он вернулся — из тумбочки, ящика или грузовика. Ему об этом знать не нужно, его задача будет состоять лишь в том, чтобы возвращать очередное значение, когда его передадут в next !
Давайте теперь попробуем вручную получить несколько очередных значений из итератора:
it = iter(tumb) print(next(it)) print(next(it)) print(next(it)) print(next(it)) None None None None
Мы получили четыре объекта None , потому что именно их возвращает нам метод __next__ . Давайте сделаем так, чтобы __next__ возвращал единичку:
class TumbochkaIterator: def __next__(self): return 1
Теперь при попытке запустить код выше мы получим четыре единички.
1 1 1 1
А теперь давайте свяжем наши объекты в тумбочке с итератором: пусть итератор при инициализации принимает в себя ссылку на все объекты трëх ящиков. Также заведëм счетчик, который поможет нам всегда знать, на каком именно объекте перебора мы сейчас находимся. А метод __next__ изменим таким образом, чтобы на каждый его вызов мы возвращали следующий элемент и увеличивали при этом значение счетчика на единицу для того, чтобы при следующем вызове __next__ вернуть очередное значение. Общий код классов будет выглядеть так:
class TumbochkaIterator: def __init__(self, some_objects): self.some_objects = some_objects self.current = 0 def __next__(self): if self.current < len(self.some_objects): result = self.some_objects[self.current] self.current += 1 return result
class Tumbochka: """Волшебная тумбочка с тремя ящиками для чего угодно""" def __init__(self): self.boxes = < 1: [], 2: [], 3: [] >def add_to_box(self, obj, box_num): if box_num not in : print("Вы ввели неправильный номер ящика!") else: self.boxes[box_num].append(obj) def remove_from_box(self, box_num): if box_num not in : print("Вы ввели неправильный номер ящика!") else: return self.boxes[box_num].pop() def __str__(self): boxes_items = self.boxes[1] + self.boxes[2] + self.boxes[3] return ", ".join(boxes_items) def __iter__(self): return TumbochkaIterator(self.boxes[1] + self.boxes[2] + self.boxes[3])
А теперь давайте запустим вот этот код:
tumb = Tumbochka() tumb.add_to_box("ножницы", 1) tumb.add_to_box("карандаш", 2) tumb.add_to_box("яблоко", 3) tumb.add_to_box("книга", 1)
it = iter(tumb) print(next(it)) print(next(it)) print(next(it)) print(next(it)) print(next(it)) print(next(it))
Вот, что мы получим в результате:
ножницы книга карандаш яблоко None None
Что это за None ? Откуда это всë взялось? Дело в том, что встроенной функции next не важно, что вернулось в качестве результата работы __next__ . Пусть даже это будет None .
А теперь давайте добавим пару методов. Один будет возвращать значение current на старт, другой — на выбранную позицию, но в пределах списка наших элементов. Выглядеть эти методы будут вот так:
def to_start(self): self.current = 0 def to_current(self, val): if val >= len(self.some_objects) or val < 0: print("Неверное значение для курсора!") else: self.current = val
Теперь мы можем при ручной итерации через next обнулять нашу итерацию или перемещать курсор к какому-то другому значению. Например, мы можем сказать, что если тот элемент, который я достал из тумбочки — это ножницы, то следующий элемент я буду пропускать и перемещать курсор на шаг вперед. Можете попробовать написать такое условие в качестве домашнего задания к этой статье 😉
А сейчас я предлагаю запустить процесс итерации нашей тумбочки в цикле for и посмотреть на результат:
for el in tumb: print(el)
Давайте взглянем на результат (скорее всего, у вас будет просто None , потому что значения будут лететь очень быстро, я рекомендую добавить sleep(.1) перед вызовом print ):
ножницы книга карандаш яблоко None None None None None None
Программа ушла в бесконечный цикл и остановить её можно только с помощью ручного останова. Почему так произошло? Да потому что циклу for тоже не важно, что вернулось в качестве очередного значения из __next__ — он будет жать кнопку «Дай!» до тех пор, пока не возникнет исключение StopIteration . Возникновение этого исключения мы и должны теперь предусмотреть в методе __next__ . Давайте сделаем это:
def __next__(self): if self.current < len(self.some_objects): result = self.some_objects[self.current] self.current += 1 return result raise StopIteration
Перезапустим наш код и взглянем на результат:
ножницы книга карандаш яблоко
Теперь всё работает замечательно! Остался последний штрих: по соглашению объекты-итераторы также должны являться итерируемыми объектами. Принято в качестве итераторов для самих итераторов использовать их самих. Звучит зубодробительно, но я думаю, что взглянув на метод __iter__ в составе нашего итератора вопросы уйдут:
class TumbochkaIterator: def __init__(self, some_objects): self.some_objects = some_objects self.current = 0 def to_start(self): self.current = 0 def to_current(self, val): if val >= len(self.some_objects) or val < 0: print("Неверное значение для курсора!") else: self.current = val def __iter__(self): return self def __next__(self): if self.current < len(self.some_objects): result = self.some_objects[self.current] self.current += 1 return result raise StopIteration
То есть объект возвращает сам себя в качестве итератора, если мы хотим проитерировать сам итератор. Это сделано для того, чтобы у мы могли перебирать объекты кастомных итераторов в цикле for точно так же, как это можно делать с generator_objects или итераторами коллекций, вроде list_iterator .
- Итерируемый объект — это объект, который можно перебирать.
- За правило перебора отвечает итератор, а не сам объект.
- Итерируемый объект при попытке его перебрать должен уметь возвращать свой итератор, чтобы уже с ним продолжалась работа.
- Метод, который возвращает итератор, называется __iter__ .
- Объект-итератор должен иметь метод __next__ , который возвращает очередное значение.
- Цикл for будет вызывать функцию next от итератора до тех пор, пока не получит исключение StopIteration .
- Возникновение StopIteration — это ответственность итератора, а именно его метода __next__ .
- Если StopIteration не возникнет никогда, то мы получим бесконечный цикл.
- Написание кастомного итератора может понадобиться в том случае, если необходимо тонко управлять процессом итерации. Для стандартных случаев зачастую достаточно использовать итераторы стандартных коллекций или объекты-генераторы.
Итератор
Итератор — это поведенческий паттерн проектирования, который даёт возможность последовательно обходить элементы составных объектов, не раскрывая их внутреннего представления.

Проблема
Коллекции — самая распространённая структура данных, которую вы можете встретить в программировании. Это набор объектов, собранный в одну кучу по каким-то критериям.

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

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

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

Вы планируете полететь в Рим и обойти все достопримечательности за пару дней. Но приехав, вы можете долго петлять узкими улочками, пытаясь найти Колизей.
Если у вас ограниченный бюджет — не беда. Вы можете воспользоваться виртуальным гидом, скачанным на телефон, который позволит отфильтровать только интересные вам точки. А можете плюнуть и нанять локального гида, который хоть и обойдётся в копеечку, но знает город как свои пять пальцев, и сможет посвятить вас во все городские легенды.
Таким образом, Рим выступает коллекцией достопримечательностей, а ваш мозг, навигатор или гид — итератором по коллекции. Вы, как клиентский код, можете выбрать один из итераторов, отталкиваясь от решаемой задачи и доступных ресурсов.
Структура

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

Так, итератор друзей перебирает всех друзей профиля, а итератор коллег — фильтрует друзей по принадлежности к компании профиля. Все итераторы реализуют общий интерфейс, который позволяет клиентам работать с профилями, не вникая в детали работы с социальной сетью (например, в авторизацию, отправку REST-запросов и т. д.)
Кроме того, Итератор избавляет код от привязки к конкретным классам коллекций. Это позволяет добавить поддержку другого вида коллекций (например, LinkedIn), не меняя клиентский код, который работает с итераторами и коллекциями.
// Общий интерфейс коллекций должен определить фабричный метод // для производства итератора. Можно определить сразу несколько // методов, чтобы дать пользователям различные варианты обхода // одной и той же коллекции. interface SocialNetwork is method createFriendsIterator(profileId):ProfileIterator method createCoworkersIterator(profileId):ProfileIterator // Конкретная коллекция знает, объекты каких итераторов нужно // создавать. class Facebook implements SocialNetwork is // . Основной код коллекции. // Код получения нужного итератора. method createFriendsIterator(profileId) is return new FacebookIterator(this, profileId, "friends") method createCoworkersIterator(profileId) is return new FacebookIterator(this, profileId, "coworkers") // Общий интерфейс итераторов. interface ProfileIterator is method getNext():Profile method hasMore():bool // Конкретный итератор. class FacebookIterator implements ProfileIterator is // Итератору нужна ссылка на коллекцию, которую он обходит. private field facebook: Facebook private field profileId, type: string // Но каждый итератор обходит коллекцию, независимо от // остальных, поэтому он содержит информацию о текущей // позиции обхода. private field currentPosition private field cache: array of Profile constructor FacebookIterator(facebook, profileId, type) is this.facebook = facebook this.profileId = profileId this.type = type private method lazyInit() is if (cache == null) cache = facebook.socialGraphRequest(profileId, type) // Итератор реализует методы базового интерфейса по-своему. method getNext() is if (hasMore()) result = cache[currentPosition] currentPosition++ return result method hasMore() is lazyInit() return currentPosition < cache.length // Вот ещё полезная тактика: мы можем передавать объект // итератора вместо коллекции в клиентские классы. При таком // подходе клиентский код не будет иметь доступа к коллекциям, а // значит, его не будут волновать подробности их реализаций. Ему // будет доступен только общий интерфейс итераторов. class SocialSpammer is method send(iterator: ProfileIterator, message: string) is while (iterator.hasMore()) profile = iterator.getNext() System.sendEmail(profile.getEmail(), message) // Класс приложение конфигурирует классы, как захочет. class Application is field network: SocialNetwork field spammer: SocialSpammer method config() is if working with Facebook this.network = new Facebook() if working with LinkedIn this.network = new LinkedIn() this.spammer = new SocialSpammer() method sendSpamToFriends(profile) is iterator = network.createFriendsIterator(profile.getId()) spammer.send(iterator, "Very important message") method sendSpamToCoworkers(profile) is iterator = network.createCoworkersIterator(profile.getId()) spammer.send(iterator, "Very important message")
Применимость
Когда у вас есть сложная структура данных, и вы хотите скрыть от клиента детали её реализации (из-за сложности или вопросов безопасности).
Итератор предоставляет клиенту всего несколько простых методов перебора элементов коллекции. Это не только упрощает доступ к коллекции, но и защищает её данные от неосторожных или злоумышленных действий.
Когда вам нужно иметь несколько вариантов обхода одной и той же структуры данных.
Нетривиальные алгоритмы обхода структуры данных могут иметь довольно объёмный код. Этот код будет захламлять всё вокруг — будь то сам класс коллекции или часть бизнес-логики программы. Применив итератор, вы можете выделить код обхода структуры данных в собственный класс, упростив поддержку остального кода.
Когда вам хочется иметь единый интерфейс обхода различных структур данных.
Итератор позволяет вынести реализации различных вариантов обхода в подклассы. Это позволит легко взаимозаменять объекты итераторов, в зависимости от того, с какой структурой данных приходится работать.
Шаги реализации
- Создайте общий интерфейс итераторов. Обязательный минимум — это операция получения следующего элемента коллекции. Но для удобства можно предусмотреть и другое. Например, методы для получения предыдущего элемента, текущей позиции, проверки окончания обхода и прочие.
- Создайте интерфейс коллекции и опишите в нём метод получения итератора. Важно, чтобы сигнатура метода возвращала общий интерфейс итераторов, а не один из конкретных итераторов.
- Создайте классы конкретных итераторов для тех коллекций, которые нужно обходить с помощью паттерна. Итератор должен быть привязан только к одному объекту коллекции. Обычно эта связь устанавливается через конструктор.
- Реализуйте методы получения итератора в конкретных классах коллекций. Они должны создавать новый итератор того класса, который способен работать с данным типом коллекции. Коллекция должна передавать ссылку на собственный объект в конструктор итератора.
- В клиентском коде и в классах коллекций не должно остаться кода обхода элементов. Клиент должен получать новый итератор из объекта коллекции каждый раз, когда ему нужно перебрать её элементы.
Преимущества и недостатки
- Упрощает классы хранения данных.
- Позволяет реализовать различные способы обхода структуры данных.
- Позволяет одновременно перемещаться по структуре данных в разные стороны.
- Не оправдан, если можно обойтись простым циклом.
Отношения с другими паттернами
- Вы можете обходить дерево Компоновщика, используя Итератор.
- Фабричный метод можно использовать вместе с Итератором, чтобы подклассы коллекций могли создавать подходящие им итераторы.
- Снимок можно использовать вместе с Итератором, чтобы сохранить текущее состояние обхода структуры данных и вернуться к нему в будущем, если потребуется.
- Посетитель можно использовать совместно с Итератором. Итератор будет отвечать за обход структуры данных, а Посетитель — за выполнение действий над каждым её компонентом.
Примеры реализации паттерна

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

Эта статья является частью нашей электронной книги Погружение в Паттерны Проектирования.

- Премиум контент
- Книга о паттернах
- Курс по рефакторингу
- Введение в рефакторинг
- Чистый код
- Технический долг
- Когда рефакторить
- Как рефакторить
- Раздувальщики
- Длинный метод
- Большой класс
- Одержимость элементарными типами
- Длинный список параметров
- Группы данных
- Операторы switch
- Временное поле
- Отказ от наследства
- Альтернативные классы с разными интерфейсами
- Расходящиеся модификации
- Стрельба дробью
- Параллельные иерархии наследования
- Комментарии
- Дублирование кода
- Ленивый класс
- Класс данных
- Мёртвый код
- Теоретическая общность
- Завистливые функции
- Неуместная близость
- Цепочка вызовов
- Посредник
- Неполнота библиотечного класса
- Составление методов
- Извлечение метода
- Встраивание метода
- Извлечение переменной
- Встраивание переменной
- Замена переменной вызовом метода
- Расщепление переменной
- Удаление присваиваний параметрам
- Замена метода объектом методов
- Замена алгоритма
- Перемещение метода
- Перемещение поля
- Извлечение класса
- Встраивание класса
- Сокрытие делегирования
- Удаление посредника
- Введение внешнего метода
- Введение локального расширения
- Самоинкапсуляция поля
- Замена простого поля объектом
- Замена значения ссылкой
- Замена ссылки значением
- Замена поля-массива объектом
- Дублирование видимых данных
- Замена однонаправленной связи двунаправленной
- Замена двунаправленной связи однонаправленной
- Замена магического числа символьной константой
- Инкапсуляция поля
- Инкапсуляция коллекции
- Замена кодирования типа классом
- Замена кодирования типа подклассами
- Замена кодирования типа состоянием/стратегией
- Замена подкласса полями
- Разбиение условного оператора
- Объединение условных операторов
- Объединение дублирующихся фрагментов в условных операторах
- Удаление управляющего флага
- Замена вложенных условных операторов граничным оператором
- Замена условного оператора полиморфизмом
- Введение Null-объекта
- Введение проверки утверждения
- Переименование метода
- Добавление параметра
- Удаление параметра
- Разделение запроса и модификатора
- Параметризация метода
- Замена параметра набором специализированных методов
- Передача всего объекта
- Замена параметра вызовом метода
- Замена параметров объектом
- Удаление сеттера
- Сокрытие метода
- Замена конструктора фабричным методом
- Замена кода ошибки исключением
- Замена исключения проверкой условия
- Подъём поля
- Подъём метода
- Подъём тела конструктора
- Спуск метода
- Спуск поля
- Извлечение подкласса
- Извлечение суперкласса
- Извлечение интерфейса
- Свёртывание иерархии
- Создание шаблонного метода
- Замена наследования делегированием
- Замена делегирования наследованием
- Введение в паттерны
- Что такое Паттерн?
- История паттернов
- Зачем знать паттерны?
- Критика паттернов
- Классификация паттернов
- Фабричный метод
- Абстрактная фабрика
- Строитель
- Прототип
- Одиночка
- Адаптер
- Мост
- Компоновщик
- Декоратор
- Фасад
- Легковес
- Заместитель
- Цепочка обязанностей
- Команда
- Итератор
- Посредник
- Снимок
- Наблюдатель
- Состояние
- Стратегия
- Шаблонный метод
- Посетитель
- C#
- C++
- Go
- Java
- PHP
- Python
- Ruby
- Rust
- Swift
- TypeScript