Вспомните: как только программа завершается, все переменные исчезают без следа. Для маленьких учебных примеров это не страшно, но для настоящей программы никуда не годится: список дел должен остаться на завтра, а настройки приложения — сохраниться после перезапуска.
Решение — хранить данные не в оперативной памяти, а на диске, в обычном файле: он переживает завершение программы. В этом уроке вы научитесь открывать файлы, читать и записывать в них текст, работать с путями через pathlib и — самое важное — пользоваться конструкцией with, которая защищает от одной из самых частых ошибок при работе с любым внешним ресурсом.
Открываем файл: функция open()
Представьте, что файл на диске — это папка в шкафу с документами: прежде чем что-то в ней прочитать или дописать, папку сначала нужно достать и открыть. С файлом в Python то же самое — сначала его нужно открыть, и Python выдаст вам "ручку", через которую вы дальше с ним работаете.
За открытие отвечает готовая функция open(). Она принимает путь к файлу и возвращает файловый объект. У неё три параметра, с которыми вы будете работать постоянно: file — путь, mode — режим (что вы собираетесь делать), encoding — кодировка текста.
f = open("notes.txt", "r", encoding="utf-8")
open("notes.txt", "r", encoding="utf-8")— открывает файлnotes.txtиз той же папки, что и программа."r"— режим "только чтение" (о режимах дальше).encoding="utf-8"задаёт кодировку текста.f = ...— результат, файловый объект, сохраняется в переменнуюf. Дальше вы обращаетесь к файлу через неё.
Всегда указывайте encoding="utf-8". Без этого параметра Python берёт кодировку операционной системы — на Windows это часто не utf-8. Одна и та же программа может тогда читать один и тот же файл по-разному на разных компьютерах.
Режимы открытия файла
Открыть файл можно с разными намерениями: только прочитать и не трогать, стереть старое и записать заново, или дописать пару строк в конец. Чтобы Python понял, чего вы хотите, вторым аргументом open() передаётся строка-режим — официально она называется режимом доступа (mode).
| Режим | Что делает | Если файла нет |
|---|---|---|
| "r" | Читать. Режим по умолчанию, если mode не указать | FileNotFoundError |
| "w" | Писать. Стирает всё старое содержимое | Создаёт новый файл |
| "a" | Добавлять в конец (append), старое не трогает | Создаёт новый файл |
| "x" | Создать новый файл, если уже есть — ошибка | Создаёт новый файл |
Главное про "w": он очищает файл в момент открытия, ещё до того, как вы что-то запишете. Чтобы добавить данные, не стирая старые — используйте "a".
Читаем содержимое файла
Допустим, на диске уже лежит файл notes.txt:
У файлового объекта есть три готовых метода для чтения — они отличаются тем, сколько текста возвращают за один вызов.
read() читает файл целиком:
f = open("notes.txt", "r", encoding="utf-8")
content = f.read()
print(content)
f.close()
f.read()— возвращает одну строку со всем текстом файла, включая переносы строк внутри неё.f.close()— закрывает файл. Почему это важно и как не забывать об этом — чуть ниже.
readline() читает по одной строке за вызов, каждый раз следующую:
f = open("notes.txt", "r", encoding="utf-8")
first_line = f.readline() # "Купить молоко\n"
second_line = f.readline() # "Позвонить маме\n"
f.close()
- Символ
\nна конце — это перенос строки, который реально есть в файле. Если вызватьreadline()после последней строки — вернётся пустая строка"", это знак, что файл закончился.
readlines() сразу возвращает список строк:
f = open("notes.txt", "r", encoding="utf-8")
lines = f.readlines()
f.close()
print(lines)
# ["Купить молоко\n", "Позвонить маме\n", "Сделать домашку"]
- Каждый элемент списка — одна строка файла. У всех, кроме последней, на конце есть
\n— чтобы его убрать, к элементу применяют уже знакомыйstrip().
Записываем в файл
Для записи файл открывают в режиме "w" (перезаписать) или "a" (дописать в конец). Метод для записи называется write() — он принимает строку и дописывает её в файл.
f = open("output.txt", "w", encoding="utf-8")
f.write("Привет, файл!\n")
f.write("Вторая строка\n")
f.close()
open(..., "w", ...)— если файла не было, он создастся; если был — старое содержимое исчезнет.f.write("Привет, файл!\n")— записывает строку. В отличие отprint(),write()не добавляет перенос строки сам — символ\nнужно писать вручную.- Второй
f.write(...)дописывает вторую строку сразу за первой.
Если строки уже лежат в списке — их можно записать одним вызовом через writelines():
lines = ["Первая строка\n", "Вторая строка\n"]
f = open("output.txt", "w", encoding="utf-8")
f.writelines(lines)
f.close()
f.writelines(lines)— записывает элементы списка подряд, без пропусков между ними. Переносы строк, как и вwrite(), нужно закладывать заранее самому.
close() и почему вместо него используют with
Открытый файл занимает системный ресурс — что-то вроде выданного библиотекой абонемента. Пока файл не закрыт, записанные данные могут остаться в промежуточном буфере и не попасть на диск. Проблема в том, что close() нужно вызвать после всей работы с файлом — а если между open() и close() случится ошибка, строка с close() просто не выполнится:
f = open("data.txt", "w", encoding="utf-8")
f.write(str(1 / 0)) # ZeroDivisionError!
f.close() # эта строка никогда не выполнится
f.write(str(1 / 0))— деление на ноль ломает программу прямо здесь.f.close()— до него выполнение не дойдёт. Файл останется открытым.
Для этого в Python есть конструкция with — официально менеджер контекста (context manager): открытие и закрытие ресурса Python берёт на себя и гарантирует закрытие в любом случае, даже при ошибке внутри блока.
with open("notes.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
# здесь блок with уже закончился — файл закрыт автоматически
with open(...) as f:— открывает файл так же, какopen(), но результат сохраняется черезas, а не через=. Двоеточие означает, что дальше — блок с отступом, как уifилиfor.- Как только отступ заканчивается — блок завершён, и Python сам закрывает файл. Отдельно вызывать
f.close()уже не нужно.
Как это устроено. Объекты, с которыми работает with, умеют выполнять два действия — одно при входе в блок, второе при выходе, каким бы он ни был. У файла эти действия уже встроены. Дальше в уроке вы напишете такой же "вход и выход" сами, одной функцией, без класса.
Используйте with open() всегда, когда работаете с файлами.
for по файлу: читаем построчно
Файловый объект можно перебирать циклом for точно так же, как строки и списки — только на каждом шаге вы получаете очередную строку файла.
with open("notes.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip())
for line in f:— на каждом шагеlineполучает следующую строку файла, включая\nна конце.line.strip()— убирает лишние пробельные символы по краям, включая\n, чтобы вывод не дублировал переносы.
readlines() сразу загружает весь файл в список в памяти — для файла на несколько гигабайт это может туда не поместиться. for line in f читает по одной строке за раз, поэтому в памяти всегда лежит только текущая строка.
pathlib: удобная работа с путями
Строка "notes.txt" в open() — это путь к файлу. Как только нужно проверить, существует ли файл, или собрать путь из папки и имени — работать с путями как с обычным текстом неудобно: на Windows и на Mac/Linux разные разделители между папками. Для этого в стандартной библиотеке есть модуль pathlib, а в нём класс Path, представляющий путь как отдельный объект.
from pathlib import Path
p = Path("notes.txt")
print(p.exists())
content = p.read_text(encoding="utf-8")
p.write_text("Новое содержимое", encoding="utf-8")
p = Path("notes.txt")— создаёт объект пути. Файл при этом ещё не открывается, мы просто описали, где он должен быть.p.exists()— возвращаетTrue, если по этому пути что-то есть на диске, иFalse, если нет. Удобно проверить перед чтением.p.read_text(encoding="utf-8")— открывает файл, читает целиком и сам закрывает — то же самое, чтоwith open(p, "r", encoding="utf-8") as f: f.read(), только в одну строку.p.write_text(...)— так же, но для записи: открывает, пишет, закрывает.
Path ещё умеет собирать путь из частей через / и разбирать его: p.name — имя файла с расширением, p.stem — без расширения, p.suffix — только расширение, p.parent — путь до папки. Объект Path можно передать прямо в обычный open() вместо строки.
Обрабатываем ошибки при работе с файлами
С файлами связано многое, что программа не контролирует: файла может не быть по пути, у пользователя может не хватать прав. Открывать файл "вслепую" — плохая идея. Два исключения встречаются чаще всего:
Обе ситуации ловятся уже знакомой конструкцией try/except:
try:
with open("notes.txt", "r", encoding="utf-8") as f:
content = f.read()
print(content)
except FileNotFoundError:
print("Файл не найден. Проверьте путь.")
except PermissionError:
print("Нет прав на чтение этого файла.")
with open(...) as f:— если файла не существует, ошибка выбросится прямо здесь, и код внутриwithне выполнится.except FileNotFoundError:иexcept PermissionError:— отдельные ветки под каждую ошибку, со своим понятным сообщением вместо падения программы.
OSError — общий класс для всех ошибок файловой системы, а FileNotFoundError и PermissionError — его частные случаи. Чтобы не перечислять каждую ошибку отдельно, можно поймать сразу except OSError:.
Свой контекстный менеджер: @contextmanager
До сих пор вы использовали with только с готовым open(). Но того же удобства иногда хочется и для своей задачи — например, замерить время выполнения куска кода. В стандартной библиотеке для этого есть декоратор contextmanager из модуля contextlib: он превращает функцию с yield (вы уже видели такую конструкцию в генераторах) в свой собственный менеджер контекста.
Правило одно:
- Код до
yieldвыполняется при входе в блокwith. - Код после
yieldвыполняется при выходе из блокаwith.
import time
from contextlib import contextmanager
@contextmanager
def timer():
start = time.time()
yield
elapsed = time.time() - start
print(f"Выполнено за {elapsed:.4f} сек")
with timer():
total = sum(range(1_000_000))
print(total)
@contextmanagerнадdef timer():— помечает функцию как заготовку менеджера контекста. Без этой строки функция сyieldвwithне сработает.start = time.time()— выполняется при входе в блок: запоминаем текущее время.yield— момент, где управление передаётся телу блокаwith. Всё внутриwith timer():выполняется как будто "на месте" этой строки.elapsed = ...иprint(...)— выполняются уже после того, как тело блока закончилось: считаем прошедшее время и печатаем его с четырьмя знаками после запятой.with timer():— используем менеджер точно так же, как раньшеwith open(...). Словоasне нужно, потому чтоyieldздесь без значения.
Если внутри блока может случиться ошибка, а код после yield обязан выполниться в любом случае — yield оборачивают в знакомый try/finally:
from contextlib import contextmanager
@contextmanager
def managed_resource(name):
print(f"Открываем: {name}")
try:
yield name
finally:
print(f"Закрываем: {name}")
with managed_resource("база данных") as res:
print(f"Работаем с {res}")
try: yield name— теперьyieldпередаёт значениеnameдальше, оно попадёт в переменную послеas.finally: print(...)— выполнится всегда, даже если внутри блокаwithслучится исключение. Так гарантируется освобождение ресурса.
Частые ошибки
Открыли на запись "w" вместо "a" и потеряли данные
open("log.txt", "w", ...) вместо "a" стирает всё, что было в файле, ещё до первого write(). Для дозаписи в конец — только "a".
Забыли \n в write()
Несколько подряд f.write("текст") без \n на конце сольются в одну строку в файле. В отличие от print(), write() перенос строки сам не ставит.
Забыли close() без with
Без close() данные могут не сохраниться на диск, а файл останется занятым. Решение простое: используйте with open(...) as f: вместо ручного открытия и закрытия.
Функция с @contextmanager без yield
Функция, помеченная @contextmanager, обязана содержать ровно один yield — именно он отмечает границу между входом и выходом. Без него использовать функцию в with не получится.
Что важно запомнить
open(file, mode, encoding)открывает файл. Всегда указывайтеencoding="utf-8".- Режим "r" — чтение, "w" — запись с очисткой файла, "a" — добавление в конец, "x" — создать новый (ошибка, если уже есть).
- read() — весь файл одной строкой, readline() — одна строка за вызов, readlines() — список всех строк.
- write() записывает строку, writelines() — список строк. Символ
\nв обоих случаях добавляется вручную. - with open(...) as f: гарантированно закрывает файл при выходе из блока, даже при ошибке — используйте вместо ручного
close(). - for line in f: — самый эффективный способ читать файл строка за строкой, без загрузки всего файла в память.
- pathlib.Path представляет путь как объект:
exists(),read_text(),write_text(), деление/для сборки пути,name/stem/suffix/parentдля разбора. - FileNotFoundError и PermissionError — частые исключения при работе с файлами, оба ловятся через
try/except. - @contextmanager из
contextlibпревращает функцию сyieldв собственный менеджер контекста: код доyield— вход в блокwith, код после — выход из него.
Проверьте себя
7 вопросов
Контекстный менеджер через @contextmanager
PremiumЗапись и чтение файла
Напишите две функции:
- save_to_file(filename, content) — записывает строку content в файл (режим "w", кодировка utf-8), затем печатает
Сохранено! - read_from_file(filename) — читает файл (режим "r", кодировка utf-8) и печатает его содержимое
В обеих функциях используйте with open().