# Чистые функции и побочные эффекты<no value>

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

- Что такое **чистая функция** (Pure Function).
- Что такое **побочный эффект** (Side Effect) и какие они бывают.
- Ловушку **изменения списка, полученного как параметр**.
- Как написать **чистую версию** функции.

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

Вот функция, которая выглядит невинно:

```python
total = 0

def add_to_total(price):
    global total
    total += price
    return total

print(add_to_total(10))
print(add_to_total(10))
```

**Вывод:**

```text
10
20
```

**Абсолютно одинаковый вызов — и разный результат.** Функция зависит от внешней переменной и к тому же меняет её. Чтобы знать, что она вернёт, нужно знать всю историю программы. Её трудно тестировать и ей трудно доверять.

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

### Что такое чистая функция

**Чистая функция** удовлетворяет двум условиям:

1. **Тот же ввод → всегда тот же результат.** Результат зависит только от параметров — не от глобальных переменных, не от времени, не от ввода пользователя.
2. **Без побочных эффектов.** Она ничего не меняет вне себя. Всё, что она «выдаёт», выходит через `return`.

### Что считается побочным эффектом

| Побочный эффект | Пример |
|---|---|
| Изменение глобальной переменной | `global total`, а затем `total += price` |
| Изменение полученного списка или словаря | `prices[i] = ...` для списка, пришедшего как параметр |
| Ввод и вывод с пользователем | `print(...)`, `input(...)` |
| Запись в файл или базу данных | `open(..., "w")`, `INSERT` |

{{< box "analogy" "Аналогия: соковыжималка" >}}
Чистая функция — как соковыжималка: кладёте 3 апельсина — выходит стакан сока. Всегда одно и то же количество, и соковыжималка больше ничего на кухне не трогает. Нечистая функция — соковыжималка, которая иногда ещё и двигает холодильник, — и вдруг в доме что-то не работает, а вы понятия не имеете почему.
{{< /box >}}

## Синтаксис

Специального синтаксиса нет — это **способ писать** функции:

```python
# pure: result depends only on the parameters
def add_price(total, price):
    return total + price

# not pure: reads and changes something outside
def add_to_total(price):
    global total
    total += price
    return total
```

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

Та же функция из начала урока в чистой версии. `week-2/pure_total.py`:

```python
def add_price(total, price):
    return total + price

total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

**Вывод:**

```text
20
10
10
```

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

| Строка | Объяснение |
|---|---|
| `def add_price(total, price):` | Всё, что нужно функции, приходит **через параметры**. |
| `return total + price` | Она ничего не меняет — только вычисляет и возвращает. |
| `total = add_price(total, 10)` | **Вызывающий код** решает, что делать с результатом: здесь — сохранить его в `total`. |
| `print(add_price(0, 10))` дважды | Тот же ввод → тот же результат, `10`, каждый раз. |

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

**Ловушка: изменение полученного списка.** `week-2/discount_trap.py`:

```python
def add_discount(prices):
    for i in range(len(prices)):
        prices[i] = prices[i] * 0.9
    return prices

original = [100, 200]
discounted = add_discount(original)
print(discounted)
print(original)
```

**Вывод:**

```text
[90.0, 180.0]
[90.0, 180.0]
```

**`original` изменился!** Как мы изучили в уроке об изменяемости, список передаётся в функцию **как ссылка** — `prices` и `original` — два имени для одного списка. Тот, кто позже в программе использует `original`, вдруг получает другие цены.

**Чистая версия — построить новый список:**

```python
def add_discount(prices):
    result = []
    for price in prices:
        result.append(price * 0.9)
    return result

original = [100, 200]
discounted = add_discount(original)
print(discounted)
print(original)
```

**Вывод:**

```text
[90.0, 180.0]
[100, 200]
```

Теперь `original` остаётся ровно таким, каким был, а `discounted` — совершенно новый список.

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

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

Правило — **разделять**:

- **Вычисление** — в чистых функциях (`calculate_total`, `apply_discount`).
- **Общение с миром** — в нескольких понятных местах (`print`, сохранение в файл).

```python
def calculate_total(prices):        # pure: easy to test
    return sum(prices)

def show_total(prices):             # side effect: printing
    print("Total:", calculate_total(prices))
```

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

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

### Упражнение 1: Чистая или нет?

Для каждой функции — чистая ли она? Если нет, в чём побочный эффект?

```text
def square(n):
    return n * n

def log_square(n):
    print("squaring", n)
    return n * n

def add_item(cart, item):
    cart.append(item)
    return cart

def with_item(cart, item):
    return cart + [item]
```

{{< solution >}}
| Функция | Чистая? | Почему |
|---|---|---|
| `square` | **Да** | Результат зависит только от `n`, и ничего не меняется |
| `log_square` | **Нет** | `print` — побочный эффект: вывод на экран |
| `add_item` | **Нет** | `append` меняет полученный список (мутация) |
| `with_item` | **Да** | `cart + [item]` создаёт **новый** список, а оригинал не тронут |
{{< /solution >}}

### Упражнение 2: Тот же вызов — другой результат

```python
total = 0

def add_to_total(price):
    global total
    total += price
    return total
```

Проблема: каждый вызов `add_to_total(10)` возвращает другой результат (10, потом 20, потом 30), потому что функция зависит от глобальной переменной и меняет её.

Перепишите функцию как чистую версию с именем `add_price(total, price)` — она получает текущую сумму как параметр и возвращает новую сумму, без `global`. Запустите проверочный код, который показывает, что тот же ввод всегда даёт тот же результат:

```python
total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

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

```text
20
10
10
```

{{< solution >}}
```python
def add_price(total, price):
    return total + price


total = 0
total = add_price(total, 10)
total = add_price(total, 10)
print(total)

print(add_price(0, 10))
print(add_price(0, 10))
```

```text
20
10
10
```

Всё, что нужно функции, приходит **через параметры**, а результат выходит только через `return`. Вызывающий код решает, что с ним делать.
{{< /solution >}}

### Упражнение 3: Не меняйте полученный список

```python
TAX = 1.17

def apply_tax(prices):
    for i in range(len(prices)):
        prices[i] = prices[i] * TAX
```

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

Напишите чистую версию `apply_tax(prices)`, которая возвращает **новый** список с ценами с учётом налога (каждая цена округлена до двух знаков) и не трогает полученный список. Запустите проверочный код — первая строка доказывает, что исходный список не изменился:

```python
prices = [100, 50]
with_tax = apply_tax(prices)
print(prices)
print(with_tax)
```

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

```text
[100, 50]
[117.0, 58.5]
```

{{< solution >}}
```python
TAX = 1.17


def apply_tax(prices):
    result = []
    for price in prices:
        result.append(round(price * TAX, 2))
    return result


prices = [100, 50]
with_tax = apply_tax(prices)
print(prices)
print(with_tax)
```

```text
[100, 50]
[117.0, 58.5]
```

`TAX` — константа, которая не меняется, поэтому её чтение не нарушает чистоту: тот же ввод по-прежнему всегда даёт тот же результат.
{{< /solution >}}

### Упражнение 4: Отделяем вычисление от печати

```python
def show_total(prices):
    total = 0
    for price in prices:
        total = total + price
    print("Total:", total)
```

Функция и вычисляет, и печатает. Разделите её на две:

- `calculate_total(prices)` — чистая: только вычисляет и возвращает сумму;
- `show_total(prices)` — вызывает `calculate_total` и печатает результат.

Проверочный код:

```python
show_total([10, 20, 30])
```

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

```text
Total: 60
```

{{< solution >}}
```python
def calculate_total(prices):
    return sum(prices)


def show_total(prices):
    print("Total:", calculate_total(prices))


show_total([10, 20, 30])
```

```text
Total: 60
```

Вычисление (`calculate_total`) чистое, и его легко тестировать. Побочный эффект (`print`) сосредоточен в одном понятном месте.
{{< /solution >}}

### Упражнение 5: Тройка лучших — ничего не меняя

Напишите чистую функцию `top_three(numbers)`, которая возвращает три наибольших числа, от большего к меньшему, не меняя исходный список. Напечатайте и исходный список, чтобы доказать, что он не изменился.

```python
scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)
```

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

```text
[99, 95, 88]
[70, 95, 88, 60, 99]
```

{{< solution >}}
```python
def top_three(numbers):
    return sorted(numbers, reverse=True)[:3]


scores = [70, 95, 88, 60, 99]
print(top_three(scores))
print(scores)
```

```text
[99, 95, 88]
[70, 95, 88, 60, 99]
```

`sorted` возвращает **новый** отсортированный список, а срез (`[:3]`) возвращает ещё один новый список — исходный список не тронут, поэтому функция чистая.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **Чистая функция** = тот же ввод → всегда тот же результат, и без изменения чего-либо снаружи.
- **Побочные эффекты**: изменение глобальной переменной, изменение полученного списка или словаря, `print`/`input`, запись в файл или базу данных.
- Список передаётся как ссылка — функция, которая его меняет, меняет **оригинал**. Чистая версия строит новый список.
- Чистые функции легко тестировать, они предсказуемы, и их легко переиспользовать.
- Не всё может быть чистым — отделяйте чистое **вычисление** от **общения с миром**.

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

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

- [Functional Programming HOWTO](https://docs.python.org/3/howto/functional.html#introduction)

<!-- docs:end -->
