2026, Jan 02 00:01
Одно выражение для цепочки x, f(x), f(f(x)) в Python
Как в Python получить последовательность x, f(x), f(f(x)) без лишних вызовов: списковое включение с оператором присваивания вместо генератора и accumulate.
Построение последовательности x, f(x), f(f(x)), … — частый микропаттерн в Python, но «очевидные» подходы нередко несут издержки: лишний вызов функции, неуклюжая попытка приспособить функциональные помощники или ловушки с именами. Ниже — компактный, идиоматичный способ сделать это в одном выражении, обходя типичные подводные камни.
Обзор задачи
Задача — получить конечный префикс итеративного применения функции, начиная с начального значения. Для наглядности возьмём удвоение по модулю 99. Нужны первые N элементов цепочки, стартующей с 1: 1, f(1), f(f(1)), …, где f(v) = (2 * v) % 99.
Базовые варианты, которые выглядят неплохо, но имеют минусы
Прямолинейный генератор работает, но он вызывает функцию на один раз больше, чем нужно, получая значение, которое ни разу не возвращается. Этого лишнего вызова можно избежать.
def run_chain(seed, step, count=10):
for _ in range(count):
yield seed
seed = step(seed)
run_chain(1, lambda t: (2 * t) % 99)
Ещё вариант — приспособить itertools.accumulate с бинарной лямбдой, игнорирующей второй аргумент. Это срабатывает, но выглядит натянуто: accumulate предназначен для бинарной агрегации по входному итерируемому объекту.
from itertools import accumulate
list(accumulate([None] * 10, lambda a, b: 2 * a, initial=1))
Во всём этом шаблоне второй параметр — балласт. Если всё же идти этим путём, намерение лучше обозначить явно, использовав заглушку для неиспользуемого аргумента: lambda a, _: 2 * a.
Однострочник, который делает ровно то, что нужно
Списковое включение с оператором присваивания позволяет один раз инициализировать последовательность, а затем многократно применять функцию без лишних действий. Счётчик определяет, установить ли начальное значение или применить шаговую функцию.
h = lambda u: (2 * u) % 99
[s := h(s) if k else 1 for k in range(10)]
В результате получаем нужную последовательность:
[1, 2, 4, 8, 16, 32, 64, 29, 58, 17]
Если не хотите жёстко задавать начальное значение и предпочитаете явно передавать seed, сохраните ту же идею и подставьте в первый элемент переданный seed:
h = lambda u: (2 * u) % 99
start = 1
[s := h(s) if idx else start for idx in range(10)]
Почему предыдущие варианты неидеальны
Шаблон с генератором считает лишний шаг, который не используется: после выдачи последнего элемента он ещё раз применяет step перед завершением цикла. Обходной путь с accumulate навязывает бинарную сигнатуру и отбрасывает параметр лишь ради соответствия API, из‑за чего теряется смысл. Списковое включение с оператором присваивания обходит обе проблемы: начальное значение задаётся ровно один раз, далее идёт чистая цепочка применений функции.
Практические замечания для чистого кода
Следите за именами. Называть функцию iter — плохая идея: iter уже встроенная. Выбирайте понятные, не конфликтующие варианты — например, iterate_function, run_chain или любой другой описательный вариант, чтобы не затмевать встроенное имя и не путать читателя. И если всё же решите использовать accumulate в таком контексте, явно помечайте игнорируемый параметр подчёркиванием — так намерение заметнее.
Вывод
Нужна компактная последовательность вида x, f(x), f(f(x)), … в одном выражении? Списковое включение с оператором присваивания выходит и точным, и читаемым. Оно избавляет от лишнего финального вызова наивного генератора и от искусственных трюков с accumulate, при этом инициализация остаётся явной.