# Декораторы: добавляем поведение функции, не трогая её<no value>

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

- Что такое **декоратор** (Decorator) и чем он полезен.
- Как построить декоратор: функция, которая получает функцию и возвращает **обёртку** (wrapper).
- Синтаксис **`@`**.
- Обёртку, подходящую для **любой** функции, с `*args` и `**kwargs`.

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

Мы хотим печатать сообщение при каждом вызове функции — для отладки:

```python
def add(a, b):
    print("Calling add")
    return a + b

def multiply(a, b):
    print("Calling multiply")
    return a * b
```

Проблемы:

- **Одна и та же строка копируется** в каждую функцию — снова дублирование, которое запрещает DRY.
- **Логика смешивается** со вспомогательным кодом, который к ней не относится.
- Чтобы **убрать** печать, нужно пройти по всем функциям и удалить её.

**Декоратор** позволяет написать дополнительное поведение **один раз** и надеть его на любую функцию одной строкой.

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

### Из чего построен декоратор

Декоратор сочетает две вещи, которые мы изучили в предыдущих уроках:

- **Функцию высшего порядка** — он получает функцию и возвращает функцию.
- **Замыкание** — функция, которую он возвращает, помнит исходную функцию.

Структура всегда одна и та же:

1. Декоратор получает исходную функцию (`func`).
2. Внутри него определяется новая функция — **`wrapper`** (обёртка), — которая делает что-то дополнительное **и вызывает `func`**.
3. Декоратор возвращает `wrapper` — **без скобок**.

### Синтаксис @

```python
@log_call
def add(a, b):
    return a + b
```

Это абсолютно то же самое, что написать после определения:

```python
add = log_call(add)
```

То есть имя `add` теперь указывает на **обёртку**, а обёртка вызывает исходную `add`.

{{< box "analogy" "Аналогия: упаковка подарка" >}}
Подарок (функция) не меняется. Вы заворачиваете его в бумагу (обёртку) — и тот, кто его получит, сначала встретит упаковку, а уже потом подарок. В одну и ту же бумагу можно завернуть любой подарок, и упаковку можно снять, не повредив подарок.
{{< /box >}}

## Синтаксис

```python
def my_decorator(func):              # 1. receive the original function
    def wrapper(*args, **kwargs):    # 2. a new function that wraps it
        # ... something before ...
        result = func(*args, **kwargs)
        # ... something after ...
        return result                # return the original result
    return wrapper                   # 3. return the wrapper (no parentheses)

@my_decorator                        # same as: my_function = my_decorator(my_function)
def my_function():
    ...
```

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

`week-2/log_call.py`:

```python
def log_call(func):
    def wrapper(a, b):
        print("Calling", func.__name__)
        return func(a, b)
    return wrapper

@log_call
def add(a, b):
    return a + b

@log_call
def multiply(a, b):
    return a * b

print(add(2, 3))
print(multiply(4, 5))
```

**Вывод:**

```text
Calling add
5
Calling multiply
20
```

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

| Строка | Объяснение |
|---|---|
| `def log_call(func):` | Декоратор получает исходную функцию. |
| `def wrapper(a, b):` | Обёртка — получает те же параметры, что и оригинал. |
| `print("Calling", func.__name__)` | Дополнительное поведение. `__name__` — имя функции. |
| `return func(a, b)` | Вызвать оригинал и **вернуть** его результат. |
| `return wrapper` | Декоратор возвращает обёртку — без скобок. |
| `@log_call` | `add = log_call(add)`. Отныне `add(2, 3)` запускает обёртку. |

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

**Обёртка, подходящая для любой функции.** Предыдущая обёртка работает только с функциями, которые получают ровно `a, b`. С `*args` и `**kwargs` она принимает **любые** параметры и передаёт их дальше. `week-2/log_any.py`:

```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__, "with", args, kwargs)
        return func(*args, **kwargs)
    return wrapper

@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name

@log_call
def total(*prices):
    return sum(prices)

print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

**Вывод:**

```text
Calling greet with ('Dana',) {'greeting': 'Hi'}
Hi, Dana
Calling total with (10, 20, 30) {}
60
```

**Полезный пример: измерение времени.** `week-2/timed.py`:

```python
import time

def timed(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        seconds = round(time.time() - start, 2)
        print(func.__name__, "took", seconds, "seconds")
        return result
    return wrapper

@timed
def slow_sum(n):
    time.sleep(0.5)
    return sum(range(n))

print(slow_sum(1000))
```

**Вывод:**

```text
slow_sum took 0.5 seconds
499500
```

(Точное время немного меняется от запуска к запуску.) Тот же `@timed` можно надеть на любую функцию программы — не меняя в ней ни одной строки.

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

**Две ошибки, которые случаются почти у всех, кто пишет первый декоратор:**

```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        func(*args, **kwargs)
    return wrapper()
```

1. **Нет `return` внутри обёртки.** Исходная функция выполняется, но её результат теряется — и декорированная функция всегда возвращает `None`. Исправление: `return func(*args, **kwargs)`.
2. **`return wrapper()` со скобками.** Это **вызывает** обёртку уже при определении и возвращает её результат вместо функции. Исправление: `return wrapper`.

**Правило:** обёртка возвращает **результат**, а декоратор возвращает **обёртку**.

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

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

### Упражнение 1: Первый декоратор

Напишите декоратор с именем `log_call`, который печатает `Calling <name>` перед каждым вызовом функции, и примените его через `@` к `add` и к `multiply`.

```python
print(add(2, 3))
print(multiply(4, 5))
```

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

```text
Calling add
5
Calling multiply
20
```

{{< solution >}}
```python
def log_call(func):
    def wrapper(a, b):
        print("Calling", func.__name__)
        return func(a, b)
    return wrapper


@log_call
def add(a, b):
    return a + b


@log_call
def multiply(a, b):
    return a * b


print(add(2, 3))
print(multiply(4, 5))
```

```text
Calling add
5
Calling multiply
20
```

`@log_call` над `def add` равносильно `add = log_call(add)`. Отныне `add` указывает на обёртку.
{{< /solution >}}

### Упражнение 2: Обёртка, подходящая для любой функции

Предыдущая обёртка работает только с функциями, которые получают ровно два параметра. Перепишите `log_call` так, чтобы он работал с любой функцией.

```python
@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name

@log_call
def total(*prices):
    return sum(prices)

print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

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

```text
Calling greet
Hi, Dana
Calling total
60
```

{{< solution >}}
```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper


@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name


@log_call
def total(*prices):
    return sum(prices)


print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

```text
Calling greet
Hi, Dana
Calling total
60
```

`*args` и `**kwargs` собирают все аргументы, а `func(*args, **kwargs)` передаёт их дальше как есть.
{{< /solution >}}

### Упражнение 3: Проверка ввода

Напишите декоратор с именем `positive_only` для функций, которые получают одно число: если число отрицательное, он выбрасывает `ValueError` и вообще не вызывает функцию. Сообщение включает имя функции (`func.__name__`) и полученное число, как в ожидаемом выводе.

```python
@positive_only
def square_root(n):
    return n ** 0.5

print(square_root(16))
try:
    square_root(-4)
except ValueError as error:
    print("error:", error)
```

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

```text
4.0
error: square_root needs a positive number, got -4
```

{{< solution >}}
```python
def positive_only(func):
    def wrapper(n):
        if n < 0:
            raise ValueError(func.__name__ + " needs a positive number, got " + str(n))
        return func(n)
    return wrapper


@positive_only
def square_root(n):
    return n ** 0.5


print(square_root(16))
try:
    square_root(-4)
except ValueError as error:
    print("error:", error)
```

```text
4.0
error: square_root needs a positive number, got -4
```

Проверка происходит **до** вызова `func` — если ввод недопустим, исходная функция вообще не выполняется.
{{< /solution >}}

### Упражнение 4: Подсчёт вызовов

Напишите декоратор с именем `count_calls`, который считает, сколько раз была вызвана функция. При каждом вызове, **до** выполнения функции, он печатает имя функции и номер вызова — как в ожидаемом выводе.

```python
@count_calls
def say_hi():
    print("Hi")

say_hi()
say_hi()
```

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

```text
say_hi call #1
Hi
say_hi call #2
Hi
```

{{< solution >}}
```python
def count_calls(func):
    count = 0

    def wrapper(*args, **kwargs):
        nonlocal count
        count += 1
        print(func.__name__, "call #" + str(count))
        return func(*args, **kwargs)

    return wrapper


@count_calls
def say_hi():
    print("Hi")


say_hi()
say_hi()
```

```text
say_hi call #1
Hi
say_hi call #2
Hi
```

`count` принадлежит `count_calls`, и обёртка его помнит (замыкание). Поскольку она **меняет** `count`, нужен `nonlocal`.
{{< /solution >}}

### Упражнение 5: Две ошибки

```python
def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        result.upper()
    return wrapper()
```

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

```python
@shout
def greet(name):
    return "hello " + name

print(greet("dana"))
```

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

```text
HELLO DANA
```

{{< solution >}}
1. **`return wrapper()` со скобками** — вызывает обёртку уже при определении, вместо того чтобы её вернуть. Должно быть `return wrapper`.
2. **Обёртка ничего не возвращает** — `result.upper()` создаёт новую строку, но она не возвращается. Должно быть `return result.upper()`.

```python
def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper


@shout
def greet(name):
    return "hello " + name


print(greet("dana"))
```

```text
HELLO DANA
```

Правило: обёртка возвращает **результат**, а декоратор возвращает **обёртку**.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **Декоратор** = функция, которая получает функцию, заворачивает её в **обёртку** с дополнительным поведением и возвращает обёртку.
- Он строится из **функции высшего порядка** и **замыкания** — двух тем предыдущих уроков.
- `@decorator` над `def` = `func = decorator(func)`.
- С `*args` и `**kwargs` обёртка подходит для любой функции.
- Обёртка возвращает **результат** (`return func(...)`), а декоратор возвращает **обёртку** (`return wrapper`, без скобок).

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

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

- [Python Glossary — decorator](https://docs.python.org/3/glossary.html#term-decorator)
- [Function definitions](https://docs.python.org/3/reference/compound_stmts.html#function-definitions)
- [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps)

<!-- docs:end -->
