Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Функциональный и процедурный подход Python: Декларативное программирование

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

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

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

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

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

INPUT = [1, 2, 5, 12, 3, 5, 2, 7, 12]


def main():
    numbers = INPUT[:]
    filter_and_sort(numbers)  # Сортируем и фильтруем по месту
    for number in numbers:
        print(number)         # Выводим поэлементно в цикле


def filter_and_sort(values):
    values.sort()  # Список сортируется по месту
    previous_value = None
    index = 0
    while index < len(values):
        value = values[index]
        if value == previous_value and index > 0:
            # Элемент удаляется из списка, то есть список опять модифицируется
            values.pop(index)
        else:
            index += 1
            previous_value = value

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

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

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

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

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

INPUT = [1, 2, 5, 12, 3, 5, 2, 7, 12]


def main():
    print('\n'.join(map(str, sorted(set(INPUT)))))

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

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

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

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

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

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

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

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

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

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

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

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

def main():
    for number in sorted(set(INPUT)):
        print(number)

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

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

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

def main():
    previous_value = None
    for value in sorted(INPUT):
        if previous_value is None or value != previous_value:
            previous_value = value
            print(value)

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

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


Дополнительные материалы

  1. Функциональный подход
  2. Императивный подход

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 21 ноября

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»