$ sudo teach IT
Модуль 6 · Функции · Урок 6.4

Лямбда и функции высшего порядка

Короткие безымянные функции и инструменты, которые умеют принимать функцию как обычный аргумент

Теория~35 минутУверенный стартlambdamap()filter()functools.reduce()any() / all()

Функция без имени: лямбда

Представьте, что вам нужно отсортировать список слов не по алфавиту, а по длине. Для этого sorted() просит передать ей правило сравнения — функцию, которая скажет, что сравнивать. Писать ради одного использования полноценную функцию с def, именем и отдельным местом в файле неудобно: три строки кода нужны один-единственный раз, прямо в этой строке.

Для таких коротких одноразовых случаев в Python есть особая форма функции — она не имеет имени и умещается в одну строку прямо там, где нужна. В официальной терминологии Python это называется лямбда-функция (lambda function) — анонимная функция, заданная одним выражением.

Синтаксис:

lambda аргументы: выражение
square = lambda x: x * x
print(square(5))

Что здесь происходит:

  • lambda — ключевое слово, с которого начинается анонимная функция
  • x перед двоеточием — параметр, как в обычной функции: значение, которое передадут при вызове
  • всё, что после двоеточия (x * x), — единственное выражение, его результат и есть ответ функции
  • square = lambda x: x * x — лямбда сама по себе является значением, поэтому её можно, как и число или строку, положить в переменную
  • square(5) — вызываем сохранённую функцию обычными круглыми скобками, ничем не отличается от вызова функции через def

Вывод программы:

25

У лямбды нет слова return — и не может быть: Python запрещает его внутри лямбды синтаксически. Но значение всё равно возвращается: правило простое — лямбда всегда возвращает результат своего единственного выражения, без отдельной команды на это.

classify = lambda x: "положительное" if x > 0 else "не положительное"
print(classify(5))
print(classify(-3))

Разбор:

  • classify принимает один параметр x
  • после двоеточия стоит тернарный оператор — вы уже с ним знакомы: это выражение, а не отдельная команда, поэтому в лямбду оно помещается
  • classify(5): 5 > 0 истинно, выражение вычисляется в "положительное"
  • classify(-3): -3 > 0 ложно, выражение вычисляется в "не положительное"

Вывод:

положительное
не положительное

Чего в лямбде нет:

Лямбда — специально урезанная форма функции. В ней нельзя написать несколько строк, нельзя использовать if как отдельный блок (только тернарный оператор как выражение), нельзя циклы и нельзя return. Если логика не помещается в одно выражение — это сигнал, что нужен обычный def, а не лямбда.

Функция как обычное значение

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

Функцию, которая принимает другую функцию как аргумент (или возвращает функцию), в документации Python называют функцией высшего порядка (higher-order function). Именно такие функции и разберём дальше в этом уроке — map, filter, reduce.

def double(x):
    return x * 2

def apply_twice(func, value):
    return func(func(value))

print(apply_twice(double, 3))

Разбор:

  • apply_twice(func, value) — параметр func ничем не отличается от обычного параметра, просто ожидает, что в него передадут функцию
  • func(func(value)) — сначала вызываем func от value, потом ещё раз вызываем func уже от результата
  • apply_twice(double, 3) — обратите внимание: double передаётся без скобок. Скобки означают "вызови прямо сейчас", а без скобок мы передаём саму функцию как значение, чтобы вызвал её кто-то другой
  • внутри: func = double, затем double(double(3)) → double(6) → 12

Вывод:

12

map() — одна операция для каждого элемента

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

Для этого в Python есть встроенная функция map(): она принимает функцию и коллекцию, и применяет эту функцию к каждому элементу коллекции по очереди.

prices_usd = [10, 25, 5, 100]
rate = 90
prices_rub = list(map(lambda p: p * rate, prices_usd))
print(prices_rub)

Разбор:

  • map(lambda p: p * rate, prices_usd) — первым аргументом функция (что делать с каждым элементом), вторым — коллекция (по каким элементам пройти)
  • лямбда lambda p: p * rate вызывается по очереди для 10, 25, 5, 100, каждый раз параметр p получает очередное число
  • map() сама по себе возвращает не список, а особый объект — итератор, который умеет отдавать значения по одному, но не хранит их все сразу в памяти
  • поэтому результат оборачиваем в list(...), чтобы получить обычный список, который можно напечатать или перебрать ещё раз

Вывод:

[900, 2250, 450, 9000]

Если забыть list(...) и просто напечатать map(...), вы увидите что-то вроде <map object at 0x...> — это не ошибка, просто вы напечатали сам итератор, а не значения внутри него.

filter() — оставляем только нужное

Другая частая задача — не преобразовать все элементы, а отобрать из них только те, что подходят под условие: только положительные числа, только совершеннолетних пользователей. Здесь помогает filter().

filter(функция, коллекция) проходит по коллекции и оставляет только те элементы, для которых функция вернула True. Такую функцию — ту, что возвращает только True/False, — в документации Python называют предикатом (predicate).

numbers = [1, -3, 5, -2, 8, -7, 0, 4]
positives = list(filter(lambda x: x > 0, numbers))
print(positives)

Разбор:

  • lambda x: x > 0 — предикат: для каждого x возвращает True, если число больше нуля, иначе False
  • filter вызывает эту лямбду для каждого элемента numbers и пропускает дальше только те, для которых получилось True
  • как и map(), filter() возвращает итератор, поэтому снова оборачиваем в list(...)

Вывод:

[1, 5, 8, 4]

map и filter отлично комбинируются друг с другом — результат одной функции можно сразу передать в другую:

numbers = [1, 2, 3, 4, 5, 6, 7, 8]
result = list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, numbers)))
print(result)

Разбор:

  • читаем изнутри наружу: сначала filter(lambda x: x % 2 == 0, numbers) оставляет только чётные — 2, 4, 6, 8
  • потом map(lambda x: x ** 2, ...) возводит каждое из оставшихся чисел в квадрат
  • list(...) в самом конце собирает итоговый итератор в список

Вывод:

[4, 16, 36, 64]

functools.reduce() — сворачиваем коллекцию в одно число

map и filter возвращают коллекцию (пусть и меньше или преобразованную). А что, если нужен не список, а одно итоговое значение — например, сумма всех чисел? Можно представить это как стопку монет: берёте текущую стопку и кладёте сверху ещё одну монету, получаете новую стопку — и так по одной, пока монеты не кончатся.

Именно так работает reduce(): она последовательно применяет функцию к паре "накопленный результат, следующий элемент" и на каждом шаге получает новый накопленный результат. В конце остаётся одно значение. В документации Python это называют свёрткой (reduction). В отличие от map и filter, которые встроены в язык, reduce лежит в отдельном модуле functools, поэтому её нужно сначала импортировать строкой from functools import reduce.

from functools import reduce

numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, x: acc + x, numbers)
print(total)

Разбор по шагам:

ШагaccxНовый acc
11 (первый элемент)23
2336
36410
410515

acc (сокращение от accumulator — накопитель) — первый параметр лямбды, он и есть текущий накопленный результат. Без начального значения первым acc становится первый элемент коллекции, а перебор начинается со второго.

Вывод:

15

Третьим аргументом можно задать начальное значение накопителя — тогда перебор начнётся с первого элемента, а не со второго:

from functools import reduce

numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, x: acc + x, numbers, 100)
print(total)

Разбор:

  • 100 — третий аргумент, начальное значение acc перед первым шагом
  • перебор идёт по всем пяти элементам списка, а не по четырём, как в примере без начального значения
  • итог: 100 + 1 + 2 + 3 + 4 + 5 = 115

Вывод:

115

any() и all() — хотя бы один или все сразу

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

any(iterable) возвращает True, если хотя бы один элемент коллекции истинный. all(iterable) возвращает True, только если истинны абсолютно все элементы. Обеим функциям обычно передают генератор — вы уже работали с таким синтаксисом, когда разбирали коллекции.

numbers = [5, 3, -1, 8, 2]
has_negative = any(x < 0 for x in numbers)
all_positive = all(x > 0 for x in numbers)
print(has_negative)
print(all_positive)

Разбор:

  • x < 0 for x in numbers — генератор: для каждого x из numbers вычисляет x < 0, не создавая промежуточный список
  • any(...) проверяет значения по очереди и останавливается, как только нашла первое True — это -1, поэтому результат True
  • all(...) во втором вызове проверяет x > 0 для всех элементов; на -1 условие ложно, поэтому весь all() сразу даёт False

Вывод:

True
False

Пустая коллекция — частая ловушка:

any([]) вернёт False — истинных элементов и правда нет ни одного. А вот all([]) вернёт True: формально все (ноль) элементы удовлетворяют условию, раз проверять нечего. Если забыть об этом, легко получить неожиданное "всё в порядке" там, где на самом деле просто нет данных.

Частые ошибки

Попытка написать return внутри лямбды

bad = lambda x: return x * 2

SyntaxError: return — это отдельная инструкция, а тело лямбды должно быть одним выражением. Правильно: lambda x: x * 2 — без слова return, значение выражения возвращается само.

Забыли обернуть map/filter в list()

result = map(lambda x: x * 2, [1, 2, 3])
print(result)

Напечатается не [2, 4, 6], а что-то вроде <map object at 0x...>. Ошибки здесь нет — просто map() вернула итератор, а не список. Чтобы увидеть значения, нужен list(result).

reduce без импорта

total = reduce(lambda acc, x: acc + x, [1, 2, 3])

NameError: name 'reduce' is not defined. В отличие от map, filter, any, all, функция reduce не встроена в язык напрямую — её нужно сначала подключить строкой from functools import reduce.

Что важно запомнить

  • lambda аргументы: выражение — анонимная функция в одну строку, результат выражения возвращается без return
  • функция в Python — обычное значение: её можно передавать в другую функцию без скобок вызова, функция, принимающая функцию, называется функцией высшего порядка
  • map(func, iterable) применяет func к каждому элементу, filter(func, iterable) оставляет только те, где func дала True — обе возвращают итератор, нужен list(...)
  • reduce(func, iterable) из functools сворачивает коллекцию в одно значение через накопитель acc
  • any(iterable) — True, если истинен хотя бы один элемент; all(iterable) — True, если истинны все; all([]) тоже True

Проверьте себя

6 вопросов

map и filter с lambda

Дан список чисел. Напиши функцию process_numbers(numbers), которая:

  1. Оставляет только чётные числа (используй filter и lambda)
  2. Возводит каждое из них в квадрат (используй map и lambda)
  3. Возвращает итоговый список

Пример: process_numbers([1, 2, 3, 4, 5, 6]) → [4, 16, 36]

map и filter

Premium

any и all

Premium