На этом шаге мы усовершенствуем созданный генератор.
Этот раздел мы начали с того, что еще раз написали бесконечный генератор. Сейчас вы, вероятно, задаетесь вопросом, как написать генератор, который через некоторое время прекращает порождать значения вместо того, чтобы без конца продолжать это делать.
Напомним, что в нашем итераторе на основе класса мы смогли подать сигнал об окончании итераций путем вызова исключения StopIteration вручную. Поскольку генераторы полностью совместимы с итераторами на основе класса, за сценой будет по-прежнему происходить то же самое.
К счастью, на этот раз мы будем работать с более приятным интерфейсом. Генераторы прекращают порождать значения, как только поток управления возвращается из функции-генератора каким-либо иным способом, кроме инструкции yield. Это означает, что вам больше вообще не нужно заботиться о вызове исключения StopIteration!
Приведем пример:
>>> def repeat_three_times(value): yield value yield value yield value
Обратите внимание: эта функция-генератор не содержит никакого цикла. В действительности она проста как божий день и состоит всего из трех инструкций yield. Если yield временно приостанавливает выполнение функции и передает значение назад источнику вызова, то что произойдет, когда мы достигнем конца этого генератора? Давайте узнаем:
>>> for x in repeat_three_times('Всем привет'): print(x) Всем привет Всем привет Всем привет
Как вы, возможно, и ожидали, этот генератор прекратил порождать новые значения после трех итераций. Можно предположить, что он это сделал путем вызова исключения StopIteration, когда исполнение достигло конца функции. Но чтобы быть до конца уверенными, давайте подтвердим это еще одним экспериментом:
>>> iterator = repeat_three_times('Всем привет') >>> next(iterator) 'Всем привет' >>> next(iterator) 'Всем привет' >>> next(iterator) 'Всем привет' >>> next(iterator) Traceback (most recent call last): . . . . StopIteration >>> next(iterator) Traceback (most recent call last): . . . . StopIteration
Этот итератор вел себя именно так, как мы и ожидали. Как только мы достигаем конца функции-генератора, он начинает вызывать StopIteration, сигнализируя о том, что у него больше нет значений, которые он мог бы предоставить.
Давайте вернемся к еще одному примеру из раздела об итераторах. Класс BoundedIterator реализовал итератор, который будет повторять значение, заданное определенное количество раз:
>>> class BoundedRepeater: def __init__(self, value, max_repeats): self.value = value self.max_repeats = max_repeats self.count = 0 def __iter__(self): return self def __next__(self): if self.count >= self.max_repeats: raise StopIteration self.count += 1 return self.value
Почему бы не попробовать реализовать класс BoundedRepeater заново как функцию-генератор? Первая попытка:
>>> def bounded_repeater(value, max_repeats): count = 0 while True: if count >= max_repeats: return count += 1 yield value
Мы преднамеренно сделали цикл while в этой функции несколько громоздким. Хотели продемонстрировать, как вызов инструкции return из генератора приводит к остановке итераций с исключением StopIteration. Мы вскоре подчистим и еще немного упростим эту функцию-генератор, но сначала давайте испытаем то, что у нас есть сейчас:
>>> for x in bounded_repeater('Привет', 4): print(x) Привет Привет Привет Привет
Великолепно! Теперь у нас есть генератор, который прекращает порождать значения после настраиваемого количества повторений. Он использует инструкцию yield, чтобы передавать значения назад до тех пор, пока он наконец не натолкнется на инструкцию return и итерации не прекратятся.
Как обещали, мы можем упростить этот генератор еще больше. Воспользуемся тем, что в конец каждой функции Python добавляет неявную инструкцию return None. И вот как будет выглядеть наша окончательная реализация:
>>> def bounded_repeater(value, max_repeats): for i in range(max_repeats): yield value
Не стесняйтесь подтвердить, что этот упрощенный генератор по-прежнему работает таким же образом. Учитывая все обстоятельства, мы прошли путь от 12-строчной реализации в классе BoundedRepeater до трехстрочной реализации на основе генератора, обеспечив ту же самую функциональность. А это 75%-ное сокращение количества строк кода - неплохо!
Как вы только что убедились, генераторы помогают "абстрагироваться от" большей части шаблонного кода, который в других обстоятельствах был бы необходим во время написания итераторов на основе класса. Они способны очень облегчить вашу программистскую жизнь и позволяют писать более чистые, короткие и удобные в сопровождении итераторы. Функции-генераторы представляют собой отличное функциональное средство языка Python, и вам следует решительно и смело использовать их в своих собственных программах.
На следующем шаге мы подитожим изученный материал.