Python: Декларативное программирование
2026-02-26 20:01 Diff

Python создавался с прицелом на активное и эффективное использование встроенных коллекций. Поэтому при выборе подхода к решению задач Python склоняет программиста к определенному стилю работы. Сам язык подталкивает использовать императивный стиль в сочетании с процедурным и объектно-ориентированным программированием.

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

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

Процедурное решение

В процедурном решении программа разбита на подпрограммы, изменяющие состояние вместо возврата значения. Код будет выглядеть так:

Это решение достаточно эффективно — оно расходует память только на копию исходного списка, потом сжимает ее и сортирует по месту. У этого решения предсказуемый расход памяти, не зависящий от входных данных. А еще после применения filter_and_sort() данные можно дальше обрабатывать так же по месту, но уже другими процедурами.

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

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

Функциональное решение

Напишем функциональный вариант с элементами декларативного стиля:

Код описывает, какой результат мы хотим получить:

  • print() — напечатанное
  • '\n'.join(...) — объединение через перевод строки
  • map(str, ...) — последовательность строк
  • sorted(set(...))— полученных из отсортированного множества исходных чисел

Обратите внимание, что в коде нет ни одной переменной. Ничего не нужно защитно копировать, и самого кода гораздо меньше.

Попробуем проанализировать, как этот код работает:

  • Создается промежуточное множество set()
  • Внутри sorted() создается список — не обычный list(), но все равно копия
  • Для каждого итогового числа будет создано по строке
  • Функция '\n'.join() соберет строку, размер которой будет равен сумме длин всех строк с числами и количеству переводов строк

Как видите, за лаконичность кода приходится платить ресурсами.

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

Истина где-то посередине

Функциональное решение читается хорошо, если разработчик в принципе знаком с таким стилем. Читаемость кода — важная характеристика, поэтому при небольшом объеме входных данных можно выбирать такой вариант.

Процедурное решение читается достаточно тяжело: нужно буквально выполнять код в уме. Еще в такой код сложнее вносить изменения — он слишком специфичен и завязан на текущую задачу. А еще нужно помнить, что процедура filter_and_sort() модифицирует свой аргумент.

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

Например, в функциональном решении можно применить цикл:

Этот код не создает промежуточных строк, потому что команда print() и так умеет выводить числа. Большая строка не склеивается перед выводом — это самая заметная экономия.

Усложним ситуацию и представим, что все входные элементы уникальны. В таком случае множество будет копией исходного списка — внутри sorted() будет еще одна копия. В итоге у нас будет две копии.

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

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

В большинстве случаев программисты на Python выбирают функциональный подход и дорабатывают его. Так они приходят к балансу — код все еще остается достаточно компактным, и работает вполне эффективно.