# Чистый код A: хорошие имена, маленькие функции и единственная ответственность<no value>

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

До сих пор целью было «чтобы код работал». С сегодняшнего дня цель — чтобы он был ещё и **понятным**. Мы изучим:

- **Именование** (naming) — как выбирать имена для переменных, функций и файлов.
- **Маленькие функции** (small functions) — почему функции должны быть короткими.
- **Единственную ответственность** (single responsibility) — каждая функция делает **одно дело**.

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

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

В компаниях код, который работает, но нечитаем, **отклоняют на Code Review**. На собеседованиях проверяют не только «решили ли вы», но и «можно ли понять решение».

Сравните:

```python
def f(l):
    t = 0
    for x in l:
        t = t + x[1] * x[2]
    return t * 1.17
```

```python
VAT_RATE = 1.17

def calculate_order_total(items):
    subtotal = 0
    for item in items:
        subtotal = subtotal + item["price"] * item["quantity"]
    return subtotal * VAT_RATE
```

Обе функции делают **абсолютно** одно и то же. Вторую понимаешь без усилий.

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

### Что такое Clean Code?

**Clean Code** (чистый код) — код, который **другой человек** может легко и уверенно прочитать, понять и изменить. Это не набор жёстких правил, а набор привычек, которые делают код понятным.

### Именование — хорошие имена

Хорошее имя отвечает на вопрос **«что это?»** или **«что это делает?»** без необходимости читать окружающий код.

**Соглашения об именах в Python (по PEP 8, официальному руководству по стилю):**

| Что | Стиль | Пример |
|---|---|---|
| Переменные и функции | `snake_case` | `total_price`, `send_email()` |
| Константы (значения, которые не меняются) | `UPPER_SNAKE_CASE` | `VAT_RATE`, `MAX_RETRIES` |
| Классы (тема ООП) | `PascalCase` | `Student`, `BankAccount` |
| Файлы и модули | `snake_case.py` | `order_utils.py` |

**Правила хорошего имени:**

1. **Описывает намерение** — `days_until_exam` лучше, чем `d`.
2. **Функция = глагол** — `calculate_total()`, `send_invoice()`, `is_valid_email()`. **Переменная = существительное** — `total`, `invoice`, `email`.
3. **Логическое значение звучит как вопрос** — `is_active`, `has_permission`, `can_edit`.
4. **Список — во множественном числе** — `students` (список), `student` (один элемент).
5. **Без непонятных сокращений** — `customer`, а не `cstmr`. (Известные сокращения вроде `id`, `url`, `max` допустимы.)
6. **Без «магических чисел»** (magic numbers) — вместо `* 1.17`, смысла которого никто не знает, — именованная константа: `VAT_RATE = 1.17`.

### Маленькие функции

Длинную функцию (50+ строк) трудно понять, трудно протестировать и трудно изменить. **Эмпирическое правило:** если функция не помещается на один экран или ей трудно дать имя без слова «и», она, вероятно, делает слишком много.

### Единственная ответственность

**Принцип единственной ответственности** (Single Responsibility Principle, SRP): у каждой функции должна быть **одна причина для изменения**, то есть одна задача.

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

## Синтаксис

Для чистого кода нет «нового синтаксиса». Это **тот же** Python, просто организованный по-другому. Шаблон:

```python
CONSTANT_NAME = value          # constants at the top, UPPER_SNAKE_CASE

def verb_noun(clear_parameter):   # a function does ONE thing, named by what it does
    ...
    return result

def main():                     # one function that connects the small pieces
    data = get_something()
    result = process_something(data)
    show_something(result)

main()
```

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

**До** — код, который работает, но не чистый. `week-2/grades_before.py`:

```python
def do(g):
    s = 0
    for x in g:
        s = s + x
    a = s / len(g)
    if a >= 55:
        print("avg", a, "pass")
    else:
        print("avg", a, "fail")

do([80, 45, 92, 60])
```

**После** — то же поведение, чистый код. `week-2/grades_after.py`:

```python
PASSING_GRADE = 55

def calculate_average(grades):
    total = 0
    for grade in grades:
        total = total + grade
    return total / len(grades)

def is_passing(average):
    return average >= PASSING_GRADE

def print_report(average):
    if is_passing(average):
        status = "pass"
    else:
        status = "fail"
    print("Average:", average, "-", status)

student_grades = [80, 45, 92, 60]
average = calculate_average(student_grades)
print_report(average)
```

**Вывод (версия «после». Старая версия печатает `avg 69.25 pass` — тот же расчёт, сформулированный менее понятно):**

```text
Average: 69.25 - pass
```

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

| В старом коде | В чистом коде | Почему это лучше |
|---|---|---|
| `do` | Три функции с именами | Имя говорит, что делает каждая. |
| `g`, `s`, `x`, `a` | `grades`, `total`, `grade`, `average` | Не нужно «расшифровывать». |
| `55` внутри кода | `PASSING_GRADE = 55` | Значение со смыслом и именем. Если проходной балл изменится, меняете в одном месте. |
| Расчёт + решение + печать в одной функции | `calculate_average`, `is_passing`, `print_report` | Каждая функция делает **одно дело**. |

**Практическое преимущество:** хотите показывать среднее на сайте, а не на экране? Меняется только `print_report`. Хотите изменить проходной балл? Только `PASSING_GRADE`. А `calculate_average` пригодится и в других местах, потому что она **возвращает** значение, а не печатает его.

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

Функция «которая делает всё» в системе регистрации, и как её разбить. **До:**

```python
def register(n, e, a):
    if "@" not in e:
        print("bad email")
        return
    if a < 16:
        print("too young")
        return
    u = n.strip().lower().replace(" ", ".")
    print("Welcome " + n + "! Your username is " + u)
```

**После:**

```python
MIN_AGE = 16

def is_valid_email(email):
    return "@" in email

def is_old_enough(age):
    return age >= MIN_AGE

def create_username(full_name):
    return full_name.strip().lower().replace(" ", ".")

def register_user(full_name, email, age):
    if not is_valid_email(email):
        print("Invalid email address")
        return
    if not is_old_enough(age):
        print("You must be at least", MIN_AGE, "to register")
        return
    username = create_username(full_name)
    print("Welcome " + full_name + "! Your username is " + username)

register_user("Dana Cohen", "dana@mail.com", 24)
register_user("Avi Levi", "avi-mail.com", 30)
```

**Вывод:**

```text
Welcome Dana Cohen! Your username is dana.cohen
Invalid email address
```

**Разбор:**

- `"@" in email` — `in` проверяет, встречается ли кусок текста внутри другого текста, и возвращает `True`/`False`.
- `.strip()` — убирает пробелы в начале и в конце. `.lower()` — строчные буквы. `.replace(" ", ".")` — заменяет пробелы точками. Это **методы** `str`, которые можно вызывать цепочкой один за другим.
- `return` без значения — немедленно выходит из функции (возвращает `None`). Так мы не продолжаем регистрацию после ошибки. Этот приём называется **Early Return** (или Guard Clause).
- `register_user` теперь «читается как рассказ»: проверить email, проверить возраст, создать имя пользователя, поприветствовать. Все детали — в маленьких функциях.

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

**Ошибка 1: имена, которые лгут.**

```python
def get_user(user_id):
    user = find_in_list(user_id)
    user["last_login"] = "today"   # changes data!
    delete_old_sessions(user)      # deletes data!
    return user
```

Имя говорит «получить», а функция **изменяет и удаляет**. Тот, кто вызовет её, чтобы «просто получить пользователя», будет удивлён. Имя должно описывать **всё**, что делает функция. А если это трудно описать без «и», то ответственность не одна.

**Ошибка 2: чрезмерное дробление.** Обратное — тоже проблема:

```python
def add_one(number):
    return number + 1
```

Однострочная функция, которая скрывает очевидную операцию, только добавляет переходов при чтении. Разбивайте, когда есть **идея с именем** (`is_old_enough`), а не каждую строку.

**Ошибка 3: слишком общие имена.** `data`, `info`, `temp`, `result`, `manager`, `process()`. Такие имена почти всегда можно улучшить: `data` → `student_records`, `process()` → `apply_discount()`.

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

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

### Упражнение 1: Имена, которые объясняют сами себя

Следующий код работает, но понять, что он делает, невозможно:

```python
def f(a, b):
    c = a * b
    if c > 500:
        c = c - 50
    return c

print(f(120, 5))
print(f(30, 2))
```

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

```text
550
60
```

{{< solution >}}
```python
DISCOUNT_THRESHOLD = 500
DISCOUNT_AMOUNT = 50


def calculate_order_price(unit_price, quantity):
    price = unit_price * quantity
    if price > DISCOUNT_THRESHOLD:
        price = price - DISCOUNT_AMOUNT
    return price


print(calculate_order_price(120, 5))
print(calculate_order_price(30, 2))
```

```text
550
60
```

- Имя функции — **глагол**, который описывает, что она делает.
- `500` и `50` были **магическими числами** — теперь ясно, что это, и если скидка изменится, меняете в одном месте.
{{< /solution >}}

### Упражнение 2: Соглашения об именах

Для каждого имени — записано ли оно по соглашениям Python? Если нет, напишите правильное имя.

```text
TotalPrice      (a variable)
numberofstudents (a variable)
max_grade       (a variable)
vat_rate        (a constant that never changes)
GetAverage      (a function)
```

{{< solution >}}
| Имя | Правильно? | Правильное имя |
|---|---|---|
| `TotalPrice` | Нет — переменные пишут в snake_case | `total_price` |
| `numberofstudents` | Нет — трудно читать без подчёркиваний | `number_of_students` |
| `max_grade` | Да | — |
| `vat_rate` | Нет — константы пишут заглавными буквами | `VAT_RATE` |
| `GetAverage` | Нет — функции пишут в snake_case, и лучше глагол | `get_average` / `calculate_average` |
{{< /solution >}}

### Упражнение 3: Одна функция — одна ответственность

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

```python
def report(name, grades):
    total = 0
    for g in grades:
        total = total + g
    avg = total / len(grades)
    if avg >= 55:
        print(name + ": average", avg, "- passed")
    else:
        print(name + ": average", avg, "- failed")

report("Dana", [80, 85])
report("Avi", [50, 46])
```

Разбейте её на три функции: `calculate_average`, `is_passing` и `print_report(name, average)`. `print_report` получает **уже вычисленное** среднее и только печатает. Вывод должен остаться:

```text
Dana: average 82.5 - passed
Avi: average 48.0 - failed
```

{{< solution >}}
```python
PASSING_GRADE = 55


def calculate_average(grades):
    return sum(grades) / len(grades)


def is_passing(average):
    return average >= PASSING_GRADE


def print_report(name, average):
    if is_passing(average):
        status = "passed"
    else:
        status = "failed"
    print(name + ": average", average, "-", status)


dana_average = calculate_average([80, 85])
print_report("Dana", dana_average)
avi_average = calculate_average([50, 46])
print_report("Avi", avi_average)
```

```text
Dana: average 82.5 - passed
Avi: average 48.0 - failed
```

- `calculate_average` только вычисляет, `is_passing` только решает, а `print_report` получает готовое среднее и только показывает его. Вычисление происходит **вне** её.
- Если бы `print_report` сама вызывала `calculate_average`, она снова и вычисляла бы, и печатала. Код короче, но ответственности не разделены.
- Теперь `calculate_average` можно использовать и там, где печатать не нужно.
{{< /solution >}}

### Упражнение 4: Без магических чисел

В интернет-магазине следующая функция получает сумму заказа и возвращает сумму к оплате:

```python
def final_price(total):
    if total < 200:
        total = total + 30
    return total

print(final_price(150))
print(final_price(250))
```

1. Что означают 200 и 30 в магазине? (Подсказка: что происходит с маленьким заказом?) Дайте им имена в виде констант.
2. Перепишите функцию так, чтобы её имя точно объясняло, что она добавляет.

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

```text
180
250
```

{{< solution >}}
```python
FREE_SHIPPING_MINIMUM = 200
SHIPPING_FEE = 30


def add_shipping(order_total):
    if order_total < FREE_SHIPPING_MINIMUM:
        return order_total + SHIPPING_FEE
    return order_total


print(add_shipping(150))
print(add_shipping(250))
```

```text
180
250
```

200 — **минимальная сумма для бесплатной доставки**, а 30 — **стоимость доставки**. С именами никому не нужно гадать.
{{< /solution >}}

### Упражнение 5: Маленькая функция, которая отвечает на один вопрос

Напишите функцию `is_valid_password(password)`, которая возвращает `True`, если длина пароля не меньше 8 символов **и** в нём есть хотя бы одна цифра, иначе `False`. Функция не печатает — только возвращает.

```python
print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))
```

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

```text
True
False
False
```

{{< solution >}}
```python
MIN_PASSWORD_LENGTH = 8


def has_digit(text):
    for char in text:
        if char.isdigit():
            return True
    return False


def is_valid_password(password):
    return len(password) >= MIN_PASSWORD_LENGTH and has_digit(password)


print(is_valid_password("abc12345"))
print(is_valid_password("abcdefgh"))
print(is_valid_password("a1"))
```

```text
True
False
False
```

Имя `is_valid_password` начинается с `is_` — ясно, что она возвращает `True` или `False`. Проверка цифры вынесена в собственную маленькую функцию.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **Clean Code** = код, который другие (и вы через месяц) легко понимают и меняют. Код читают гораздо чаще, чем пишут.
- **Имена:** `snake_case` для переменных и функций, `UPPER_SNAKE_CASE` для констант. Функция = глагол, логическое значение = вопрос (`is_...`).
- **Без магических чисел.** Значение со смыслом получает именованную константу.
- **Маленькие функции** с **единственной ответственностью** (SRP). Если в имени нужно «и», вероятно, нужно разбить.
- Функция, которая вычисляет, — **возвращает**. Печать — отдельная ответственность.

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

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

- [PEP 8 — Naming Conventions](https://peps.python.org/pep-0008/#naming-conventions)
- [PEP 8 — Function and Variable Names](https://peps.python.org/pep-0008/#function-and-variable-names)
- [PEP 20 — The Zen of Python](https://peps.python.org/pep-0020/)

<!-- docs:end -->
