Итераторы и генераторы в Python
Что такое генераторы и итераторы в Python и как их эффективно использовать?
Начнем с итераторов
Вы когда-нибудь задумывались, как работает перебор списка for element in list_
? За это отвечают итераторы. Чтобы понять итераторы и генераторы, для начала мы должны понять, что такое итератор и что значит «итерируемый».
Итератор — это объект, который управляет перебором по ряду значений (итерируемых). Для объекта итератора i
каждый вызов встроенной функции next(i)
вернет следующий элемент составного объекта. Если в объекте больше нет элементов, вызывается исключение StopIteration
, которое показывает, что достигнут конец перебора. Мы можем создать итератор с помощью iter(obj)
.
Итерируемым объект — тот, по которому итератор выполняет итерацию, например, список или кортеж. Посмотрим синтаксис.
data = [1, 2, 3] i = iter(data) print(next(i)) print(next(i)) print(next(i))
Синтаксис цикла for
в Python (for i in data
) под капотом просто реализует описанный выше процесс. Создается объект итератора для данной последовательности и затем последовательно вызывается next(iterator)
, пока не возникнет исключение StopIteration
. Что произойдет, если добавить элемент в список перед вызовом функции next
? Элемент будет добавлен в список и может быть получен последним вызовом функции.
data = [1, 2, 3, 4, 5] i = iter(data) print(next(i)) print(next(i)) # 1 # 2 data.append('hi') print(next(i)) print(next(i)) print(next(i)) print(next(i)) # 3 # 4 # 5 # hi
Имплементируем итератор внутри класса:
"""Этот код возвращает все чётные числа, начиная с нуля и до введенного числа""" class EvenNum: def __init__(self, num = 0): self.num = num self.x = 0 def __iter__(self): return self def __next__(self): if self.x<=self.num: even_num = self.x self.x += 2 return even_num else: raise StopIteration # можно запустить так: for num in EvenNum(10): # цикл for просто автоматизирует процесс итерации print(num) # или можно сделать так: obj = EvenNum(10) i = iter(obj) # здесь вызов iter(obj) вернет self, т.е. сам obj. # поэтому, если вы запустите obj is i, он отобразит True print(next(i)) print(next(i)) # и так далее # можно и так obj2 = EvenNum(10) i = obj2.__iter__() print(i.__next__()) print(i.__next__()) # и так далее # и ещё так: obj3 = EvenNum(10) print(next(obj3)) # вы можете напрямую вызвать метод next с объектом класса, # потому что iter вернет тот же объект класса, # с которым мы вызываем iter. iter.print(next(obj3)) # и так далее # проверьте, что obj2 и i – один и тот же объект print(obj2 is i)
Почему стоит двойное подчеркивание перед и после __next__
и __iter__
методов? Дело в том, что они являются dunder (Double UNDERscore) методами, также называемыми магическими методами. Они предопределены для встроенных классов в Python, но могут быть переопределены с целью реализации нужного нам полиморфизма.
В коде выше __iter__
метод возвращает self
, который является тем же объектом класса, с которым мы вызываем __iter__
. __iter__
метод возвращает self
через __iter__
, потому что мы переопределили dunder-метод iter
, и таким образом мы можем создавать итератор для нашего класса и управлять его поведением. Каждый вызов метода __next__
вернет следующее чётное число. Основной механизм цикла for num in EvenNum(10):
теперь переопределен, поскольку мы переопределили __iter__
и __next__
методы нашего класса, поэтому каждая итерация в цикле for
будет возвращать следующее четное число и остановится, как только возникнет исключение StopIteration
, вызванное нашим __next__
методом.
Генераторы
Генераторы считаются самым удобным способом для создания итераторов в Python. Синтаксис генераторов аналогичен синтаксису традиционной функции, но вместо оператора return
, генераторы используют yield
, для указания каждого элемента последовательности. Рассмотрим пример ниже.
def factors_return(n): results = [] for k in range(1, n + 1): if n % k == 0: results.append(k) return results def factors_yield(n): for k in range(1, n + 1): if n % k == 0: yield k
В коде выше можно видеть использование ключевого слова yield
. Python использует его для того, чтобы различать обычную функцию и генератор. Функция factors_return(n)
вернет список, содержащий все значения, тогда как factor_yield(n)
создаст последовательность значений, а ключевое слово yield
используется для их перебора.
Если мы напишем for factor in factors(100):
, будет создан экземпляр нашего генератора, и для каждой итерации цикла Python будет выполнять нашу процедуру, представленную в factor_yield()
, до тех пор, пока оператор yield
не укажет следующее значение. В этот момент процедура временно приостанавливается и возобновляется только при запросе другого значения. Когда весь поток управления достигает естественного конца нашей процедуры, автоматически возникает исключение StopIteration
.
Другими словами, оператор yield
прерывает выполнение процедуры и отправляет значение обратно вызывающей стороне, но сохраняет состояние, достаточное для возобновления процедуры с того места, где она была остановлена, и когда цикл for
запрашивает следующее значение, процедура возобновляет свою работу сразу после последнего выполнения yield
. В отличие от этого, factor_return(n)
вернёт список всех значений вместо создания последовательности, поэтому может занимать много памяти, если будет передано большое число, например, 100000000000000000000000.
Зачем использовать генераторы и итераторы?
Преимущество итераторов и генераторов заключается в том, что они используют отложенные вычисления, чего нет в обычных функциях. Результаты вычисляются только по запросу, и весь объект не обязательно должен находиться в памяти одновременно. Генератор может эффективно производить бесконечный ряд значений. Рассмотрим пример ниже.
# последовательность Фибоначчи через генератор def fibonacci_generator(n): a = 0 b = 1 for i in range(n): yield a future = a + b a = b b = future for i in fibonacci_generator(10000): print(i) # последовательность Фибоначчи через список def fibonacci_list(n): a = 0 b = 1 list_ = [] count = 0 while count < n: list_.append(a) future = a + b a = b b = future count += 1 return list_ print(fibonacci_list(10000))
В приведенном выше коде функция fibonacci_list
создаст список и сохранит в нём все элементы последовательности Фибоначчи. Этот список будет увеличиваться по мере добавления в него новых элементов, в результате чего он будет занимать всё больше и больше места, что рано или поздно приведёт к проблемам с памятью.
Функция fibonacci_generator
, в свою очередь, создаст последовательность. Элементы Фибоначчи не будут храниться в списке, один за другим они будут возвращаться после каждой итерации цикла for
, и, таким образом, объём памяти не будет увеличиваться. Так мы можем создавать бесконечные последовательности с помощью генераторов.
Генераторы также широко используются при загрузке данных для задач машинного и глубокого обучения. Это связано с тем, что, например, весь набор данных, содержащий тысячи изображений, не может быть загружен в память. В этом случае спасают генераторы. Рассмотрим небольшой фрагмент кода из проекта распознавания образов с использованием глубокого обучения.
def data_generator(descriptions, features, tokenizer, max_length): while 1: for key, description_list in descriptions.items(): feature = features[key][0] input_image, input_sequence, output_word create_sequences(tokenizer, max_length, description_list, feature) yield ([input_image, input_sequence], output_word)
Итак, в этой статье мы рассмотрели основы итераторов и генераторов, способы их реализации и то, когда их стоит использовать.
Оригинал: medium.com
Перевод и адаптация: @honey_valy
👉🏻Подписывайтесь на PythonTalk в Telegram 👈🏻