Статьи
June 15, 2022

Итераторы и генераторы в 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 👈🏻

👨🏻‍💻Чат PythonTalk в Telegram💬

🍩 Поддержать канал 🫶