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

Цели урока
  • Открывать файл в подходящем режиме — r, w или a — и понимать, что каждый режим делает с существующим содержимым
  • Использовать with, чтобы файл всегда закрывался, даже при ошибке
  • Читать весь файл, строку за строкой, а также записывать и дописывать в него
  • Строить путь к файлу относительно расположения скрипта с помощью pathlib и обрабатывать FileNotFoundError

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

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

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

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

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

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

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

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

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

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

open("data.txt", "w") очищает файл в момент открытия, ещё до того, как вы записали хоть один символ. Если вы хотели дописать, прежняя информация уже удалена. Чтобы дописывать, всегда используйте "a".

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

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

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

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

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

Аналогия: книга в библиотеке

open = взять книгу с полки. close = поставить её обратно. Без with нужно не забыть вернуть её, а если вас посередине куда-то позовут (ошибка), книга останется на столе. with = библиотекарь, который ставит книгу на место, как только вы встаёте из-за стола, — неважно, почему вы встали.

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

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

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

Запись

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

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

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

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 указывает на один и тот же файл, откуда бы вы ни запускали.

Синтаксис

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:

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))

Вывод:

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:

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()

Три запуска:

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

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

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

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

Rina,64

week-2/files/grades_report.py:

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()

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

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

report.txt:

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 из-за относительного пути.

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

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

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

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

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

file.write(42)
TypeError: write() argument must be str, not int

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

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

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

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

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

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

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

Практика

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

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

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

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

Day 1: started
Day 2: networks
Показать решение
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="")
Day 1: started
Day 2: networks
  • with закрывает файл автоматически, даже если возникла ошибка.
  • write сам не добавляет перевод строки — нужен \n.
  • end="" — потому что содержимое уже заканчивается на \n.

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

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", "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="")
second
third
  • "w" удаляет файл и начинает заново — поэтому first удалилось, когда файл второй раз открыли с "w".
  • "a" дописывает в конец, поэтому third добавилось после second.

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

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

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

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

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

dana 90
avi 75
noa 88
Показать решение
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))
dana 90
avi 75
noa 88
  • for line in file — проходит по файлу строка за строкой.
  • strip() — убирает \n и пробелы по краям.
  • split(",") — разделяет строку, а распаковка кладёт её в две переменные.

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

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

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

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

line 2 skipped: avi,abc
Average: 85.0
Показать решение
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))
line 2 skipped: avi,abc
Average: 85.0

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

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

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

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

[]
['buy milk', 'call mom']
Показать решение
  1. Будет выброшен FileNotFoundError.
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"))
[]
['buy milk', 'call mom']

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

Итоги

  • 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.

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