Ввод-вывод
Ввод-вывод в Python устроен как стек из трёх слоёв, и почти каждая ловушка темы объясняется тем, на каком слое вы сейчас стоите. Внизу — «сырой» слой RawIOBase; для файла это FileIO, тонкая обёртка над дескриптором операционной системы, умеющая только отдавать и принимать байты. Над ним — буферизующий слой BufferedReader / BufferedWriter / BufferedRandom, который копит данные в памяти, чтобы не делать системный вызов на каждый байт. Сверху — TextIOWrapper, который декодирует байты в str, кодирует обратно и попутно переписывает переводы строк. open('f.txt') собирает всю тройку, open('f.bin', 'rb') останавливается на втором слое, а open('f.bin', 'rb', buffering=0) отдаёт голый FileIO.
Специфика Python здесь — в жёстком разделении str и bytes. Текстовый режим работает только со str и обязательно применяет кодировку; бинарный работает только с bytes и не трогает ни одного байта. Из этого разделения растут остальные правила темы, и они не интуитивны: buffering=0 разрешён только в бинарном режиме, tell() в текстовом возвращает непрозрачный маркер, а не смещение в байтах, seek от ненулевого whence там запрещён, w обрезает файл в момент открытия, а a+ пишет в конец даже сразу после seek(0). Отдельно стоит сериализация — json переносим между языками, но знает шесть типов, а pickle сериализует почти любой объект и при загрузке недоверенных данных выполняет произвольный код.
Карта темы
- Файловый объект и стек io — что такое поток, из каких трёх слоёв его собирает
open()и почему файл — не обязательно файл на диске. - Режимы open() —
r,w,x,aзадают судьбу файла,t/b— тип данных,+— направление; и что каждое сочетание делает с содержимым. - Текстовый и бинарный режимы —
strпротивbytes, где именно применяется кодировка и какnewline=управляет переписыванием\r\n. - Буферизация, flush и close — построчная и блочная буферизация, почему
buffering=0вне бинарного режима незаконен и чемflush()отличается отos.fsync(). - tell() и seek() — байтовые смещения в бинарном режиме и непрозрачные маркеры в текстовом, из-за которых арифметика над позицией запрещена.
- StringIO и BytesIO — потоки в памяти с тем же файловым API — для тестов, захвата вывода и сборки данных без диска.
- json и pickle — какие типы переживают каждый формат и почему
pickle.loadна недоверенном вводе — это выполнение чужого кода.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Не закрывать файл и полагаться на сборщик мусора | close() сначала делает flush(), поэтому без него теряется хвост записи, а не только дескриптор; момент сборки мусора непредсказуем |
Считать flush() гарантией сохранности при отключении питания | flush() отдаёт данные ядру, а не носителю; на диск их выталкивает os.fsync(f.fileno()) |
Открыть существующий файл в режиме w, чтобы «дописать» | w обрезает файл до нуля в момент открытия, ещё до первой записи |
Ждать, что seek(0) в режиме a+ заставит писать с начала | Флаг O_APPEND переводит позицию в конец перед каждой записью — seek влияет только на чтение |
Считать tell() в текстовом режиме смещением в байтах | Это непрозрачный маркер состояния декодера; арифметика над ним и seek от whence=1/2 запрещены |
| Открыть изображение или архив в текстовом режиме | Декодирование либо падает с UnicodeDecodeError, либо тихо портит данные, а запись ещё и перепишет \n в \r\n на Windows |
Загрузить pickle из сети или от пользователя | Опкод REDUCE вызывает произвольный объект — это удалённое выполнение кода, а не «ошибка разбора» |
Прочитать StringIO сразу после записи и получить пустую строку | Позиция стоит в конце — нужен seek(0) либо getvalue() |
Значение для собеседований
Тему спрашивают у всех — от junior до senior, — но глубина проверки резко разная. На junior-уровне хотят услышать буквы режимов и разницу текста и байтов: w обрезает, x падает на существующем файле, a дописывает, b даёт bytes, а текстовый режим даёт str и применяет кодировку. Дальше почти всегда идёт «зачем закрывать файл» — и правильный ответ не «чтобы не течь дескрипторами», а «потому что close() сбрасывает буфер». Кто назвал только дескрипторы, показал, что не держит в голове буферизующий слой.
Middle-вопросы — про механику. Что контролирует аргумент buffering, почему buffering=0 нельзя в текстовом режиме, чем flush() отличается от os.fsync(), что делают tell() и seek() и почему в текстовом режиме нельзя сдвинуться «на десять байтов назад». Здесь же спрашивают про io.StringIO — обычно в контексте тестов, где коду подсовывают поток вместо файла. Senior-уровень уходит в сериализацию — как научить json своим типам через default и object_hook и почему pickle нельзя применять к данным, пересёкшим границу доверия. Типичная ошибка на последнем вопросе — «формат бинарный, значит код не выполнится». Всё ровно наоборот: pickle — это маленькая виртуальная машина, и её опкоды умеют вызывать что угодно.