Итераторы и генераторы в 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)) data.append('hi') print(next(i)) print(next(i)) print(next(i)) print(next(i)) # Output: # 1 # 2 # 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Почему стоит двойное подчеркивание перед и после __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Генераторы итераторы python различия[0] input_image, input_sequence, output_word create_sequences(tokenizer, max_length, description_list, feature) yield ([input_image, input_sequence], output_word)Итак, в этой статье мы рассмотрели основы итераторов и генераторов, способы их реализации и то, когда их стоит использовать.