# Модули: зачем делить код на файлы, import, from и as<no value>

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

- Что такое **модуль** (Module).
- Зачем проект делят на **несколько файлов**.
- Три формы импорта: `import`, `from ... import`, `as`.
- Что **именно** происходит, когда Python импортирует файл.

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

Мы уже пользовались модулями: `import os`, `import logging`, `import socket`. Сегодня напишем **свои**.

Настоящий проект — это сотни и тысячи строк. Один огромный файл:

- **Трудно ориентироваться** — где функция, которая отправляет email? Строка 1840?
- **Трудно работать в команде** — три разработчика редактируют один файл и мешают друг другу (в Git, в теме Git).
- **Трудно переиспользовать** — хорошая функция проверки «застряла» в файле игры.

Деление на файлы — это **разделение ответственности на уровне проекта**. В темах бэкенда и баз данных сервер будет разделён именно так: `main.py` (маршруты), `db.py` (доступ к базе данных), `models.py`.

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

### Что такое модуль?

**Модуль = любой файл `.py`.** Вот и всё. Каждый файл Python, который вы написали, — уже модуль, и из него можно импортировать функции, константы и переменные в другой файл.

- Имя модуля = имя файла **без** `.py`. Файл `prices.py` → модуль `prices`.
- Поэтому имена файлов должны быть допустимыми именами Python: `snake_case`, без дефисов (`my-utils.py` не импортируется!) и не начинаться с цифры.

{{< box "analogy" "Аналогия: кухня с ящиками" >}}
Один огромный файл = один ящик со всеми инструментами: ножи, ложки, щипцы, ножницы. Чтобы что-то найти, перебираешь всё. **Модули = отдельные ящики** с подписями: «столовые приборы», «инструменты для выпечки», «специи». `import` = «принеси мне кое-что из этого ящика».
{{< /box >}}

### Три формы импорта

| Форма | Как потом использовать | Когда |
|---|---|---|
| `import prices` | `prices.add_vat(100)` | По умолчанию. Ясно, откуда берётся каждое имя. |
| `from prices import add_vat` | `add_vat(100)` | Когда вы часто используете несколько конкретных имён. |
| `import prices as p` / `from prices import add_vat as vat` | `p.add_vat(100)` / `vat(100)` | Длинное имя или конфликт имён. Известные соглашения: `import numpy as np`. |

### Что происходит в момент `import`?

1. Python **ищет** файл: сначала в папке запущенного скрипта, затем в библиотеках Python.
2. Python **выполняет весь файл сверху вниз** один раз: создаёт функции и константы и **выполняет любой код на верхнем уровне**.
3. Результат сохраняется. Повторный `import` того же модуля **не выполняет его снова**.

Пункт 2 очень важен, и его последствия мы увидим в типичной ошибке.

### `__pycache__`

После первого импорта появится папка с именем `__pycache__` с файлами `.pyc`. Это **байт-код** (Bytecode, вспомните тему о компьютере и операционной системе!) модулей, сохранённый, чтобы следующий импорт был быстрее. Не трогайте её, а в проектах Git игнорируйте (увидим её в `.gitignore` в теме Git).

## Синтаксис

```python
# file: prices.py   (the module)
VAT_RATE = 0.17

def add_vat(amount):
    return amount * (1 + VAT_RATE)
```

```python
# file: main.py     (uses the module; same folder)
import prices
prices.add_vat(100)

from prices import add_vat, VAT_RATE
add_vat(100)

import prices as p
p.add_vat(100)

from prices import add_vat as with_vat
with_vat(100)
```

**Принятый порядок в начале файла:** сначала импорты из стандартной библиотеки (`os`, `json`), пустая строка, а затем импорты из ваших собственных модулей.

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

Создайте папку `week-2/shop/` и в ней два файла.

`week-2/shop/prices.py`:

```python
VAT_RATE = 0.17

def add_vat(amount):
    return round(amount * (1 + VAT_RATE), 2)

def apply_discount(amount, percent):
    return round(amount - amount * percent / 100, 2)
```

`week-2/shop/main.py`:

```python
import prices

base_price = 200
discounted = prices.apply_discount(base_price, 10)
final_price = prices.add_vat(discounted)

print("Base:", base_price)
print("After 10% discount:", discounted)
print("With VAT (" + str(prices.VAT_RATE) + "):", final_price)
```

**Запуск** (из `week-2/shop`): `python main.py`

**Вывод:**

```text
Base: 200
After 10% discount: 180.0
With VAT (0.17): 210.6
```

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

| Строка | Объяснение |
|---|---|
| `import prices` | Python ищет `prices.py` **в той же папке**, что и `main.py`, выполняет его и создаёт имя `prices`, которое представляет модуль. |
| `prices.apply_discount(...)` | К функции обращаются **через** имя модуля, через точку. Точно как `os.getcwd()`. |
| `prices.VAT_RATE` | К константам обращаются так же. |
| `main.py` не определяет ни одной функции цен | Вся логика цен — в **одном месте**. Если НДС изменится, вы меняете только `prices.py`. |

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

**Три формы импорта и второй модуль.** Добавьте `week-2/shop/validation.py`:

```python
MIN_NAME_LENGTH = 2

def is_valid_price(price):
    return isinstance(price, (int, float)) and price >= 0

def is_valid_name(name):
    return len(name.strip()) >= MIN_NAME_LENGTH
```

И обновите `main.py`:

```python
import prices as p
from validation import is_valid_price, is_valid_name

products = [("Laptop", 3500), ("X", 20), ("Mouse", -5), ("Desk", 900)]

for name, price in products:
    if not is_valid_name(name):
        print("Skipping invalid name:", repr(name))
    elif not is_valid_price(price):
        print("Skipping invalid price for", name, ":", price)
    else:
        print(name, "->", p.add_vat(price), "NIS incl. VAT")
```

**Вывод:**

```text
Laptop -> 4095.0 NIS incl. VAT
Skipping invalid name: 'X'
Skipping invalid price for Mouse : -5
Desk -> 1053.0 NIS incl. VAT
```

**Разбор:**

- `import prices as p` — короткий псевдоним. Обращаются через `p.add_vat`.
- `from validation import is_valid_price, is_valid_name` — импортирует **только** две функции и использует их напрямую, без префикса.
- `repr(name)` — показывает значение в кавычках (`'X'`), как `%r` в логах.
- **Структура проекта:** `prices.py` = логика цен. `validation.py` = правила допустимости. `main.py` = соединяет и запускает. У каждого файла одна ответственность.

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

**Ошибка 1: «тестовый» код на верхнем уровне модуля.** Допустим, внизу `prices.py` вы добавили проверку:

```python
print("Testing prices module:", add_vat(100))
```

Теперь **каждый** `import prices` из любого файла будет печатать эту строку, потому что импорт **выполняет файл**. В следующем уроке изучим решение: `if __name__ == "__main__":`.

**Ошибка 2: имя файла, которое совпадает со стандартным модулем.**

Вы создали файл с именем `random.py` (или `logging.py`, `json.py`), а в другом файле написали `import random`:

```text
AttributeError: module 'random' has no attribute 'randint'
```

Python находит **ваш** `random.py` **первым**, в той же папке, и импортирует его вместо настоящего модуля. Никогда не называйте файлы именами стандартных модулей.

**Ошибка 3: `ModuleNotFoundError`.**

```text
ModuleNotFoundError: No module named 'prices'
```

Частые причины: файл не в той же папке, что запущенный скрипт, опечатка в имени или имя файла с дефисом (`my-prices.py`).

**Ошибка 4: `from module import *`.** Импортирует **все** имена из модуля. Непонятно, откуда взялось каждое имя, и имена могут молча перезаписать друг друга. Импортируйте явно то, что нужно.

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

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

### Упражнение 1: Первый модуль

Создайте файл `prices.py` с константой `VAT_RATE = 0.17` и двумя функциями: `add_vat(amount)` и `apply_discount(amount, percent)` (обе округляют до двух знаков). В файле `main.py` импортируйте модуль, вычислите цену 200 после скидки 10%, а затем добавьте к ней НДС.

**Ожидаемый вывод (запуск main.py):**

```text
After discount: 180.0
With VAT: 210.6
```

{{< solution >}}
**prices.py**

```py
VAT_RATE = 0.17


def add_vat(amount):
    return round(amount * (1 + VAT_RATE), 2)


def apply_discount(amount, percent):
    return round(amount - amount * percent / 100, 2)
```

**main.py**

```py
import prices

discounted = prices.apply_discount(200, 10)
final_price = prices.add_vat(discounted)
print("After discount:", discounted)
print("With VAT:", final_price)
```

`import prices` загружает файл `prices.py` из той же папки. К его содержимому обращаются через точку: `prices.add_vat(...)`.
{{< /solution >}}

### Упражнение 2: Три формы импорта

Напишите одну и ту же строку — вычисление НДС от 100 — в трёх формах импорта: `import`, `from ... import` и `import ... as`. Когда стоит использовать каждую?

{{< solution >}}
```py
import prices
print(prices.add_vat(100))

from prices import add_vat
print(add_vat(100))

import prices as p
print(p.add_vat(100))
```

- **`import prices`** — всегда ясно, откуда берётся каждое имя (`prices.add_vat`). Подходит, когда вы используете из модуля много всего.
- **`from prices import add_vat`** — пишете меньше. Подходит для нескольких конкретных имён, которые вы часто используете.
- **`import prices as p`** — короткое имя для модуля с длинным именем.
{{< /solution >}}

### Упражнение 3: Стандартная библиотека

Не создавая собственного модуля, воспользуйтесь встроенными модулями Python:

1. `math` — квадратный корень из 81 и `math.pi`, округлённое до двух знаков.
2. `random` — с `random.seed(1)` выберите случайное число от 1 до 6.

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

```text
9.0
3.14
2
```

{{< solution >}}
```python
import math
import random

print(math.sqrt(81))
print(round(math.pi, 2))

random.seed(1)
print(random.randint(1, 6))
```

```text
9.0
3.14
2
```

`math` и `random` входят в **стандартную библиотеку** (Standard Library) — они поставляются вместе с Python, и импортируют их точно так же, как модуль, написанный нами. `random.seed(1)` фиксирует случайный выбор, чтобы результат был одинаковым при каждом запуске.
{{< /solution >}}

### Упражнение 4: Код, который выполняется при импорте

В файле `helpers.py`:

```text
def shout(text):
    return text.upper() + "!"

print("helpers loaded")
```

В файле `main.py`:

```text
from helpers import shout
print(shout("hi"))
```

1. Что будет напечатано при запуске `python main.py` и почему?
2. Как предотвратить первую строку?

{{< solution >}}
1. Будет напечатано:

```text
helpers loaded
HI!
```

**`import` выполняет весь файл** — включая `print` на верхнем уровне. Даже `from helpers import shout` выполняет весь `helpers.py`, а не только функцию.

2. В модуле, предназначенном для импорта, не должно быть кода на верхнем уровне — только определения. Удалите `print`. (В следующем уроке изучим `if __name__ == "__main__":`, который позволяет оставить тестовый код, выполняющийся только при прямом запуске файла.)
{{< /solution >}}

### Упражнение 5: Модуль температуры

Создайте модуль `temperature.py` с `c_to_f(celsius)` и с `describe(celsius)`, которая возвращает `"hot"` от 30 градусов, `"cold"` ниже 15 и `"pleasant"` в промежутке. В `main.py` напечатайте для каждого города температуру в обеих единицах и описание.

```py
cities = ["Eilat", "Haifa", "Jerusalem"]
temperatures = [35, 22, 9]
```

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

```text
Eilat: 35 C / 95.0 F - hot
Haifa: 22 C / 71.6 F - pleasant
Jerusalem: 9 C / 48.2 F - cold
```

{{< solution >}}
**temperature.py**

```py
HOT_FROM = 30
COLD_BELOW = 15


def c_to_f(celsius):
    return round(celsius * 9 / 5 + 32, 1)


def describe(celsius):
    if celsius >= HOT_FROM:
        return "hot"
    if celsius < COLD_BELOW:
        return "cold"
    return "pleasant"
```

**main.py**

```py
from temperature import c_to_f, describe

cities = ["Eilat", "Haifa", "Jerusalem"]
temperatures = [35, 22, 9]

for i in range(len(cities)):
    celsius = temperatures[i]
    print(cities[i] + ":", celsius, "C /", c_to_f(celsius), "F -", describe(celsius))
```

Логика температуры — в модуле, а `main.py` только использует её и печатает.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **Модуль = файл `.py`.** Имя модуля = имя файла без `.py` (`snake_case`, без дефисов).
- `import m` → `m.func()`. `from m import func` → `func()`. `import m as x` → `x.func()`.
- Python ищет модули сначала **в папке скрипта**. Не называйте файлы `random.py`, `json.py` и т. п.
- Импорт **выполняет модуль** один раз. Код на верхнем уровне выполняется даже при импорте.
- Деление на файлы = разделение ответственности на уровне проекта. Никаких `import *`.

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

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

- [Python Tutorial — Modules](https://docs.python.org/3/tutorial/modules.html)
- [More on Modules](https://docs.python.org/3/tutorial/modules.html#more-on-modules)

<!-- docs:end -->
