💻 Примеры

⚡ Пять инструментов на одном наборе данных

from functools import reduce
nums = [5, 3, 8, 1]
print(list(map(lambda x: x ** 2, nums)))        # [25, 9, 64, 1]  преобразовать
print(list(filter(lambda x: x > 3, nums)))      # [5, 8]          отобрать
print(reduce(lambda acc, x: acc + x, nums, 0))  # 17              свернуть
print(sorted(nums, key=lambda x: -x))           # [8, 5, 3, 1]    сравнить по критерию
print(any(x > 7 for x in nums))                 # True            проверить
ИнструментЧто делает с коллекциейЧто вернёт
map / filterменяет каждый элемент / оставляет часть элементовитератор → нужен list()
reduceсводит всю коллекцию к одному значениюзначение
key=критерий сравнения для sorted, min, maxсписок / элемент
any / all«хотя бы один» / «все», с досрочным выходомTrue или False
Топ-3 ошибки: square(5) вместо square · забытый list() вокруг map/filter · слишком сложная lambda вместо def.

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

Пример 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 «Замыкания».

⚠️ Проверить по документации: поведение встроенных функций и их сигнатуры сверяйте с официальной документацией — map, filter, functools.reduce, Sorting HOW TO.

Что делать дальше

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