💻 Практические примеры

⚡ Функции высшего порядка: шпаргалка

from functools import reduce

nums = [1, 2, 3, 4, 5, 6]
print(list(filter(lambda x: x % 2 == 0, nums)))   # [2, 4, 6]
print(reduce(lambda acc, x: acc * x, nums, 1))    # 720
print(sorted(["kiwi", "apple", "fig"], key=len))  # ['fig', 'kiwi', 'apple']
ИнструментЧто делаетПример
filter(fn, it)оставляет элементы, где предикат истиненlist(filter(is_even, nums))
reduce(fn, it, start)сворачивает коллекцию в одно значениеreduce(lambda a, b: a * b, nums, 1)
sorted(it, key=fn)сортирует по вычисляемому ключуsorted(words, key=len)
Callable[[int], bool]тип функции-предиката для аннотацийdef is_even(n: int) -> bool:
lambda x: ...анонимная функция на один разfilter(lambda x: x % 2 == 0, nums)
Топ-3 ошибки: забытый list() вокруг filter/map · вызов predicate() вместо передачи ссылки predicate · reduce без начального значения падает на пустой коллекции.

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

Пример 1. Предикат и фильтрация

Отдельная функция-предикат делает код понятнее, когда условие сложное или используется в нескольких местах. Аннотация Callable[[int], bool] честно говорит: сюда передают функцию, которая принимает один int и возвращает bool.

# predicate_filter.py
from typing import Callable


def is_even(n: int) -> bool:
    return n % 2 == 0


def filter_numbers(
    predicate: Callable[[int], bool],
    numbers: list[int]
) -> list[int]:
    return [n for n in numbers if predicate(n)]


nums = [1, 2, 3, 4, 5, 6]
print(filter_numbers(is_even, nums))  # [2, 4, 6]

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

Пример 2. Фильтрация с lambda

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

# lambda_filter.py
nums = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens)  # [2, 4, 6]

raw = filter(lambda x: x % 2 == 0, nums)
print(raw)  # <filter object at 0x...>

Что происходит: filter возвращает не список, а ленивый итератор — объект, который отдаёт элементы по одному при обходе. Почему так: печать самого объекта raw показывает <filter object ...>, а не значения — поэтому результат нужно явно обернуть в list(), иначе увидеть отфильтрованные элементы не получится.

Пример 3. Агрегирование через reduce

Произведение элементов удобно вычислять через reduce из модуля functools: он последовательно применяет функцию к накопленному результату и очередному элементу.

# reduce_product.py
from functools import reduce


def product(numbers: list[int]) -> int:
    return reduce(lambda acc, x: acc * x, numbers, 1)


numbers = [1, 2, 3, 4, 5]
print(product(numbers))    # 120
print(product([]))         # 1 — начальное значение спасает пустой список

Почему так: начальное значение 1 важно вдвойне: оно даёт корректный результат 1 для пустого списка (без него reduce выбросит TypeError, см. типичные ошибки) и не меняет произведение для непустого, потому что умножение на 1 нейтрально.

Пример 4. Сортировка по длине

Параметр key у sorted — это функция, которая вычисляет значение для сравнения; сам список при этом не меняется, возвращается новый.

# sort_by_length.py
def sort_by_length(words: list[str]) -> list[str]:
    return sorted(words, key=len)


words = ["apple", "banana", "kiwi", "grape"]
print(sort_by_length(words))  # ['kiwi', 'apple', 'grape', 'banana']
print(words)                  # ['apple', 'banana', 'kiwi', 'grape'] — исходный список не тронут

Что происходит: функция len вызывается для каждого слова, и sorted сравнивает не сами слова, а полученные числа. Почему так: sorted всегда возвращает новый список, поэтому исходный words остаётся в первоначальном порядке — в отличие от метода words.sort() (см. ошибку 2).

Пример 5. Сортировка по сумме кодов символов

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

# sort_by_ord_sum.py
words = ["banana", "kiwi", "grapefruit", "apple"]
sorted_words = sorted(words, key=lambda word: sum(ord(c) for c in word))
print(sorted_words)  # ['kiwi', 'apple', 'banana', 'grapefruit']

for word in words:
    print(word, sum(ord(c) for c in word))
# banana 609
# kiwi 436
# grapefruit 1081
# apple 530

Что происходит: генератор sum(ord(c) for c in word) вычисляет сумму кодов символов слова, и sorted использует эту сумму как ключ сравнения. Почему так: цикл ниже печатает промежуточные суммы — по ним видно, что порядок в результате (436, 530, 609, 1081) действительно соответствует возрастанию сумм, а не длине слова.

Пример 6. Композиция функций для шифрования

Цепочку независимых преобразований строки (верхний регистр → сдвиг символов → переворот) можно оформить одной функцией, которая принимает список шагов и не знает заранее, сколько их и какие они.

# encryption_chain.py
from typing import Callable


def to_uppercase(text: str) -> str:
    return text.upper()


def shift_encrypt(text: str, shift: int = 5) -> str:
    return "".join(chr(ord(c) + shift) for c in text)


def reverse_string(text: str) -> str:
    return text[::-1]


def encrypt(
    text: str,
    functions: list[Callable[[str], str]]
) -> str:
    result = text
    for func in functions:
        result = func(result)
    return result


sentence = "Functional programming is powerful"
functions = [to_uppercase, shift_encrypt, reverse_string]
print(encrypt(sentence, functions))
# QZKWJ\TU%XN%LSNRRFWLTWU%QFSTNYHSZK

Что происходит: каждая функция по очереди преобразует строку, и результат одного шага становится входом следующего. Почему так: функция encrypt не зависит от конкретного списка шагов — список functions можно сократить, расширить или переставить, и encrypt не изменится, потому что она лишь обходит список и вызывает то, что в нём лежит.

Пример 7. Фабрика типизированных предикатов

Функция может не только принимать предикат, но и возвращать его — так параметр (например, минимальная длина слова) «вшивается» в готовую функцию-предикат. Это прямое развитие заданий 2.2–2.3 из заданий: критерий отбора становится настраиваемым, а не зашитым в код.

# predicate_factory.py
from typing import Callable


def make_min_length_predicate(min_len: int) -> Callable[[str], bool]:
    def predicate(word: str) -> bool:
        return len(word) >= min_len
    return predicate


def filter_words(
    predicate: Callable[[str], bool],
    words: list[str]
) -> list[str]:
    return [word for word in words if predicate(word)]


words = ["hi", "Hello", "a", "python", "Ok"]
at_least_two = make_min_length_predicate(2)
at_least_five = make_min_length_predicate(5)
print(filter_words(at_least_two, words))   # ['hi', 'Hello', 'python', 'Ok']
print(filter_words(at_least_five, words))  # ['Hello', 'python']

Что происходит: make_min_length_predicate возвращает не значение, а новую функцию predicate, которая «помнит» переданный min_len. Почему так: тип возврата Callable[[str], bool] честно описывает это — вызывающий код получает не bool, а функцию str → bool, которую затем можно передать в filter_words как обычный предикат.

Пример 8. Типичная ошибка: вызов предиката вместо передачи ссылки

Разница между «передать функцию» (predicate) и «вызвать функцию и передать её результат» (predicate()) — источник самой частой ошибки в этой теме. Второй частый провал — забытый list() вокруг filter.

# common_mistakes.py
def is_even(n: int) -> bool:
    return n % 2 == 0


nums = [1, 2, 3, 4, 5, 6]

try:
    bad = list(filter(is_even(), nums))  # ошибка: is_even() вызван без аргумента
except TypeError as error:
    print("TypeError:", error)
    # TypeError: is_even() missing 1 required positional argument: 'n'

good = list(filter(is_even, nums))  # верно: передаём ссылку, вызовет сам filter
print(good)  # [2, 4, 6]

forgot_list = filter(is_even, nums)
print(forgot_list)        # <filter object at 0x...> — забыли list()
print(list(forgot_list))  # [2, 4, 6]

Что происходит: is_even() со скобками — это уже вызов, а не ссылка, и Python пытается выполнить его немедленно, без аргумента n, поэтому падает TypeError ещё до того, как filter получит хоть один элемент. Почему так: filter сам вызывает переданную функцию для каждого элемента — ему нужна ссылка на функцию (is_even), а не готовый результат. Вторая ловушка — печать самого объекта filter вместо его элементов, разобрана подробнее в типичных ошибках.

Пример 9. Готовые инструменты вместо lambda: itemgetter и partial

Две частые задачи практикума — «отсортировать словарь по значению» (задание 6) и «применить функцию с одним и тем же зафиксированным аргументом к списку» — уже решены в стандартной библиотеке модулями operator и functools.

# partial_itemgetter.py
from functools import partial
from operator import itemgetter

students_avg = {"Charlie": 92.33, "Alice": 87.67, "Diana": 84.67, "Bob": 78.0}

by_score = sorted(students_avg.items(), key=itemgetter(1), reverse=True)
print(by_score)
# [('Charlie', 92.33), ('Alice', 87.67), ('Diana', 84.67), ('Bob', 78.0)]
# itemgetter(1) делает то же, что lambda item: item[1]

round_to_1 = partial(round, ndigits=1)
print(list(map(round_to_1, [92.33, 87.666, 84.674])))  # [92.3, 87.7, 84.7]
⚠️ Проверить по документации: operator.itemgetter и functools.partial выходят за рамки лекции — это дополнение курса. Сверьтесь с разделами operator.itemgetter и functools.partial. В самом практикуме достаточно lambda; эти инструменты пригодятся в рабочем коде.

Пример 10. Очередь задач с ограничением времени (задание 5)

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

# task_queue_limit.py
def schedule_within_limit(
    tasks: dict[str, int],
    time_limit: int
) -> dict[str, int]:
    ordered = sorted(tasks.items(), key=lambda item: item[1], reverse=True)
    selected: dict[str, int] = {}
    total = 0
    for name, duration in ordered:
        if total + duration <= time_limit:
            selected[name] = duration
            total += duration
    return selected


tasks = {"task1": 5, "task2": 3, "task3": 7, "task4": 2}
print(schedule_within_limit(tasks, 10))  # {'task3': 7, 'task2': 3}

Что происходит: sorted(tasks.items(), key=lambda item: item[1], reverse=True) сортирует пары «имя — время» по убыванию времени (сначала task3=7, затем task1=5, task2=3, task4=2), а цикл добавляет задачу, только если она ещё помещается в оставшийся лимит. Почему так: после task3 (7 из 10) для task1 (5) места уже не остаётся, поэтому она пропускается, а более короткая task2 (3) укладывается ровно в остаток — исходный словарь tasks при этом не изменяется, функция возвращает новый selected (см. ошибку 4).

Как запустить в VS Code

  1. Создайте файл, например practice11.py.
  2. Скопируйте код одного из примеров.
  3. Откройте терминал в VS Code (Ctrl + `).
  4. Убедитесь, что активировано виртуальное окружение: venv\Scripts\activate (Windows) или source venv/bin/activate (Mac/Linux).
  5. Запустите: python practice11.py