Файлы и with
Теория
Backend постоянно работает с файлами: выгрузки заказов для бухгалтерии, отчёты, логи, импорт товаров от поставщика. Файл открывают функцией open(путь, режим, encoding=...), получают файловый объект и через него читают или пишут. Режим определяет, что можно делать: "r" — читать, "w" — писать с нуля (старое содержимое стирается), "a" — дописывать в конец. Кодировку указывают всегда — encoding="utf-8": иначе Python возьмёт кодировку операционной системы, и на Windows русский текст превратится в «кракозябры».
Открытый файл нужно закрыть: пока он открыт, данные могут не дописаться на диск, а система ограничивает число открытых файлов. Надёжный способ — конструкция with open(...) as f:. Она закрывает файл автоматически, когда блок заканчивается, — даже если внутри произошла ошибка. Объекты, которые умеют так «прибирать за собой» в with, называют контекстными менеджерами; как написать свой, — в разделе ООП.
Наглядно
Таблица 1. Режимы открытия файла.
| Режим | Что делает | Если файла нет | Если файл есть |
|---|---|---|---|
"r" |
чтение (по умолчанию) | FileNotFoundError |
читаем с начала |
"w" |
запись с нуля | создаётся | содержимое стирается |
"a" |
дозапись в конец | создаётся | пишем после существующего |
"x" |
только создание | создаётся | FileExistsError |
"rb", "wb" |
то же для байтов (картинки, архивы) | без encoding |
Таблица 2. Способы читать текстовый файл.
| Запись | Результат | Когда |
|---|---|---|
f.read() |
весь текст одной строкой | файл маленький |
for line in f: |
строки по одной, с \n на конце |
файл любого размера — построчно |
f.readlines() |
список всех строк с \n |
редко: загружает всё в память |
f.write(text) |
записывает строку, \n сам не добавляет |
На практике
Задача: выгрузить номера заказов в отчёт, вести журнал событий дозаписью и прочитать список артикулов от поставщика.
Шаг 1. Записываем файл
with открывает файл и закроет его после блока. write не добавляет перевод строки — его пишем сами:
order_ids = [1042, 1043, 1044] with open("report.txt", "w", encoding="utf-8") as f: f.write("Заказы за день\n") for order_id in order_ids: f.write(f"{order_id}\n") print(f.closed)
Результат:
True
После выхода из блока файл уже закрыт.
Шаг 2. Читаем целиком
Маленький файл можно прочитать одной строкой:
with open("report.txt", encoding="utf-8") as f: text = f.read() print(text)
Результат:
Заказы за день 1042 1043 1044
Шаг 3. Читаем построчно
Файловый объект — итератор строк: большой файл читается по одной строке, не целиком. У каждой строки на конце \n:
with open("report.txt", encoding="utf-8") as f: header = next(f) ids = [int(line) for line in f] print(repr(header), ids)
Результат:
'Заказы за день\n' [1042, 1043, 1044]
int() сам отбросил пробельные символы по краям, а в строке заголовка \n остался. Для текста строки обычно чистят: line.rstrip("\n").
Шаг 4. Дописываем, а не перезаписываем
Журнал событий растёт: режим "a" добавляет строки в конец. Для сравнения — что делает "w":
with open("events.log", "a", encoding="utf-8") as f: f.write("1042 оплачен\n") with open("events.log", "a", encoding="utf-8") as f: f.write("1043 отменён\n") with open("events.log", encoding="utf-8") as f: print(f.read(), end="") with open("events.log", "w", encoding="utf-8") as f: f.write("1044 отправлен\n") with open("events.log", encoding="utf-8") as f: print(f.read(), end="")
Результат:
1042 оплачен 1043 отменён 1044 отправлен
После "w" в журнале осталась одна строка: две прежние стёрты.
Шаг 5. Убеждаемся, что with закрывает файл при ошибке
Ошибка внутри блока не мешает закрыть файл:
try: with open("broken.txt", "w", encoding="utf-8") as f: f.write("начало выгрузки\n") raise ValueError("сбой в середине выгрузки") except ValueError as e: print("ошибка:", e) print("файл закрыт:", f.closed)
Результат:
ошибка: сбой в середине выгрузки файл закрыт: True
Шаг 6. Обрабатываем отсутствующий файл
Файла от поставщика может не оказаться — это ожидаемая ситуация, а не повод падать:
def read_skus(path): try: with open(path, encoding="utf-8") as f: return [line.rstrip("\n") for line in f] except FileNotFoundError: return [] with open("supplier.txt", "w", encoding="utf-8") as f: f.write("KB-0142\nMS-0007\n") print(read_skus("supplier.txt"), read_skus("missing.txt"))
Результат:
['KB-0142', 'MS-0007'] []
Частые ошибки
"w"вместо"a". Режим"w"стирает файл при открытии — журнал или накопленная выгрузка пропадают.- Забытый
\n.writeне переводит строку: без"\n"все записи слипаются в одну строку. \nв прочитанных строках.for line in fотдаёт строки с переводом строки:"KB-0142\n" != "KB-0142". Чиститеrstrip("\n").- Без
encoding. Кодировка по умолчанию зависит от системы; файл, записанный на Linux, на Windows прочитается с «кракозябрами». Всегдаencoding="utf-8". openбезwith. Если доclose()случится ошибка, файл останется открытым, а записанное — не сброшенным на диск.
Итог
Открытие
open(путь, режим, encoding="utf-8"); всегда черезwith— файл закроется сам, даже при ошибке.- Режимы:
"r"— читать,"w"— писать с нуля (стирает!),"a"— дописывать,"x"— только создать;"b"— байты.
Чтение и запись
f.read()— всё сразу;for line in f— построчно, строки с\n.f.write(text)— без автоматического\n.
Ошибки
- Нет файла при чтении —
FileNotFoundError; если это ожидаемо, его ловят.
Вопросы на собеседовании
Зачем открывать файлы через with?
with гарантирует закрытие файла при выходе из блока — и при нормальном завершении, и при исключении. Без этого файл может остаться открытым, а данные из буфера не записаться на диск. open() возвращает контекстный менеджер, у которого есть методы входа и выхода.
Как прочитать файл размером 10 ГБ?
Построчно: for line in f: — файловый объект сам является итератором строк и держит в памяти одну строку. f.read() и f.readlines() загрузят весь файл в память.
Чем режим "w" отличается от "a"?
"w" при открытии обрезает файл до нуля и пишет с начала, "a" открывает файл на дозапись в конец. Оба создают файл, если его нет.
Глубже
Что происходит в with по шагам и как сделать свой контекстный менеджер, — в разделе ООП, тема «Свои контекстные менеджеры».