Примеры идут от простого к сложному: сначала сама идея «функция — это значение», затем встроенные функции высшего порядка по отдельности, затем их связка в реальном конвейере обработки данных. Весь вывод в комментариях получен запуском кода, а не набран по памяти.
Пример 1. Функция — это объект: ссылка против вызова
Имя функции без скобок — это ссылка на объект-функцию. Скобки означают «выполни прямо сейчас и подставь результат». Вся тема урока держится на этом различии.
def square(x):
return x * x
f = square # ссылка: функция НЕ вызывалась
print(square) # <function square at 0x...>
print(type(square)) # <class 'function'>
print(f(5)) # 25 — вызываем через другое имя
print(square(5)) # 25 — здесь скобки, значит уже число
Как проверить себя: напечатайте объект. Если видите <function ...> — у вас ссылка, её можно куда-то передать. Если видите число или строку — функция уже отработала, передавать нечего.
Пример 2. Словарь функций вместо цепочки if
Раз функция — обычное значение, её можно хранить в списке или словаре. Так строится «диспетчер»: ключ выбирает поведение, и новый пункт меню добавляется одной строкой словаря, а не новой веткой elif.
def add(a, b):
return a + b
def multiply(a, b):
return a * b
def subtract(a, b):
return a - b
operations = {"+": add, "*": multiply, "-": subtract}
print(operations["+"](10, 5)) # 15
print(operations["*"](10, 5)) # 50
# безопасный выбор: неизвестный ключ не роняет программу
operation = operations.get("/")
if operation is None:
print("Операция '/' не поддерживается")
# по словарю функций можно пройти циклом
for symbol, func in operations.items():
print(symbol, func(12, 4), func.__name__)
# + 16 add
# * 48 multiply
# - 8 subtract
Разбор: operations["+"] возвращает функцию, и вторая пара скобок (10, 5) её вызывает. Атрибут __name__ напоминает: функция — объект со своими атрибутами.
Пример 3. Функция высшего порядка: принимает функцию и возвращает функцию
Функция высшего порядка — та, которая принимает другую функцию как аргумент и/или возвращает функцию. Обе половины определения одинаково важны.
# 1) принимает функцию
def apply(func, x):
return func(x)
print(apply(square, 5)) # 25
print(apply(len, "hello")) # 5 — встроенная функция тоже объект
# 2) возвращает функцию (фабрика функций)
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10), triple(10)) # 20 30
print(double.__name__) # multiplier
Что важно: make_multiplier возвращает новую функцию, которая помнит значение n — каждая из них «носит с собой» свой множитель. Механику этого запоминания подробно разбирает урок 62 «Замыкания»; здесь достаточно видеть, что функция бывает результатом.
Пример 4. lambda и def — одно и то же, разный синтаксис
lambda — синтаксический сахар: одно выражение, результат возвращается автоматически, имени нет. Полезна там, где функция нужна на один раз прямо в аргументе.
def f1(x):
return x * x
f2 = lambda x: x * x # так делать не надо: PEP 8 против именованных lambda
print(f1(6), f2(6)) # 36 36
print((lambda x: x * x)(6)) # 36 — вызов на месте, без имени
# уместное применение: короткий callback в аргументе
words = ["banana", "kiwi", "apple"]
print(sorted(words, key=lambda w: len(w))) # ['kiwi', 'apple', 'banana']
print(sorted(words, key=len)) # то же самое, но короче
len, str, abs) — передавайте её, lambda лишняя. Если выражение не помещается в одну понятную строку или ему нужно имя и docstring — пишите def. lambda живёт ровно между этими границами.
Пример 5. map: одна и несколько последовательностей, ленивость
map применяет функцию к каждому элементу и возвращает итератор, а не список. Итератор считает значения по требованию и исчерпывается после первого прохода — это самая частая неожиданность в теме.
from typing import Iterator
nums = [1, 2, 3, 4, 5, 6, 7, 8]
map_object = map(str, nums)
print(map_object) # <map object at 0x...>
print(isinstance(map_object, Iterator)) # True
print(*map_object) # 1 2 3 4 5 6 7 8
print(*map_object) # (пусто!) итератор уже исчерпан
# map умеет работать с несколькими последовательностями сразу
print(list(map(lambda x, y: x + y, nums, nums))) # [2, 4, 6, ..., 16]
print(list(map(lambda a, b: a * b, [1, 2, 3], [10, 20]))) # [10, 40]
# частый практический случай: смена типа перед вычислением
prices = ["10.5", "3", "7.25"]
print(sum(map(float, prices))) # 20.75
Два вывода из примера. Первый: результат map нужно либо сразу обернуть в list(), либо использовать ровно один раз. Второй: при нескольких последовательностях map останавливается на самой короткой — [1, 2, 3] и [10, 20] дали два элемента, а не три, и никакой ошибки при этом не будет.
Пример 6. filter: по условию и по «истинности»
filter не меняет элементы, он решает, какие оставить. Функция-предикат возвращает True/False; вместо неё можно передать None — тогда останутся все «истинные» значения.
nums = [1, 2, 3, 4, 5, 6, 7, 8]
print(list(filter(lambda x: x % 2 == 0, nums))) # [2, 4, 6, 8]
# filter(None, ...) — быстрый способ выкинуть пустышки
values = [0, 1, "", "text", None, [], [1], 0.0, "0"]
print(list(filter(None, values))) # [1, 'text', [1], '0']
words = ["apple", "", "kiwi", None, "banana"]
print(list(filter(None, words))) # ['apple', 'kiwi', 'banana']
Внимание к строке "0": она осталась в результате. Пустыми считаются 0, 0.0, "", None, [], {}, а строка из символа нуля — непустая и потому истинная.
Пример 7. reduce: аккумулятор, initial и пустая коллекция
reduce(function, iterable, initial) берёт функцию от двух аргументов — накопленного значения и очередного элемента — и «сворачивает» коллекцию в одно значение. Без initial начальным значением становится первый элемент.
from functools import reduce
nums = [1, 2, 3, 4, 5, 6, 7, 8]
print(reduce(lambda acc, x: acc + x, nums)) # 36
print(reduce(lambda acc, x: acc + x, nums, 10)) # 46 — начали с 10
print(reduce(lambda acc, x: acc + x, [10, 0, 0])) # 10
# пустая коллекция без initial — ошибка
try:
reduce(lambda acc, x: acc + x, [])
except TypeError as error:
print("TypeError:", error)
# reduce() of empty iterable with no initial value
print(reduce(lambda acc, x: acc + x, [], 0)) # 0 — с initial всё в порядке
Свёртка работает не только с числами: аккумулятором может быть список, строка или словарь.
chunks = [["a", "b"], ["c"], ["d", "e"]]
print(reduce(lambda acc, x: acc + x, chunks, [])) # ['a', 'b', 'c', 'd', 'e']
longest = reduce(lambda acc, w: acc if len(acc) >= len(w) else w,
["kiwi", "banana", "fig"])
print(longest) # banana
reduce не нужен: для суммы есть sum(nums), для самого длинного слова — max(words, key=len). Встроенные функции читаются лучше и работают быстрее. reduce оправдан там, где готовой встроенной свёртки нет — например, при накоплении словаря или последовательном применении своей операции.
Пример 8. key=: сортировка по одному и по нескольким критериям
Параметр key — это функция, которая для каждого элемента вычисляет значение для сравнения. Сам элемент при этом не меняется: сортируется список кортежей, а сравниваются, например, только вторые поля.
d = {"c": 5, "b": 1, "a": 3}
print(dict(sorted(d.items(), key=lambda item: item[0])))
# {'a': 3, 'b': 1, 'c': 5} — по ключу, по возрастанию
print(dict(sorted(d.items(), key=lambda item: -item[1])))
# {'c': 5, 'a': 3, 'b': 1} — по значению, по убыванию
print(dict(sorted(d.items(), key=lambda item: item[1], reverse=True)))
# {'c': 5, 'a': 3, 'b': 1} — то же самое через reverse
Если критериев несколько, key возвращает кортеж: элементы сравниваются по первому полю, при равенстве — по второму, и так далее. Минус перед числом переворачивает направление только для этого поля.
data = [("apple", 5), ("banana", 2), ("apple", 8), ("banana", 7), ("cherry", 1)]
print(sorted(data, key=lambda x: (x[0], -x[1])))
# [('apple', 8), ('apple', 5), ('banana', 7), ('banana', 2), ('cherry', 1)]
# название по алфавиту, внутри одинаковых названий — количество по убыванию
people = [{"name": "Alice", "age": 25},
{"name": "Bob", "age": 20},
{"name": "Carol", "age": 25}]
print([p["name"] for p in sorted(people, key=lambda p: (-p["age"], p["name"]))])
# ['Alice', 'Carol', 'Bob'] — сначала старшие, ровесники по алфавиту
Про reverse и минус: reverse=True переворачивает порядок целиком, минус — только одно поле. Для «по возрасту убыв., по имени возр.» годится только вариант с минусом. И ещё: сортировка в Python устойчивая — элементы с одинаковым ключом сохраняют исходный взаимный порядок.
Пример 9. min, max, sum, any, all — свёртка без reduce
key принимает не только sorted. А any/all сворачивают коллекцию к одному булеву ответу и останавливаются, как только он становится известен.
people = [{"name": "Alice", "age": 25},
{"name": "Bob", "age": 20},
{"name": "Carol", "age": 25}]
print(max(people, key=lambda p: p["age"])) # {'name': 'Alice', 'age': 25}
print(sum(p["age"] for p in people)) # 70
print(any(p["age"] > 24 for p in people)) # True — хотя бы один
print(all(p["age"] > 24 for p in people)) # False — не все
print(all([]), any([])) # True False — пустая коллекция
Короткое замыкание видно, если добавить печать внутрь проверки:
def probe(n):
print(" проверяю", n)
return n > 3
print(any(probe(n) for n in [1, 5, 2, 7]))
# проверяю 1
# проверяю 5
# True — элементы 2 и 7 даже не проверялись
all([]): для пустой коллекции all возвращает True («ни один элемент не нарушил условие»), а any — False. Проверка вида if all(order.is_valid for order in orders) на пустом списке заказов радостно скажет «всё в порядке». Если пустой список — отдельный случай, проверяйте его явно.
Пример 10. Когда lambda заменяется готовым инструментом
Две частые lambda — «достань поле» и «зафиксируй аргумент» — уже реализованы в стандартной библиотеке: operator.itemgetter и functools.partial.
from functools import partial
from operator import itemgetter
data = [("apple", 5), ("banana", 2), ("apple", 8), ("banana", 7), ("cherry", 1)]
print(sorted(data, key=itemgetter(1))[:2]) # [('cherry', 1), ('banana', 2)]
print(list(map(itemgetter(0), data))[:3]) # ['apple', 'banana', 'apple']
# itemgetter(1) делает то же, что lambda x: x[1]
def power(base, exponent):
return base ** exponent
cube = partial(power, exponent=3) # зафиксировали один аргумент
print(cube(2), cube(5)) # 8 125
print(list(map(partial(round, ndigits=2), [3.14159, 2.71828]))) # [3.14, 2.72]
operator и functools.partial выходят за рамки лекции — это дополнение курса. Сверьтесь с разделами operator и functools.partial. На экзамене и в домашнем задании достаточно lambda; эти инструменты — то, что вы встретите в рабочем коде.
Пример 11. Конвейер обработки данных и его pythonic-альтернатива
Собираем всё вместе на живой задаче: из списка заказов взять оплаченные, посчитать выручку и найти крупнейших покупателей.
from functools import reduce
orders = [
{"id": 1, "customer": "Alice", "total": 120.0, "paid": True},
{"id": 2, "customer": "Bob", "total": 45.5, "paid": False},
{"id": 3, "customer": "Carol", "total": 300.0, "paid": True},
{"id": 4, "customer": "Dave", "total": 80.0, "paid": True},
]
paid = list(filter(lambda order: order["paid"], orders)) # отобрали
totals = list(map(lambda order: order["total"], paid)) # преобразовали
revenue = reduce(lambda acc, value: acc + value, totals, 0.0) # свернули
print(len(paid), totals, revenue) # 3 [120.0, 300.0, 80.0] 500.0
top = sorted(paid, key=lambda order: -order["total"])[:2]
print([order["customer"] for order in top]) # ['Carol', 'Alice']
print(max(orders, key=lambda order: order["total"])["customer"]) # Carol
Тот же результат на генераторных выражениях — и в Python это, как правило, предпочтительный вариант записи:
revenue = sum(order["total"] for order in orders if order["paid"])
print(revenue) # 500.0
Вывод, который стоит запомнить: map и filter с lambda почти всегда переписываются в генераторное выражение или list comprehension (урок 30), и такая запись читается лучше. Знать map/filter/reduce всё равно нужно: их постоянно встречают в чужом коде, они естественны при передаче готовой функции (map(float, prices)) и они — точка входа в функциональный стиль.
Пример 12. Ловушка: lambda в цикле помнит переменную, а не значение
Классическая ошибка, на которой спотыкаются все, кто впервые создаёт функции в цикле.
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2] — а ожидали [0, 1, 2]
# способ 1: зафиксировать значение аргументом по умолчанию
funcs_fixed = [lambda i=i: i for i in range(3)]
print([f() for f in funcs_fixed]) # [0, 1, 2]
# способ 2: фабрика функций — каждый вызов создаёт свою область имён
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
factories = [make_multiplier(n) for n in (2, 3)]
print([f(10) for f in factories]) # [20, 30]
Почему так: lambda запоминает переменную i, а не её значение на момент создания. К моменту вызова цикл давно закончился и i равно 2 — все три функции видят одно и то же. Аргумент по умолчанию вычисляется в момент создания функции и потому «замораживает» значение. Полный разбор механизма — в уроке 62 «Замыкания».
Что делать дальше
Разберите файлы репозитория — там те же идеи в исходном виде, затем переходите к заданиям. Если что-то из примеров не запустилось — сначала загляните в типичные ошибки.