# Работа с текстовыми файлами: open, with, чтение и запись<no value>

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

- **Открытие файла** с помощью `open` и **режимы открытия**.
- Почему файл **нужно закрывать** и как **`with`** делает это автоматически.
- **Чтение**: весь файл, строку за строкой.
- **Запись** и **дописывание**.
- **Надёжный** путь, который не зависит от того, откуда вы запускаете (мы обещали это в теме о компьютере, операционной системе и сети!).

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

Тема о компьютере, операционной системе и сети: «всё, что в RAM, исчезает, когда программа завершается». Файлы — основной способ сохранить информацию на **накопителе**: настройки, отчёты, историю, данные для импорта и экспорта. Счётчик, который всегда возвращался к 1 в `memory_test.py`? Сегодня мы заставим его пережить перезапуски.

А в проекте ООП вы будете сохранять всю информацию системы в файл.

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

### Открытие файла

`open(path, mode)` просит операционную систему (системный вызов!) о доступе к файлу и возвращает **файловый объект**, через который читают и пишут.

### Режимы открытия

| Режим | Название | Если файл существует | Если не существует | Действия |
|---|---|---|---|---|
| `"r"` | Read (по умолчанию) | Открывает для чтения | **`FileNotFoundError`** | Только чтение |
| `"w"` | Write | **Удаляет всё содержимое!** | Создаёт его | Запись |
| `"a"` | Append | Сохраняет содержимое и пишет **в конец** | Создаёт его | Запись в конец |

{{< box "warn" "Режим w удаляет!" >}}
`open("data.txt", "w")` **очищает файл в момент открытия**, ещё до того, как вы записали хоть один символ. Если вы хотели дописать, прежняя информация уже удалена. Чтобы дописывать, всегда используйте `"a"`.
{{< /box >}}

### Зачем закрывать файл?

- Запись в файл сначала собирается в **буфере** в памяти и действительно записывается на диск только при закрытии файла или когда буфер заполнится. Без закрытия часть информации может не сохраниться.
- Операционная система ограничивает, сколько файлов процесс может держать открытыми. Незакрытые файлы «утекают».
- В Windows открытый файл иногда **блокируется**, и другие программы не могут к нему обратиться.

### `with` — автоматическое закрытие

```python
with open("notes.txt", "r") as file:
    content = file.read()
# here the file is already closed - always
```

`with` гарантирует, что файл закроется **в момент выхода из блока**: при обычном завершении, при `return` **и даже при выброшенной ошибке**. Помните `finally` из темы исключений и логирования? `with` делает ровно это, в короткой форме. **Всегда открывайте файлы через `with`.**

{{< box "analogy" "Аналогия: книга в библиотеке" >}}
`open` = взять книгу с полки. `close` = поставить её обратно. Без `with` нужно не забыть вернуть её, а если вас посередине куда-то позовут (ошибка), книга останется на столе. `with` = библиотекарь, который ставит книгу на место, как только вы встаёте из-за стола, — неважно, почему вы встали.
{{< /box >}}

### Способы чтения

| Способ | Возвращает | Когда |
|---|---|---|
| `file.read()` | Одну строку со **всем** файлом | Маленькие файлы |
| `file.readlines()` | **Список** строк (каждая с `\n` в конце) | Когда нужны все строки в виде списка |
| `for line in file:` | Строку за строкой | **Рекомендуемый способ**. Работает даже для огромных файлов, потому что не загружает всё в память |

**`\n`** — символ **перевода строки** (newline). Каждая строка файла заканчивается им. `line.strip()` убирает его (и пробелы по краям).

### Запись

`file.write(text)` записывает **ровно** этот текст, **не** переходя на новую строку автоматически (в отличие от `print`). Нужна новая строка? Добавьте `"\n"` сами. И `write` принимает **только строку**: числа сначала преобразуйте через `str()`.

### Надёжный путь с `pathlib`

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

```python
from pathlib import Path

BASE_DIR = Path(__file__).parent      # the folder this .py file is in
DATA_FILE = BASE_DIR / "data.txt"     # "/" joins path parts, on every OS
```

- `__file__` — встроенная переменная: путь текущего файла.
- `Path(...).parent` — папка, в которой он лежит.
- `/` между `Path` и строкой — правильно соединяет части пути в любой операционной системе (`\` в Windows, `/` на Mac).

Теперь `DATA_FILE` указывает на один и тот же файл, **откуда бы вы ни запускали**.

## Синтаксис

```python
from pathlib import Path

path = Path(__file__).parent / "notes.txt"

# write (creates or overwrites)
with open(path, "w", encoding="utf-8") as file:
    file.write("first line\n")

# append (adds to the end)
with open(path, "a", encoding="utf-8") as file:
    file.write("another line\n")

# read everything
with open(path, "r", encoding="utf-8") as file:
    content = file.read()

# read line by line
with open(path, "r", encoding="utf-8") as file:
    for line in file:
        print(line.strip())

path.exists()     # True / False
```

**`encoding="utf-8"`** — всегда указывайте. Без этого в Windows по умолчанию используется другая кодировка, и неанглийский текст (например, на русском или иврите) или эмодзи запишутся «кракозябрами» или вызовут `UnicodeDecodeError`.

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

`week-2/files/diary.py`:

```python
from pathlib import Path

DIARY_FILE = Path(__file__).parent / "diary.txt"

with open(DIARY_FILE, "w", encoding="utf-8") as file:
    file.write("Day 1: started the course\n")
    file.write("Day 2: learned the terminal\n")

with open(DIARY_FILE, "a", encoding="utf-8") as file:
    file.write("Day 3: networks!\n")

with open(DIARY_FILE, "r", encoding="utf-8") as file:
    content = file.read()

print(content)
print("File size in characters:", len(content))
```

**Вывод:**

```text
Day 1: started the course
Day 2: learned the terminal
Day 3: networks!

File size in characters: 71
```

А в папке создан файл `diary.txt`. Откройте его в VS Code.

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

| Строка | Объяснение |
|---|---|
| `Path(__file__).parent / "diary.txt"` | Полный путь к файлу рядом со скриптом. Работает из любой папки. |
| `open(DIARY_FILE, "w", ...)` | Режим записи: создаёт файл или **очищает** его, если он существует. |
| `file.write("...\n")` | Записывает текст. `\n` создаёт перевод строки. |
| Конец первого блока `with` | Файл **закрывается**, и содержимое записывается на диск. |
| `open(..., "a", ...)` | Режим дописывания: существующее содержимое сохраняется, запись идёт в конец. |
| `file.read()` | Всё содержимое одной строкой. |
| Пустая строка в выводе | Файл заканчивается на `\n`, а `print` добавляет свой `\n`. |

**Попробуйте:** запустите ещё раз. Файл вырос? **Нет**, потому что режим `"w"` в начале скрипта очищает его при каждом запуске. Измените первое открытие на `"a"` и запустите дважды. Что произойдёт?

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

**Счётчик запусков, который действительно сохраняется** (проблема из темы о компьютере, операционной системе и сети!) + **обработка файла данных строка за строкой.**

`week-2/files/run_counter.py`:

```python
from pathlib import Path

COUNTER_FILE = Path(__file__).parent / "counter.txt"

def read_count():
    if not COUNTER_FILE.exists():
        return 0
    with open(COUNTER_FILE, "r", encoding="utf-8") as file:
        return int(file.read().strip())

def save_count(count):
    with open(COUNTER_FILE, "w", encoding="utf-8") as file:
        file.write(str(count))

def main():
    count = read_count() + 1
    save_count(count)
    print("This program has run", count, "times")

if __name__ == "__main__":
    main()
```

**Три запуска:**

```text
This program has run 1 times
This program has run 2 times
This program has run 3 times
```

**Теперь работает**, потому что число сохраняется на **накопителе** между запусками, а не только в RAM.

**Обработка файла оценок.** Создайте `week-2/files/grades.txt`:

```text
Dana,90
Avi,72
Noa,abc
Tom,85

Rina,64
```

`week-2/files/grades_report.py`:

```python
from pathlib import Path

GRADES_FILE = Path(__file__).parent / "grades.txt"
REPORT_FILE = Path(__file__).parent / "report.txt"

def load_grades(path):
    grades = {}
    with open(path, "r", encoding="utf-8") as file:
        for line_number, line in enumerate(file, start=1):
            line = line.strip()
            if line == "":
                continue
            name, grade_text = line.split(",")
            try:
                grades[name] = int(grade_text)
            except ValueError:
                print("Skipping line", line_number, "- bad grade:", repr(grade_text))
    return grades

def write_report(grades, path):
    with open(path, "w", encoding="utf-8") as file:
        for name, grade in grades.items():
            status = "PASS" if grade >= 65 else "FAIL"
            file.write(name + ": " + str(grade) + " " + status + "\n")
        average = sum(grades.values()) / len(grades)
        file.write("Average: " + str(round(average, 1)) + "\n")

def main():
    grades = load_grades(GRADES_FILE)
    write_report(grades, REPORT_FILE)
    print("Report written to", REPORT_FILE.name)

if __name__ == "__main__":
    main()
```

**Вывод на экран:**

```text
Skipping line 3 - bad grade: 'abc'
Report written to report.txt
```

**`report.txt`:**

```text
Dana: 90 PASS
Avi: 72 PASS
Tom: 85 PASS
Rina: 64 FAIL
Average: 77.8
```

**Разбор:**

- `for line_number, line in enumerate(file, start=1)` — проходит по файлу **строка за строкой** с номером строки, чтобы сообщить, где проблема.
- `line.strip()` — убирает `\n`. Без этого `int("90\n")` на самом деле работает, но имя пришло бы с лишними пробелами.
- `if line == "": continue` — пропускать пустые строки.
- `name, grade_text = line.split(",")` — разделение по запятой и распаковка в две переменные.
- `"PASS" if grade >= 65 else "FAIL"` — **условное выражение** (Conditional Expression): короткий `if`/`else` в одну строку, который возвращает одно из двух значений.
- `REPORT_FILE.name` — только имя файла, без полного пути.
- `Path` — не только «путь». У него есть полезные методы, такие как `.exists()` и `.name`.

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

**Ошибка 1: `"w"` вместо `"a"` — и удаление данных.** Мы видели это выше. **Перед** каждым `open(..., "w")` спросите себя: «меня устраивает, что прежнее содержимое удалится?»

**Ошибка 2: `FileNotFoundError` из-за относительного пути.**

```python
with open("grades.txt") as file:
```

Работает, только когда вы запускаете из той же папки. С кнопки Run в VS Code или из другой папки:

```text
FileNotFoundError: [Errno 2] No such file or directory: 'grades.txt'
```

Исправление: `Path(__file__).parent / "grades.txt"`. А для файлов, которых может не быть, проверяйте `.exists()` или ловите `FileNotFoundError`.

**Ошибка 3: записать число через `write`.**

```python
file.write(42)
```

```text
TypeError: write() argument must be str, not int
```

Исправление: `file.write(str(42))`.

**Ошибка 4: забыть `\n`.**

```python
file.write("line one")
file.write("line two")
```

В файле: `line oneline two`, в одной строке. `write` не добавляет перевод строки.

**Ошибка 5: читать дважды из одного открытого файла.**

```python
with open(path, encoding="utf-8") as file:
    first = file.read()
    second = file.read()
print(repr(second))   # ''
```

Файл «помнит», откуда он читает (**позиция чтения**). После `read()` позиция в конце, поэтому второе чтение возвращает пустую строку.

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

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

### Упражнение 1: Запись и чтение

Запишите в файл `diary.txt` две строки: `Day 1: started` и `Day 2: networks`. Затем прочитайте файл и напечатайте его содержимое.

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

```text
Day 1: started
Day 2: networks
```

{{< solution >}}
```python
with open("diary.txt", "w", encoding="utf-8") as file:
    file.write("Day 1: started\n")
    file.write("Day 2: networks\n")

with open("diary.txt", "r", encoding="utf-8") as file:
    print(file.read(), end="")
```

```text
Day 1: started
Day 2: networks
```

- `with` закрывает файл автоматически, даже если возникла ошибка.
- `write` сам не добавляет перевод строки — нужен `\n`.
- `end=""` — потому что содержимое уже заканчивается на `\n`.
{{< /solution >}}

### Упражнение 2: w или a

```python
with open("log.txt", "w", encoding="utf-8") as file:
    file.write("first\n")
with open("log.txt", "w", encoding="utf-8") as file:
    file.write("second\n")
with open("log.txt", "a", encoding="utf-8") as file:
    file.write("third\n")
```

Что будет в файле в конце? Объясните.

{{< solution >}}
```python
with open("log.txt", "w", encoding="utf-8") as file:
    file.write("first\n")
with open("log.txt", "w", encoding="utf-8") as file:
    file.write("second\n")
with open("log.txt", "a", encoding="utf-8") as file:
    file.write("third\n")

with open("log.txt", "r", encoding="utf-8") as file:
    print(file.read(), end="")
```

```text
second
third
```

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

### Упражнение 3: Строка за строкой

В файле `grades.txt` каждая строка — `name,grade`. Напишите код, который читает файл строка за строкой и печатает каждое имя с его оценкой. Пустые строки пропускайте.

Создайте файл так:

```py
with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\n\navi,75\nnoa,88\n")
```

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

```text
dana 90
avi 75
noa 88
```

{{< solution >}}
```python
with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\n\navi,75\nnoa,88\n")

with open("grades.txt", "r", encoding="utf-8") as file:
    for line in file:
        line = line.strip()
        if not line:
            continue
        name, grade_text = line.split(",")
        print(name, int(grade_text))
```

```text
dana 90
avi 75
noa 88
```

- `for line in file` — проходит по файлу строка за строкой.
- `strip()` — убирает `\n` и пробелы по краям.
- `split(",")` — разделяет строку, а распаковка кладёт её в две переменные.
{{< /solution >}}

### Упражнение 4: Испорченный ввод

Тот же файл оценок, но теперь в нём есть испорченная строка. Прочитайте файл: строку, в которой оценка не число, пропустите и напечатайте сообщение с номером строки. В конце напечатайте среднее допустимых оценок.

```py
with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\navi,abc\nnoa,80\n")
```

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

```text
line 2 skipped: avi,abc
Average: 85.0
```

{{< solution >}}
```python
with open("grades.txt", "w", encoding="utf-8") as file:
    file.write("dana,90\navi,abc\nnoa,80\n")

grades = []
with open("grades.txt", "r", encoding="utf-8") as file:
    for number, line in enumerate(file, start=1):
        line = line.strip()
        name, grade_text = line.split(",")
        try:
            grades.append(int(grade_text))
        except ValueError:
            print("line", number, "skipped:", line)

print("Average:", sum(grades) / len(grades))
```

```text
line 2 skipped: avi,abc
Average: 85.0
```

`enumerate(file, start=1)` даёт ещё и номер строки. `try`/`except` из предыдущих уроков позволяет пропустить испорченную строку без падения.
{{< /solution >}}

### Упражнение 5: Файл, которого не существует

1. Что произойдёт, если попытаться открыть для чтения несуществующий файл?
2. Напишите функцию `load_tasks(path)`, которая возвращает строки файла в виде списка, а если файла нет — пустой список. Используйте `Path(path).exists()`.

```python
print(load_tasks("todo.txt"))
with open("todo.txt", "w", encoding="utf-8") as file:
    file.write("buy milk\ncall mom\n")
print(load_tasks("todo.txt"))
```

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

```text
[]
['buy milk', 'call mom']
```

{{< solution >}}
1. Будет выброшен **`FileNotFoundError`**.
2.

```python
from pathlib import Path


def load_tasks(path):
    if not Path(path).exists():
        return []
    tasks = []
    with open(path, "r", encoding="utf-8") as file:
        for line in file:
            tasks.append(line.strip())
    return tasks


print(load_tasks("todo.txt"))
with open("todo.txt", "w", encoding="utf-8") as file:
    file.write("buy milk\ncall mom\n")
print(load_tasks("todo.txt"))
```

```text
[]
['buy milk', 'call mom']
```

Проверяйте **до** открытия, существует ли файл. При первом запуске программы файла ещё нет — и это допустимое состояние, а не ошибка.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- `open(path, mode, encoding="utf-8")`: **`"r"`** чтение (по умолчанию), **`"w"`** запись, **которая удаляет**, **`"a"`** дописывание в конец.
- **Всегда `with`**: файл закрывается автоматически, даже при ошибке.
- Чтение: `read()` (всё), `for line in file` (строка за строкой, рекомендуется). `strip()` убирает `\n`.
- `write` записывает только `str` и без перевода строки. Добавляйте `"\n"` сами.
- Надёжный путь: `Path(__file__).parent / "file.txt"`. Проверяйте существование через `.exists()` или ловите `FileNotFoundError`.

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

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

- [Reading and Writing Files](https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files)
- [Methods of File Objects](https://docs.python.org/3/tutorial/inputoutput.html#methods-of-file-objects)
- [open()](https://docs.python.org/3/builtins/functions.html#open)

<!-- docs:end -->
