Шаг 161.
Python: тонкости программирования.
Циклы и итерации. Цепочки итераторов

    На этом шаге мы рассмотрим особенности формирования таких цепочек.

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

    Если вы воспользуетесь преимуществами функций-генераторов и выражений-генераторов Python, то вы в мгновение ока будете строить сжатые и мощные цепочки итераторов. На этом шаге вы узнаете, как этот технический прием выглядит на практике и как вы можете его применять в своих собственных программах.

    В качестве краткого резюме: генераторы и выражения-генераторы представляют собой синтаксический сахар для написания итераторов на Python. Они абстрагируются от большей части шаблонного кода, необходимого во время написания итераторов на основе класса.

    В то время как обычная функция производит одно-единственное возвращаемое значение, генераторы производят последовательность результатов. Можно сказать, что они генерируют поток значений на протяжении своего жизненного цикла.

    Например, можно определить следующий ниже генератор, который производит серию целочисленных значений от одного до восьми, поддерживая нарастающий счетчик и выдавая новое значение всякий раз, когда с ним вызывается функция next():

>>> def integers():
	for i in range(1, 9):
		yield i

    Вы можете подтвердить такое поведение, выполнив данный ниже фрагмент кода в интерпретаторе REPL Python:

>>> chain = integers()
>>> list(chain)
[1, 2, 3, 4, 5, 6, 7, 8]

    Пока что не очень интересно. Но сейчас мы быстро это изменим. Дело в том, что генераторы могут быть "присоединены" друг к другу, благодаря чему можно строить эффективные алгоритмы обработки данных, которые работают как конвейер.

    Вы можете взять "поток" значений, выходящих из генератора integers(), и направить их в еще один генератор. Например, такой, который принимает каждое число, возводит его в квадрат, а затем передает его дальше:

>>> def squared(seq):
	for i in seq:
		yield i * i

		

    Ниже показано, что будет теперь делать наш "конвейер данных", или "цепочка генераторов":

>>> chain = squared(integers())
>>> list(chain)
[1, 4, 9, 16, 25, 36, 49, 64]

    И мы можем продолжить добавлять в этот конвейер новые структурные блоки. Данные текут только в одном направлении, и каждый шаг обработки защищен от других четко определенным интерфейсом.

    Это похоже на то, как работают конвейеры в UNIX. Мы состыковываем последовательность процессов в цепочку так, чтобы результат каждого процесса подавался непосредственно на вход следующего.

    Почему бы в наш конвейер не добавить еще один шаг, который инвертирует каждое значение, а потом передает его на следующий шаг обработки в цепи:

>>> def negated(seq):
	for i in seq:
		yield -i

		

    Если мы перестроим нашу цепочку генераторов и добавим negated() в конец, то вот что мы получим на выходе:

>>> chain = negated(squared(integers()))
>>> list(chain)
[-1, -4, -9, -16, -25, -36, -49, -64]

    Обработка данных происходит по одному элементу за один раз. Буферизация между шагами обработки в цепочке отсутствует:

  1. Генератор integers выдает одно-единственное значение, скажем, 3.
  2. Это значение "активирует" генератор squared, который обрабатывает значение и передает его на следующую стадию как 3 * 3 = 9.
  3. Квадрат целого числа, выданный генератором squared, немедленно передается в генератор negated, который модифицирует его в -9 и выдает его снова.

    Вы можете продолжать расширять эту цепочку генераторов, чтобы отстроить конвейер обработки со многими шагами. Он по-прежнему будет выполняться эффективно и может легко быть модифицирован, потому что каждым шагом в цепочке является отдельная функция-генератор.

    Каждая отдельная функция-генератор в этом конвейере обработки довольно сжатая. С помощью небольшой уловки мы можем сжать определение этого конвейера еще больше, не сильно жертвуя удобочитаемостью:

>>> integers = range(8)
>>> squared = (i * i for i in integers)
>>> negated = (-i for i in squared)

    Обратите внимание, как заменяется каждый шаг обработки в цепочке на выражение-генератор, строящийся на выходе из предыдущего шага. Этот программный код эквивалентен цепочке генераторов, которые мы построили выше на этом шаге:

>>> negated
<generator object <genexpr> at 0x000001BCE7D18270>
>>> list(negated)
[-1, -4, -9, -16, -25, -36, -49, -64]

    Единственным недостатком применения выражений-генераторов является то, что их не получится сконфигурировать с использованием аргументов функции и вы не сможете повторно использовать то же самое выражение-генератор многократно в том же самом конвейере обработки.

    Но, безусловно, во время сборки конвейеров вы можете свободно комбинировать выражения-генераторы и обычные генераторы на свой вкус. В случае с составными конвейерами это поможет улучшить удобочитаемость.

    На следующем шаге мы подитожим изученный материал.




Предыдущий шаг Содержание Следующий шаг