Что на самом деле происходит без close

Операционная система ведёт собственную таблицу открытых файлов для каждого процесса. Когда программа вызывает open, система выделяет свободный номер-дескриптор и связывает его с файлом на диске. Закрытие файла возвращает этот номер обратно в пул свободных, а без close номер числится занятым до самого завершения программы или до вмешательства сборщика мусора.

Типичный сценарий поломки выглядит скромно: цикл читает сотни или тысячи мелких файлов один за другим, и в каждой итерации вызывается open, но close забыт. На первых десятках итераций ничего не происходит, программа работает как ни в чём не бывало. Ближе к концу большого цикла операционная система начинает отказывать в новых дескрипторах, и Python поднимает исключение о нехватке открытых файлов.

Дескриптор как ограниченный ресурс

Лимит на число одновременно открытых файлов задаёт операционная система, а не объём оперативной памяти компьютера. На Linux типичный лимит на процесс — порядка тысячи, его можно посмотреть и изменить командой ulimit, но сам факт ограничения никуда не девается. Даже на машине с десятками гигабайт свободной памяти программа упрётся именно в этот потолок, а не в нехватку места.

CPython закрывает файл сам, когда объект перестаёт быть кому-либо нужен и попадает под сборку мусора по счётчику ссылок — это работает почти сразу и создаёт ложное чувство безопасности. В PyPy и других реализациях Python сборка мусора устроена иначе, объект может простаивать неопределённо долго, и рассчитывать на автоматическое закрытие рискованно в любом случае, независимо от конкретной реализации.

Буферизация и потеря данных

Запись в файл на практике не долетает до диска мгновенно. Python и операционная система буферизуют данные в памяти ради скорости и сбрасывают их на диск пачками, а не построчно. Пока буфер не сброшен, физически на диске может лежать пустой или неполный файл, хотя код, кажется, уже всё туда записал.

Close делает сразу две вещи: сбрасывает буфер на диск и возвращает дескриптор операционной системе. Если программа аварийно упадёт или её принудительно завершат до close, несброшенные данные пропадут безвозвратно, а открытый файл на диске может выглядеть короче, чем ожидалось. Метод flush сбрасывает буфер сам по себе, но дескриптор при этом остаётся занятым.

Контекстный менеджер with

Конструкция with open заменяет ручной вызов close автоматическим закрытием файла при выходе из блока — причём сработает это и при обычном завершении кода, и при исключении внутри блока. Программисту не нужно помнить о close в каждой ветке кода и городить try finally вокруг каждой операции с файлом.

Ручной вызов close в конце функции выглядит рабочим решением, пока где-то в середине не случится ошибка и выполнение не прыгнет мимо строки с close, оставив файл открытым. With устраняет этот класс ошибок целиком, поэтому в современном коде на Python именно эта форма считается основной, а ручной close остаётся скорее исключением для нестандартных случаев.

Как заметить проблему в собственном коде

Проще всего воспроизвести проблему нарочно: написать цикл на несколько тысяч итераций, который открывает файл и ничего не закрывает, и понаблюдать за числом открытых дескрипторов процесса. На Linux для этого подходит команда lsof с фильтром по процессу, на Windows — счётчик хендлов в диспетчере задач или в Process Explorer.

  • Windows не даёт удалить или переименовать файл, хотя скрипт вроде бы закончил с ним работать
  • процесс на Linux падает с ошибкой о нехватке открытых файлов после долгой работы
  • в файле, который вроде бы был записан целиком, оказывается меньше данных, чем ожидалось
  • число открытых дескрипторов процесса стабильно растёт со временем по данным мониторинга

Частые вопросы

Обязательно ли явно вызывать close, если файл открыт через with

Нет, with закрывает файл автоматически при выходе из блока, в том числе при исключении внутри него, поэтому вызывать close вручную поверх with избыточно и ничего не добавляет.

Что будет, если просто забыть закрыть файл в маленьком скрипте

Скорее всего, ничего заметного: Python сам закроет файл, когда сборщик мусора уничтожит объект, а короткий скрипт завершается раньше, чем накопится реальная проблема. Риск проявляется в длительно работающих программах и в циклах с массовым открытием файлов.

Помогает ли flush вместо close

Flush сбрасывает буфер на диск, но не освобождает дескриптор — файл продолжает числиться открытым у операционной системы, пока явно не вызван close или пока не завершится сама программа.

Можно ли открыть один и тот же файл дважды одновременно

Технически да, операционная система выдаст два отдельных дескриптора на один и тот же файл, но одновременная запись из двух мест почти всегда приводит к путанице в порядке данных на диске.