Тяжело проводить глубокое изучение классов Python, не затрагивая вопрос лямбда выражений. Я практически всегда получаю кучу вопросов о них. Люди часто замечают их в коде на StackOverflow, или в коде коллеги (который, к слову, также может быть кодом с StackOverflow).
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Мне задают много вопросов про lambda и я не решаюсь рекомендовать студентам лямбда-выражения в Python. У меня была неприязнь к лямбда-выражениям на протяжении многих лет, и за годы обучения Python, это ощущение только усиливалось.
В этой статье я хочу объяснить, как я вижу лямбда-выражения и почему я рекомендую студентам избегать их использования.
Лямбда-выражения в Python: кто они?
Лябмда-выражения — это особый синтаксис в Python, необходимый для создания анонимных функций. Я назову синтаксис лямбда как лямбда-выражение, а получаемую функцию — лямбда-функцию.
Лямбда-выражения в Python позволяют функции быть созданной и переданной (зачастую другой функции) в одной строчке кода.
Лямдба-выражения позволяют нам превратить следующий код:
1 2 3 4 5 6 |
colors = ["Goldenrod", "Purple", "Salmon", "Turquoise", "Cyan"]) def normalize_case(string): return string.casefold() normalized_colors = map(normalize_case, colors) |
Вот в такой код:
1 2 3 |
colors = ["Goldenrod", "Purple", "Salmon", "Turquoise", "Cyan"]) normalized_colors = map(lambda s: s.casefold(), colors) |
Лямбда-выражения — это особый синтаксис для создания функций. Они могут иметь только один оператор и автоматически возвращают его результат.
Встроенные ограничения лямбда-выражений являются их визитной карточкой. Когда опытный программист Python видит лямбда-выражение, он знает, что оно работает с функцией, которая используется только в одном месте и выполняет одну единственную задачу.
Если вы когда-либо использовали анонимные функции в JavaScript до этого, вы можете трактовать лямбда-выражения в Python аналогичным образом, с тем исключением, что у них больше ограничений и их синтаксис заметно отличается от привычного синтаксиса функций.
Где они используются?
Как правило, lambda-выражения используются при вызове функций (или классов), которые принимают функцию в качестве аргумента.
Встроенная функция сортировки Python принимает функцию в качестве ключевого аргумента. Эта ключевая функция использует для вычисления сравнительного ключа при определении порядка сортировки элементов.
Таким образом, сортировка — отличный пример места, в котором используются лямбда-выражения:
1 2 3 4 |
colors = ["Goldenrod", "purple", "Salmon", "turquoise", "cyan"] print(sorted(colors, key=lambda s: s.casefold())) # Результат: ['cyan', 'Goldenrod', 'purple', 'Salmon', 'turquoise'] |
Приведенный выше пример возвращает указанные цвета, которые сортируются без учета регистра.
Функция sorted — не единственная область применения лямбда-выражений, но встречается наиболее часто.
Преимущества и недостатки lambda
Мое представление о лямбда-выражениях можно описать как постоянное сравнение с использованием def для определения функций. Оба этих инструмента дают нам функции, но каждый из них имеет разные ограничения и используют разный синтаксис.
Лямбда-выражения отличаются от def по следующим признакам:
- Их можно передавать мгновенно (переменная не нужна);
- Они могут содержать только одну строку кода;
- Они возвращаются автоматически;
- Они не могут содержать docstring и иметь наименование;
- Синтаксис отличается и мало знаком.
Тот факт, что lambda-выражения могут свободно передаваться является главным преимуществом. Автоматический возврат — тоже удобно, но его с трудом можно назвать весомым преимуществом, на мой взгляд.
Наличие единственной строки кода я не вижу ни как преимуществом, ни как недостатком. Тот факт, что лямбда функции не имеют docstring и не имеют наименований — может быть неудобно, а незнакомый синтаксис — стать проблемой для новичков в Python.
На мой взгляд, в целом — недостатки немного перевешивают преимущества лямбда-выражений, но что мне больше всего не нравится, это то, что их постоянно либо используют неправильно, либо используют слишком часто.
Lambda используется неправильно и слишком часто
Когда я вижу лямбда-выражения в незнакомом коде, я сразу настраиваюсь скептически. Когда они появляются, я понимаю, что если их убрать, читаемость кода только улучшится.
Иногда проблема может заключаться в том, что они используются неправильно. Под этим имеется ввиду то, что способ их использования далек от идеала и неуместен. В других случаях, лямбда-выражения используются слишком часто. Под этим подразумевается то, что их наличие приемлемо, но код вполне может быть написан иначе.
Давайте посмотрим на различные примеры того, когда лямбда-выражения используются неправильно, или слишком часто.
Неправильное использование: наименование лямбда-выражений
PEP8, являющийся официальным руководством Python, рекомендует писать код таким образом:
1 |
normalize_case = lambda s: s.casefold() |
Указанный вверху оператор создает анонимную функцию и затем присваивает её переменной. Этот код игнорирует причину, по которой лямбда функции являются полезными:
лямбда функции могут быть переданы без необходимости в предварительном присваивании переменной.
Если вы хотите создать однострочную функцию и хранить ее в переменной, вам нужно использовать def вместо этого:
1 |
def normalize_case(s): return s.casefold() |
Это рекомендуется в PEP8, так как названные функции — это простой и понятный элемент. Также полезно давать функции правильное наименование, чтобы упростить лечение возможных багов. В отличие от функций, определенных при помощи def, функции лямбда никогда не имеют названий (название всегда будет <lambda>):
1 2 3 4 5 6 7 8 |
>>> normalize_case = lambda s: s.casefold() >>> normalize_case <function <lambda> at 0x7f264d5b91e0> >>> def normalize_case(s): return s.casefold() ... >>> normalize_case <function normalize_case at 0x7f247f68fea0> |
Если вы хотите создать функции и хранить ее в переменной, определите вашу функцию при помощи def. Это именно то, для чего он нужен. Неважно, если ваша функция длиной в одну строку кода, или вы определяете функцию внутри другой функции, def отлично работает в таких случаях.
Неправильное использование: ненужные вызовы функций
Я часто сталкиваюсь с тем, что лямбда-выражения используются как обертка вокруг функции, которая и так подходит для решения задачи.
Рассмотрим следующий код в качестве примера:
1 |
sorted_numbers = sorted(numbers, key=lambda n: abs(n)) |
Скорее всего, человек, написавший этот код, прочитал о том, что лямбда-выражения используются для того, чтобы создать функцию, которая может передаваться. Однако он не учел то, что картина несколько шире: все функции в Python (не только функции лямбда) могут передаваться.
Поскольку abs (который возвращает абсолютное значение числа) является функцией, а все функции могут передаваться, мы можем написать вышеупомянутый код следующим образом:
1 |
sorted_numbers = sorted(numbers, key=abs) |
Теперь этот пример может казаться слегка надуманным, но и чрезмерное использование лямбда-выражений можно воспринимать аналогично. Вот еще один пример:
1 2 |
pairs = [(4, 11), (8, 8), (5, 7), (11, 3)] sorted_by_smallest = sorted(pairs, key=lambda items: min(items)) |
Так как мы принимает абсолютно те же аргументы, которые передаем в min, нам не нужен дополнительный вызов функции. Мы можем просто передать функцию min нашему ключу:
1 2 |
pairs = [(4, 11), (8, 8), (5, 7), (11, 3)] sorted_by_smallest = sorted(pairs, key=min) |
Вам не нужна лямбда функция, если у вас уже есть другая функция, выполняющая то, что вам нужно.
Злоупотребление: простые, но нетривиальные функции
Часто встречается использование лямбда-выражений для создания функции, которая возвращает несколько значений в кортеж python:
1 2 |
colors = ["Goldenrod", "Purple", "Salmon", "Turquoise", "Cyan"]) colors_by_length = sorted(colors, key=lambda c: (len(c), c.casefold())) |
Используемая функция-ключ в этом примере помогает нам распределить цвета по длине в соответствии с их названиями.
Этот код аналогичен приведенному выше, но я нахожу его более читаемым:
1 2 3 4 5 |
def length_and_alphabetical(string): return (len(string), string.casefold()) colors = ["Goldenrod", "Purple", "Salmon", "Turquoise", "Cyan"]) colors_by_length = sorted(colors, key=length_and_alphabetical) |
Этот код немного нуднее, но на мой взгляд, наименование нашей функции делает более понятным по каким признакам мы собираемся проводить сортировку. Мы не просто сортируем по длине и не по цвету: мы сортируем по обеим признакам.
Если функция важная — желательно дать ей название. Вы можете не согласиться и сказать, что большинство функций, которые мы используем в лямбда-выражении настолько тривиальны, что их даже не нужно называть, и есть небольшие недостатки в наименовании функций, но на мой взгляд, это делает код более читаемым в конечном счете.
Обычные функции часто делают код более читаемым, так же, как использование распаковки кортежей вместо использования произвольных индексированных запросов также делает код более читаемым.
Злоупотребление: когда множество строк должны помочь
Иногда такой аспект, как «просто одна строка» в лямбда-выражениях заставляет нас делать код более запутанным. Например, вот так:
1 2 |
points = [((1, 2), 'red'), ((3, 4), 'green')] points_by_color = sorted(points, key=lambda p: p[1]) |
Мы старательно прописываем индексный поиск, чтобы сортировать точки по их цветам. Если мы используем обычную функцию, мы сможем применить распаковку кортежей, чтобы сделать этот код более читаемым:
1 2 3 4 5 6 |
def color_of_point(point): (x, y), color = point return color points = [((1, 2), 'red'), ((3, 4), 'green')] points_by_color = sorted(points, key=color_of_point) |
Распаковка кортежей может улучшить читаемость, используя детально прописанный индексный поиск. Использование лямбда-выражений нередко означает пожертвовать определенными языковыми возможностями Python, особенно теми, которые требуют несколько строк кода (таких, как дополнительный оператор присваивания).
Злоупотребление: лямбда с map и filter
Функции Python map и filter практически всегда идут рука об руку с лямбда-выражениями. В StackOverflow обычным делом считается ответом на вопрос «что такое лямбда?» следующий код:
1 2 3 |
>>> numbers = [2, 1, 3, 4, 7, 11, 18] >>> squared_numbers = map(lambda n: n**2, numbers) >>> odd_numbers = filter(lambda n: n % 2 == 1, numbers) |
Такие примеры кажутся мне слегка озадачивающими, так как я практически никогда не использую map и filter у себя в коде.
Функции Python map и filter используются для создания цикла над итерацией и создания новой итерации, которая либо слегка меняет каждый элемент, или фильтрует итерацию только по отношению к тем элементам, которые соответствуют определенным условиям.
Мы можем выполнить обе эти задачи при помощи списка или выражений генератора:
1 2 3 |
>>> numbers = [2, 1, 3, 4, 7, 11, 18] >>> squared_numbers = (n**2 for n in numbers) >>> odd_numbers = (n for n in numbers if n % 2 == 1) |
Лично я предпочитаю видеть указанные выражения генератора написанные над несколькими строками кода, но даже эти однострочные выражения генератора читаются легче, чем вызовы filter и map.
Общие операции отображения и фильтрации полезны, но нам в действительности не нужно фильтровать и отображать сами функции. Выражения генератора являются особым синтаксисом, который существует для задач, связанных с отображением и фильтрацией. Так что мой совет — использовать выражения генератора вместо функций map и filter.
Неправильное использование: иногда вам даже не нужно передавать функцию
Что на счет тех случаев, где вам нужно передать куда-нибудь функцию, которая выполняет одну единственную операцию?
Новички, которые только осваивают функциональное программирование иногда пишут код следующим образом:
1 2 3 4 |
from functools import reduce numbers = [2, 1, 3, 4, 7, 11, 18] total = reduce(lambda x, y: x + y, numbers) |
Этот код вносит числа в список. Но есть лучший способ сделать это:
1 2 |
numbers = [2, 1, 3, 4, 7, 11, 18] total = sum(numbers) |
Встроенная функция sum в Python была создана именно для этой задачи.
Функция sum, наряду с рядом других специализированных инструментов Python, легко упустить из виду. Но я бы посоветовал вам найти более точечные инструменты при необходимости, так как они также хорошо сказываются на читаемости кода.
Вместо передачи функций другим функциям, поищите более специализированный способ решить свою проблему.
Злоупотребление: использование лямбды для очень простых операций
Скажем, вместо того, чтобы сложить числа, мы их умножаем:
1 2 3 4 |
from functools import reduce numbers = [2, 1, 3, 4, 7, 11, 18] product = reduce(lambda x, y: x * y, numbers, 1) |
Это лямбда-выражение необходимо, так как мы не можем передавать оператор * также, как если бы он был функцией. Если есть функция, являющаяся эквивалентом *, мы можем передать ее функции reduce вместо этого.
Стандартная библиотека Python также имеет целый модуль, созданный для того, чтобы решить эту проблему:
1 2 3 4 5 |
from functools import reduce from operator import mul numbers = [2, 1, 3, 4, 7, 11, 18] product = reduce(mul, numbers, 1) |
Модуль операторов в Python существует, чтобы упростить использование функций для различных операторов. Если вы практикуете функциональное программирование, операционный модуль Python может послужить вам другом.
В дополнение к предоставлению функций, соответствующих ряду операторов Python, модуль operator предоставляет несколько функций более высокого уровня для доступа к элементам и атрибута методов вызова.
Есть itemgetter, который помогает получить доступ к индексам списка (или последовательности) ключей словаря:
1 2 3 4 5 6 |
# Без оператора: получение доступа к индексу/ключу rows_sorted_by_city = sorted(rows, key=lambda row: row['city']) # С оператором: получение доступа к индексу/клюу from operator import itemgetter rows_sorted_by_city = sorted(rows, key=itemgetter('city')) |
Также есть attrgetter для получения доступа к атрибутам объекта:
1 2 3 4 5 6 |
# Без оператора: получение доступа к атрибуту products_by_quantity = sorted(products, key=lambda p: p.quantity) # С оператором: получение доступа к атрибуту from operator import attrgetter products_by_quantity = sorted(products, key=attrgetter('quantity')) |
И methodcaller для вызова методов объекта:
1 2 3 4 5 6 |
# Без оператора: вызов метода sorted_colors = sorted(colors, key=lambda s: s.casefold()) # С оператором: вызов метода from operator import methodcaller sorted_colors = sorted(colors, key=methodcaller('casefold')) |
Я постоянно отмечаю, что использование функций в модуле operator делает код чище, чем если бы я использовал аналогичные лямбда-выражения.
Злоупотребление: когда использование функций высокого порядка только мешает
Функция, которая принимает функцию в качестве аргумента называется функцией высокого порядка. Функции высокого порядка — это такой тип функций, которым мы обычно передаем наши функции лямбда.
Использование функций высокого порядка — обычное дело при освоении функционального программирования. Функциональное программирование — не единственный способ использовать Python: этот язык с несколькими парадигмами, так что мы можем совмещать и сопоставлять дисциплины программирования, чтобы сделать код более читаемым.
Сравните следующее:
1 2 3 4 |
from functools import reduce numbers = [2, 1, 3, 4, 7, 11, 18] product = reduce(lambda x, y: x * y, numbers, 1) |
С этим кодом:
1 2 3 4 5 6 7 8 |
def multiply_all(numbers): product = 1 for n in numbers: product *= n return product numbers = [2, 1, 3, 4, 7, 11, 18] product = multiply_all(numbers) |
Второй код длиннее, но ребята без опыта в функциональном программировании могут найти его более простым для понимания.
Любой, кто прошел те или иные курсы Python, вполне может понять, что делает функция multiply_all, в то время как комбинация reduce/lambda будет, скорее всего, чем-то более загадочным для многих программистов Python.
В целом, передача одной функции другой, как правило, делает код более сложным, что вредит читаемости.
Стоит ли вообще использовать лямбда-выражения?
Я нахожу использование лямбда-выражений несколько проблематичным, и вот почему:
- Лямбда-выражения имеют незнакомый, или непонятный синтаксис для многих программистов Python;
- По сути, лямбда-функциям не хватает наименований или документации. Иными словами, непосредственное чтение такого кода — единственный способ понять, что они делают;
- Лямбда-выражения могут содержать в себе только один оператор, так что определенные языковые возможности, улучшающие читаемость, такие как распаковка кортежей не могут быть использованы;
- Функции лямбда зачастую можно заменить уже существующими функциями стандартной библиотеки;
- Лямбда-выражении редко на столько же быстро читаемые, как хорошо названные def функции.
В то время как оператор def зачастую более понятен, в Python также имеется ряд возможностей, которые можно использовать для замены лямбда-выражений, включая особый синтаксис, встроенные функции (sum) и функции стандартной библиотеки (в модуле operators).
Скажу так: использование лямбда-выражений приемлемо только тогда, когда ваша ситуация соответствует всем следующим четырем критериям:
- Вы выполняете тривиальную операцию, т. е. функции не нужно название;
- Наличие лямбда-выражения делает ваш код понятнее, чем другие функции;
- Вы знаете, что у вас нет функции, которая делает то, что вам нужно;
- Каждый человек в вашей команде понимает лямбда-выражения и вы договорились использовать их.
Если какое-либо из перечисленных утверждений не соответствует вашей ситуации, я бы порекомендовал написать новую функцию при помощи def (там, где это возможно), охватывающую функцию, которая уже существует в среде Python, и которая делает то, что вам нужно.
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»