Функции высшего порядка: функции, которые получают и возвращают функции

Цели урока
  • Объяснить, что такое функция высшего порядка (Higher-Order Function) и почему она возможна в Python
  • Передавать функцию как параметр — без скобок — и вызывать её изнутри другой функции
  • Использовать map, filter и параметр key в sorted, max и min
  • Писать функцию, которая возвращает функцию

Что мы изучим?

  • Что такое функция высшего порядка (Higher-Order Function).
  • Как передать функцию как параметр — и почему без скобок.
  • Встроенные функции map и filter.
  • Параметр key в sorted, max и min.
  • Функцию, которая возвращает функцию.

Зачем это нужно?

Две почти одинаковые функции:

def double_all(numbers):
    result = []
    for n in numbers:
        result.append(n * 2)
    return result

def square_all(numbers):
    result = []
    for n in numbers:
        result.append(n * n)
    return result

Цикл одинаковый — отличается только действие над каждым элементом. Каждое новое действие (половина, плюс 10, округление…) требует ещё одной целой функции, а это именно то дублирование, которое запрещает DRY.

Идея: написать цикл один раз, а само действие передавать как параметр.

Объяснение с основ

Функция — это объект

Мы уже видели в уроке «функции подробно», что в Python функция — объект, как число или список:

  • double (без скобок) — сама функция. Её можно сохранить в переменной или передать дальше.
  • double(5) (со скобками) — вызов функции. Результат — то, что она возвращает.

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

Функция высшего порядка (Higher-Order Function) — функция, которая:

  • Получает другую функцию как параметр, или
  • Возвращает функцию.

Одной вы уже пользовались: sorted(products, key=lambda p: p[1]) — sorted получает функцию в параметре key.

Аналогия: стиральная машина с программами

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

Синтаксис

def apply_to_all(numbers, action):     # action is a function
    result = []
    for n in numbers:
        result.append(action(n))       # call it for each item
    return result

apply_to_all([1, 2, 3], double)        # pass it: no parentheses
list(map(double, [1, 2, 3]))           # built-in: map
list(filter(is_even, [1, 2, 3, 4]))    # built-in: filter
sorted(words, key=len)                 # key: sort by the result of len

Первый пример кода

week-2/apply_to_all.py:

def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result

def double(n):
    return n * 2

print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))

Вывод:

[2, 4, 6]
[1, 4, 9]
[2, 2]

Разбор кода построчно

СтрокаОбъяснение
def apply_to_all(numbers, action):action — обычный параметр, только мы ожидаем, что в нём будет функция.
result.append(action(n))Вызываем полученную функцию для каждого элемента.
apply_to_all([1, 2, 3], double)Передаём double без скобок — функцию, а не её результат.
lambda n: n * nКороткая функция без имени, созданная на месте.
roundМожно передать и встроенную функцию.

Ещё один пример

map, filter и key. week-2/hof_builtins.py:

prices = [100, 250, 40, 80]

with_fee = list(map(lambda p: p + 10, prices))
cheap = list(filter(lambda p: p < 100, prices))
print(with_fee)
print(cheap)

words = ["banana", "fig", "cherry", "kiwi"]
print(sorted(words, key=len))
print(max(words, key=len))

students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]
print(max(students, key=lambda s: s[1]))

Вывод:

[110, 260, 50, 90]
[40, 80]
['fig', 'kiwi', 'banana', 'cherry']
banana
('Dana', 92)
  • map(func, items) — запускает func для каждого элемента. list(...) превращает результат в список.
  • filter(func, items) — оставляет только элементы, для которых func возвращает True.
  • key=len — sorted и max сравнивают по результату len для каждого слова, а не по самому слову.

Функция, которая возвращает функцию. week-2/make_multiplier.py:

def make_multiplier(factor):
    def multiply(n):
        return n * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))
print(triple(5))

Вывод:

10
15

make_multiplier — «фабрика», которая производит функции: каждый вызов возвращает новую функцию, которая помнит свой factor. Как именно она помнит — увидим в следующем уроке, о вложенных функциях.

Типичная ошибка

Ошибка: вызвать функцию вместо того, чтобы передать её.

apply_to_all([1, 2], double())
TypeError: double() missing 1 required positional argument: 'n'

Почему? double() со скобками вызывает функцию немедленно — без параметра, — поэтому она падает ещё до того, как начнётся apply_to_all. Передавайте функцию без скобок: apply_to_all([1, 2], double). Вызовет её та функция, которая её получила.

Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

Упражнение 1: Функция как параметр

Напишите функцию apply_to_all(numbers, action), которая запускает action для каждого числа и возвращает новый список. Используйте её с обычной функцией double, с lambda, которая возводит в квадрат, и с round.

print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))

Ожидаемый вывод:

[2, 4, 6]
[1, 4, 9]
[2, 2]
Показать решение
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))
[2, 4, 6]
[1, 4, 9]
[2, 2]

Передавайте функцию без скобок (double, round). Вызывает её apply_to_all.

Упражнение 2: map и filter

prices = [20, 75, 120, 45, 90]
  1. Оставьте только цены выше 50 с помощью filter.
  2. Прибавьте 10 к каждой оставшейся цене с помощью map.

Ожидаемый вывод:

[75, 120, 90]
[85, 130, 100]
Показать решение
prices = [20, 75, 120, 45, 90]

expensive = list(filter(lambda p: p > 50, prices))
raised = list(map(lambda p: p + 10, expensive))
print(expensive)
print(raised)
[75, 120, 90]
[85, 130, 100]

filter оставляет только то, для чего lambda возвращает True. map запускает lambda для каждого элемента. list(...) превращает результат в список.

Упражнение 3: key — по чему сортировать

words = ["banana", "fig", "cherry", "kiwi"]
students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]
  1. Отсортируйте слова по длине.
  2. Найдите самое длинное слово.
  3. Отсортируйте студентов по оценке, от высшей к низшей, и напечатайте только имена.

Ожидаемый вывод:

['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi
Показать решение
words = ["banana", "fig", "cherry", "kiwi"]
students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]

print(sorted(words, key=len))
print(max(words, key=len))

for name, grade in sorted(students, key=lambda s: s[1], reverse=True):
    print(name)
['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi

key=len — сравнивать по результату len для каждого слова, а не по самому слову.

Упражнение 4: Функция, которая возвращает функцию

Напишите функцию make_discount(percent), которая возвращает функцию, получающую цену и возвращающую её после скидки.

ten_off = make_discount(10)
half_price = make_discount(50)
print(ten_off(200))
print(half_price(200))

Ожидаемый вывод:

180.0
100.0
Показать решение
def make_discount(percent):
    def apply(price):
        return price - price * percent / 100
    return apply


ten_off = make_discount(10)
half_price = make_discount(50)
print(ten_off(200))
print(half_price(200))
180.0
100.0

make_discount — «фабрика»: каждый вызов производит новую функцию, которая помнит свой percent.

Упражнение 5: Вызов вместо передачи

def double(n):
    return n * 2

print(apply_to_all([1, 2], double()))
  1. Какую ошибку вы получите и почему?
  2. Как исправить?
Показать решение
  1. TypeError: double() missing 1 required positional argument: 'n'. Скобки в double() вызывают функцию немедленно — без параметра — ещё до того, как начнётся apply_to_all.
  2. Передайте саму функцию, без скобок:
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2], double))
[2, 4]

Итоги

  • Функция высшего порядка = функция, которая получает функцию или возвращает функцию. Это возможно, потому что функция — объект.
  • Передавайте функцию без скобок — вызовет её та функция, которая её получила.
  • map — действие над каждым элементом. filter — оставляет то, что удовлетворяет условию.
  • key= в sorted, max, min — задаёт, по чему сравнивать.
  • Функция может возвращать функцию — «фабрика», которая производит настроенные функции.

Официальная документация