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

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

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

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

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

```python
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`.

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

## Синтаксис

```python
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`:

```python
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))
```

**Вывод:**

```text
[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`:

```python
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]))
```

**Вывод:**

```text
[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`:

```python
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))
```

**Вывод:**

```text
10
15
```

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

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

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

```python
apply_to_all([1, 2], double())
```

```text
TypeError: double() missing 1 required positional argument: 'n'
```

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

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

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

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

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

```python
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))
```

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

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

{{< solution >}}
```python
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))
```

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

Передавайте функцию **без скобок** (`double`, `round`). Вызывает её `apply_to_all`.
{{< /solution >}}

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

```python
prices = [20, 75, 120, 45, 90]
```

1. Оставьте только цены выше 50 с помощью `filter`.
2. Прибавьте 10 к каждой оставшейся цене с помощью `map`.

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

```text
[75, 120, 90]
[85, 130, 100]
```

{{< solution >}}
```python
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)
```

```text
[75, 120, 90]
[85, 130, 100]
```

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

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

```python
words = ["banana", "fig", "cherry", "kiwi"]
students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]
```

1. Отсортируйте слова по длине.
2. Найдите самое длинное слово.
3. Отсортируйте студентов по оценке, от высшей к низшей, и напечатайте только имена.

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

```text
['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi
```

{{< solution >}}
```python
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)
```

```text
['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi
```

`key=len` — сравнивать по **результату** `len` для каждого слова, а не по самому слову.
{{< /solution >}}

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

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

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

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

```text
180.0
100.0
```

{{< solution >}}
```python
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))
```

```text
180.0
100.0
```

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

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

```python
def double(n):
    return n * 2

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

1. Какую ошибку вы получите и почему?
2. Как исправить?

{{< solution >}}
1. `TypeError: double() missing 1 required positional argument: 'n'`. Скобки в `double()` **вызывают** функцию немедленно — без параметра — ещё до того, как начнётся `apply_to_all`.
2. Передайте саму функцию, **без скобок**:

```python
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))
```

```text
[2, 4]
```
{{< /solution >}}

<!-- exercises:end -->

## Итоги

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

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

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

- [Functional Programming HOWTO — Built-in functions](https://docs.python.org/3/howto/functional.html#built-in-functions)
- [sorted() — key](https://docs.python.org/3/builtins/functions.html#sorted)

<!-- docs:end -->
