July 15, 2018

Генераторы

В этой статье я хочу рассказать о такой интересной особенности Python, которой нет во многих других языках, и которая носит название "генераторы". Что, если я вам скажу, что вот такой код, при некоторых условиях, может выполняться до бесконечности?

for number in fibonacci_numbers:
	print(number)

Наверное у вас появится закономерный вопрос: "Как же так, ведь fibonacci_numbers должен быть ограничен по длине и когда-нибудь цикл закончится?". И вы были бы правы, будь эта переменная списком или чем-то подобным, но в примере выше, она - генератор и не имеет конца. Заинтересовались? Тогда давайте разберёмся, как работает эта сущность в Python.

Для начала, поймём, как работает цикл for. На самом деле, всё, что он делает:

  1. Вызывает на нашем объекте "магический" метод __iter__ и получает специальный объект-итератор, у которого есть метод __next__
  2. При каждой итерации, этот метод вызывается и значение переменной, указанной нами в цикле обновляется.
  3. Итерация прекращается, когда метод __next__ вызывает исключение StopIteration.

То есть, мы можем сделать генератор своими руками. Прикрепляю ссылку на Pastebin потому что там есть подсветка синтаксиса и копировать код оттуда проще.

Код на Pastebin

class FibonacciGenerator:                            
  """                                     
  Класс, объект которого мы будем создавать                  
  """                                     
  class FibonacciIterator:                           
    """                                   
    Вспомогательный класс, который реализует метод __next__         
    Этот метод вызывается на каждой итерации for               
    """                                   
    def __init__(self):                           
      """                                 
      Начинаем нашу последовательность с двух единиц            
      """                                 
      self.first_number = 1                        
      self.second_number = 1                        
      self.iterations = 0                         
                                         
    def __next__(self):                           
      self.iterations += 1                         
      if self.iterations <= 2: # Первые 2 раза мы возвращаем единицу     
        return 1                             
      self.first_number, self.second_number = self.second_number, self.first_number + self.second_number
      return self.second_number                      
                                         
  def __iter__(self):                             
    return self.FibonacciIterator()                     
                                         
fib_gen = FibonacciGenerator()                          
for i in fib_gen:                                
  print(i)

Если вы запустите у себя этот код, то увидите бесконечно выводящиеся числа Фибоначи. Теперь о том, как это работает: for, когда начинает свою работу, вызывает метод __iter__ и получает объект класса FibonacciIterator. На нём, перед каждой итерацией, он запускает метод __next__ и получает следующее число, которое записывается в переменную i. Согласитесь, выглядит не очень красиво.Наверное поэтому в Python существует специальный синтаксис, призванный упростить создание генераторов. Главным средством для этого, является ключевое слово yield. Это как return, но функция не заканчивается, а её состояние запоминается и при вызове __next__ продолжается с того же места. Пример с числами Фибоначи:

Код на Pastebin

def fibonacci_generator():                            
  """                                     
  Эта функция возвращает специальный объект-генератор               
  """                                     
  yield 1                                   
  yield 1                                   
  first, second = 1, 1                             
  while True:                                 
    first, second = second, first + second                  
    yield second                               
                                         
test_generator = fibonacci_generator()                      
for i in test_generator:                             
  print(i)

Эта программа делает всё то же самое. test_generator - объект специального класса generator, который реализует то, что мы описали. Кроме yield есть ещё конструкция yield from <collection>, которая делает yield последовательно каждого элемента <collection>. Выглядит это так:

def yield_from_generator():
	yield from (1,2,3)

Объект, созданный этой функцией вернёт 1, потом 2, потом 3.

Бонус:

У генераторов есть сокращённый синтаксис, как в list comprehensions, но вместо квадратных скобок надо поставить круглые. Например вот генератор квадратов чисел от 1 до 100:

(i**2 for i in range(1, 101))

Генераторы - очень мощный инструмент, который может, при правильном применении, сильно упростить код. Используйте с умом и наслаждайтесь красотой и мощью Python!

До новых встреч в группе SnakeBlog