Зарегистрируйтесь для доступа к 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(str.join('\n', map(str, sorted(set(INPUT)))))

Код буквально можно прочитать как "(результат это) напечатанное (print()) объединение через перевод строки str.join('\n', ...) последовательности строк (map(str, ...)), полученных из отсортированного множества (sorted(set(...))) исходных чисел.". И ни одной переменной, ничего не нужно защитно копировать, и кода гораздо меньше!

Однако, если проанализировать то, как этот код работает, мы увидим, что здесь, как минимум

  • создаётся промежуточное множество set()
  • внутри sorted() создаётся список — не обычный list(), но всё равно копия
  • для каждого итогового числа будет создано по строке
  • str.join() соберёт строку размером в сумму длин всех строк с числами плюс переводы строк

А в Python2 map() ещё и список возвращал вместо итератора! Хорошо, что тут мы на этом списке экономим.

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

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

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

Процедурное решение читается достаточно тяжело: нужно буквально "выполнять код в уме". К тому же в такой код сложнее вносить изменения — слишком уж он специфичен и завязан на текущую задачу. А ещё нужно помнить о том, что процедура 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 выбирают варианты вроде улучшенного функционального: код всё ещё достаточно компактен и уже достаточно эффективен.


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

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

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

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

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

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Python-разработчик
Профессия
Разработка веб-приложений на Django
8 декабря 8 месяцев

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

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

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

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»