Хватит использовать lambda выражения в Python

Тяжело проводить глубокое изучение классов Python, не затрагивая вопрос лямбда выражений. Я практически всегда получаю кучу вопросов о них. Люди часто замечают их в коде на StackOverflow, или в коде коллеги (который, к слову, также может быть кодом с StackOverflow).

Есть вопросы по Python?

На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!

Telegram Чат & Канал

Вступите в наш дружный чат по Python и начните общение с единомышленниками! Станьте частью большого сообщества!

Паблик VK

Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!

Мне задают много вопросов про lambda и я не решаюсь рекомендовать студентам лямбда-выражения в Python. У меня была неприязнь к лямбда-выражениям на протяжении многих лет, и за годы обучения Python, это ощущение только усиливалось.

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

Лямбда-выражения в Python: кто они?

Лябмда-выражения — это особый синтаксис в Python, необходимый для создания анонимных функций. Я назову синтаксис лямбда как лямбда-выражение, а получаемую функцию — лямбда-функцию.

Лямбда-выражения в Python позволяют функции быть созданной и переданной (зачастую другой функции) в одной строчке кода.

Лямдба-выражения позволяют нам превратить следующий код:

Вот в такой код:

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

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

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

Где они используются?

Как правило, lambda-выражения используются при вызове функций (или классов), которые принимают функцию в качестве аргумента.

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

Таким образом, сортировка — отличный пример места, в котором используются лямбда-выражения:

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

Функция sorted — не единственная область применения лямбда-выражений, но встречается наиболее часто.

Преимущества и недостатки lambda

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

Лямбда-выражения отличаются от def по следующим признакам:

  1. Их можно передавать мгновенно (переменная не нужна);
  2. Они могут содержать только одну строку кода;
  3. Они возвращаются автоматически;
  4. Они не могут содержать docstring и иметь наименование;
  5. Синтаксис отличается и мало знаком.

Тот факт, что lambda-выражения могут свободно передаваться является главным преимуществом. Автоматический возврат — тоже удобно, но его с трудом можно назвать весомым преимуществом, на мой взгляд.

Наличие единственной строки кода я не вижу ни как преимуществом, ни как недостатком. Тот факт, что лямбда функции не имеют docstring и не имеют наименований — может быть неудобно, а незнакомый синтаксис — стать проблемой для новичков в Python.

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

Lambda используется неправильно и слишком часто

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

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

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

Неправильное использование: наименование лямбда-выражений

PEP8, являющийся официальным руководством Python, рекомендует писать код таким образом:

Указанный вверху оператор создает анонимную функцию и затем присваивает её переменной. Этот код игнорирует причину, по которой лямбда функции являются полезными:

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

Если вы хотите создать однострочную функцию и хранить ее в переменной, вам нужно использовать def вместо этого:

Это рекомендуется в PEP8, так как названные функции — это простой и понятный элемент. Также полезно давать функции правильное наименование, чтобы упростить лечение возможных багов. В отличие от функций, определенных при помощи def, функции лямбда никогда не имеют названий (название всегда будет <lambda>):

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

Неправильное использование: ненужные вызовы функций

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

Рассмотрим следующий код в качестве примера:

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

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

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

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

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

Злоупотребление: простые, но нетривиальные функции

Часто встречается использование лямбда-выражений для создания функции, которая возвращает несколько значений в кортеж python:

Используемая функция-ключ в этом примере помогает нам распределить цвета по длине в соответствии с их названиями.

Этот код аналогичен приведенному выше, но я нахожу его более читаемым:

Этот код немного нуднее, но на мой взгляд, наименование нашей функции делает более понятным по каким признакам мы собираемся проводить сортировку. Мы не просто сортируем по длине и не по цвету: мы сортируем по обеим признакам.

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

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

Злоупотребление: когда множество строк должны помочь

Иногда такой аспект, как «просто одна строка» в лямбда-выражениях заставляет нас делать код более запутанным. Например, вот так:

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

Распаковка кортежей может улучшить читаемость, используя детально прописанный индексный поиск. Использование лямбда-выражений нередко означает пожертвовать определенными языковыми возможностями Python, особенно теми, которые требуют несколько строк кода (таких, как дополнительный оператор присваивания).

Злоупотребление: лямбда с map и filter

Функции Python map и filter практически всегда идут рука об руку с лямбда-выражениями. В StackOverflow обычным делом считается ответом на вопрос «что такое лямбда?» следующий код:

Такие примеры кажутся мне слегка озадачивающими, так как я практически никогда не использую map и filter у себя в коде.

Функции Python map и filter используются для создания цикла над итерацией и создания новой итерации, которая либо слегка меняет каждый элемент, или фильтрует итерацию только по отношению к тем элементам, которые соответствуют определенным условиям.

Мы можем выполнить обе эти задачи при помощи списка или выражений генератора:

Лично я предпочитаю видеть указанные выражения генератора написанные над несколькими строками кода, но даже эти однострочные выражения генератора читаются легче, чем вызовы filter и map.

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

Неправильное использование: иногда вам даже не нужно передавать функцию

Что на счет тех случаев, где вам нужно передать куда-нибудь функцию, которая выполняет одну единственную операцию?

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

Этот код вносит числа в список. Но есть лучший способ сделать это:

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

Функция sum, наряду с рядом других специализированных инструментов Python, легко упустить из виду. Но я бы посоветовал вам найти более точечные инструменты при необходимости, так как они также хорошо сказываются на читаемости кода.

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

Злоупотребление: использование лямбды для очень простых операций

Скажем, вместо того, чтобы сложить числа, мы их умножаем:

Это лямбда-выражение необходимо, так как мы не можем передавать оператор * также, как если бы он был функцией. Если есть функция, являющаяся эквивалентом *, мы можем передать ее функции reduce вместо этого.

Стандартная библиотека Python также имеет целый модуль, созданный для того, чтобы решить эту проблему:

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

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

Есть itemgetter, который помогает получить доступ к индексам списка (или последовательности) ключей словаря:

Также есть attrgetter для получения доступа к атрибутам объекта:

И methodcaller для вызова методов объекта:

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

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

Функция, которая принимает функцию в качестве аргумента называется функцией высокого порядка. Функции высокого порядка — это такой тип функций, которым мы обычно передаем наши функции лямбда.

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

Сравните следующее:

С этим кодом:

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

Любой, кто прошел те или иные курсы Python, вполне может понять, что делает функция multiply_all, в то время как комбинация reduce/lambda будет, скорее всего, чем-то более загадочным для многих программистов Python.

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

Стоит ли вообще использовать лямбда-выражения?

Я нахожу использование лямбда-выражений несколько проблематичным, и вот почему:

  • Лямбда-выражения имеют незнакомый, или непонятный синтаксис для многих программистов Python;
  • По сути, лямбда-функциям не хватает наименований или документации. Иными словами, непосредственное чтение такого кода — единственный способ понять, что они делают;
  • Лямбда-выражения могут содержать в себе только один оператор, так что определенные языковые возможности, улучшающие читаемость, такие как распаковка кортежей не могут быть использованы;
  • Функции лямбда зачастую можно заменить уже существующими функциями стандартной библиотеки;
  • Лямбда-выражении редко на столько же быстро читаемые, как хорошо названные  def функции.

В то время как оператор def зачастую более понятен, в Python также имеется ряд возможностей, которые можно использовать для замены лямбда-выражений, включая особый синтаксис, встроенные функции (sum) и функции стандартной библиотеки (в модуле operators).

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

  1. Вы выполняете тривиальную операцию, т. е. функции не нужно название;
  2. Наличие лямбда-выражения делает ваш код понятнее, чем другие функции;
  3. Вы знаете, что у вас нет функции, которая делает то, что вам нужно;
  4. Каждый человек в вашей команде понимает лямбда-выражения и вы договорились использовать их.

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