
В этой статье мы разберёмся, как открывать файлы в Python, как происходит обращение к файлу, что такое стандартные потоки, буферизация и файловые дескрипторы, и научимся понимать, что происходит в операционной системе, когда вы работаете с данными из файла.
Текст данной статьи содержит много сложных для новичка терминов и посвящён базовым принципам работы с операционной системой в программировании. Слово «базовый» здесь означает не начальный уровень, а подробный (насколько это возможно) разбор всех основных моментов и терминов.
Если вам интересны именно методы Python, отвечающие за работу с файлами, то мы их кратко описывали ранее в этой статье.
Итак, представьте, что вы уже находитесь в вашей любимой IDE и хотите прочитать данные с какого-либо файла. Первое, о чем вам стоит подумать: «где я нахожусь, и где находится нужный мне файл?». Поговорим о расположении файла и работе с файловой системой.
Навигация по странице
Расположение файла
Вспомните, как вы открываете тот или иной файл в вашей операционной системе. В Windows вы находите директорию (папку) этого файла и кликаете по нему. Далее запускается программа, с которой ассоциирован тип этого файла. Так, если вы кликнете по файлу с расширением «.doc», то, скорее всего, откроется Microsoft Office Word.
Но здесь мы говорим еще не об открытии файла, а о его местоположении. Для того чтобы кликнуть по файлу, вам необходимо было «пройти» до него определённый путь. Например, такой: «C:\Users\Admin\Documents\file.doc». Если что, то это путь до файла file.doc в папке «Документы» вашего пользователя, здесь пользователь зарегистрирован в системе под именем Admin.
В других операционных системах, например, тех, что написаны на базе ядра Unix (macOS и различные дистрибутивы Linux), вы чаще открываете файлы в консоли/терминале.
Например, вывести содержимое файла в Linux можно такой командой:
cat [опции] <путь_к_файлу/имя_файла>
Из этого следует уяснить одно: нам всегда необходимо указывать системе путь до файла, который мы хотим открыть. И, когда вы пишете свою программу на Python, вы точно так же должны указывать путь до файла.
Путь до файла может быть абсолютным или относительным. Пример абсолютного файла у вас выше, там мы указываем системе полный путь: «логический диск (С) → директория (Users) → директория (Admin) → директория (Documents) → имя файла (file.doc)».
Но можно использовать и относительный путь. Относительным называется путь к файлу относительно текущей директории. То есть если бы мы находились в директории «Admin», то относительный путь к нашему файлу был бы такой: «Documents\file.doc». Если же мы находимся в одной директории с файлом, то достаточно просто указать его имя.
При написании небольших программ на Python всегда удобнее пользоваться именно относительным путём до файла и размещать файлы либо в одной директории с исполняемым (.py) файлом (в котором ваша программа написана), либо во вложенной директории.
Представим ситуацию, что вы написали код, который периодически скачивает данные со страниц в интернете и сохраняет их в файл. Хотелось бы, чтобы этот файл сохранялся в директорию «files», которая расположена в той же директории, что и файл с вашей программой.
Но как в таком случае получить путь до этой директории? Особенно это актуально, если ваша программа будет работать на разных компьютерах и, соответственно, находится всегда в разных директориях.
Получить путь до текущей директории, в которой находится исполняемый файл с вашей программой, можно следующим кодом:
import os
import sys
# Получаем путь до директории, в которой находится исполняемый файл
current_file = os.path.abspath(sys.argv[0])
current_directory = os.path.dirname(current_file)
# Выводим путь
print("Путь до текущей директории", current_directory)
В переменной current_file находится абсолютный путь до исполняемого файла, например: «C:\Users\Шаг в будущее\Работа с файлами\main.py»
В первом элементе списка sys.argv содержится название вашего python-файла. Что это вообще за список? Он содержит аргументы, с которыми запускается ваша программа. Например, вместо нажатия сочетания клавиш Shift+F10 в PyCharm, можно запускать код через командную строку Windows такой командой:
python <название_файла> [дополнительные аргументы]
В переменной current_directory находится путь до директории с исполняемым файлом, например: «C:\Users\Шаг в будущее\Работа с файлами».
Таким образом, для решения нашей задачи со скачиванием данных с интернета и сохранением их в директорию с исполняемым файлом, нам достаточно будет указать значение переменной current_directory в качестве пути для сохранения и добавить к ней строку «\files».

Давайте подведём итоги этого раздела. Начинали мы с вопроса «где я нахожусь, и где находится нужный мне файл?». Ответ на первый вопрос мы уже дали, написав код выше. Теперь определим, где находится нужный файл. Точнее будет сказать, куда именно сохранять файл для удобства работы с ним.
Ответ здесь будет простой: если вы пишете разовую программу, чтобы решить какую-то одну задачу, то сохраняйте файл в ту же директорию, где находится исполняемый файл. Тогда внутри программы будете просто указывать имя этого файла и не задумываться о пути вовсе.
Открытие файла
Итак, мы разобрались с тем, где находится файл, ведь сохранили его в ту же директорию, в которой работаем. Теперь надо бы открыть его. Давайте выясним, что происходит в операционной системе, когда мы открываем файл в нашей программе.
Для открытия файла есть простая и понятная функция: open()
. Полный синтаксис её выглядит следующим образом:
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
Довольно устрашающе, не правда ли? Но чаще всего мы будем пользоваться здесь только одним параметром — file, который отвечает за путь до открываемого файла. По совместительству он единственный здесь обязательный параметр.
Как мы уже говорили ранее, можно передавать имя файла, если он находится в той же директории, что и исполняемый файл. Если же нужный файл находится в любой другой директории, то в параметр file передаётся абсолютный или относительный путь до файла.
Функция open()
возвращает файловый объект, с которым мы уже можем производить какие-либо действия: считывать, записывать данные или изменять его содержимое. Помните, что это лишь объект, а не сами данные из файла! Как и у любого объекта, у него есть методы, которые и позволяют работать с данными.
Если погружаться в ООП Python, то тут корректнее будет сказать, что методы эти есть у базового абстрактного класса IOBase, от которого наследуются другие, работающие с потоками данных: RawIOBase, BufferedIOBase и TextIOBase.
То есть в них определены некоторые «правила», по которым мы можем работать с данными. Мы будем разбирать только текстовые потоки, наш класс здесь — TextIOBase (точнее, наследуемый от него TextIOWrapper).
Именно это название вы увидите, если попытаетесь вызвать тип файлового объекта:
f = open('file.txt')
print(type(f))
>>> <class '_io.TextIOWrapper'>
Давайте разберём все параметры функции open()
. Далее под заголовками будут написаны названия параметров, после чего будет краткий обзор терминов, которые используются в описании каждого параметра.
Режим работы
Под параметром mode у нас скрывается режим работы с файлом. Он указывает, будет ли открыт файл для чтения, записи или изменения данных.
Основные режимы следующие:
- ‘r’ (read) — открытие для чтения (по умолчанию). Если файл не существует, вызывается исключение FileNotFoundError.
- ‘w’ (write) — открытие для записи. Если файл существует, его содержимое удаляется. Если файл не существует, он создается.
- ‘x’ (exclusive creation) — открытие для записи, но только если файл не существует. Если файл существует, вызывается исключение FileExistsError.
- ‘a’ (append) — открытие для добавления данных в конец файла. Если файл не существует, он создается.
- ‘b’ (binary) — открытие в бинарном режиме. Используется для работы с бинарными данными (например, изображениями).
- ‘t’ (text) — открытие в текстовом режиме (по умолчанию).
- ‘+’ — открытие для обновления (чтение и запись одновременно).
Режим работы с файлом также влияет и на класс файлового объекта. Если открываем файл в режимах (‘w’, ‘r’, ‘wt’, ‘rt’ и т.д.) то файловый объект будет уже рассмотренного класса TextIOWrapper.
При открытии файла в бинарном режиме возвращаемый объект будет класса BufferedIOBase. При отключённой буферизации в бинарном режиме будет возвращаться объект класса FileIO, наследуемый от RawIOBase.
Буферизация
Параметр buffering определяет стратегию буферизации:
- 0 — отключение буферизации (только в бинарном режиме).
- 1 — построчная буферизация (только в текстовом режиме).
- >1 — размер буфера в байтах.
- -1 (по умолчанию) — использование системного буфера (обычно 4096 или 8192 байт).
А теперь давайте разбираться, что такое буферизация и зачем она нужна. Примем как данное тот факт, что с файлами мы будем работать как с буферизированным текстовым потоком. Также, кстати, работают и операции ввода и вывода данных функциями input()
и print()
.
Дело в том, что при работе с файлами или с выводом текста на экран нам необходимо обращаться напрямую к операционной системе. То есть запрашивать у неё ресурсы на каждую такую операцию. Чем меньше таких обращений к операционной системе, тем быстрее будет работать ваша программа.
Грубо говоря, вместо того чтобы выводить на экран по одному символу, каждый раз «отвлекая» операционную систему и «приказывая» ей вывести этот символ, мы сначала помещаем все данные в буфер и потом только 1 раз обращаемся к операционной системе и посылаем все накопившиеся данные разом.
Аналогично дело обстоит и с чтением данных из файла. Вместо того чтобы считывать текст по одной букве, система сразу помещает все данные в буфер (которые поместятся) и, по мере необходимости, достаёт их оттуда для какой-либо работы с ними.
Систематизируем сказанное. При чтении данных происходит следующее:
- Когда вы читаете данные из файла, система сначала загружает большой блок данных в буфер (например, 4096 байт).
- Последующие операции чтения берут данные из буфера, пока он не опустеет.
- Когда буфер опустеет, система загружает следующий блок данных.
При записи данных:
- Когда вы записываете данные в файл, они сначала накапливаются в буфере.
- Когда буфер заполняется, его содержимое разом записывается на диск (или в другой поток).
Кодировка
Параметр encoding определяет кодировку, используемую для чтения или записи текстового файла. По умолчанию используется кодировка вашей операционной системы.
Чтобы посмотреть кодировку текстового файла в Windows, откройте его в блокноте и прочитайте надпись справа внизу.
Изменить кодировку можно при сохранении файла, с помощью команды «Сохранить как…».

При попытке открыть файл с неверной кодировкой вы увидите непонятные символы внутри вместо ожидаемых букв и цифр.
В таком случае вам следует уточнить кодировку файла и указать её в параметре encoding. На практике рекомендуется всегда явно указывать кодировку при открытии текстовых файлов.
Ошибки кодирования
Как мы уже выяснили, при работе с текстовыми файлами, данные кодируются (при записи) и декодируются (при чтении) с использованием указанной кодировки (параметр encoding).
Если встречаются символы, которые не могут быть корректно преобразованы в указанную кодировку, возникает ошибка. Параметр errors позволяет указать, как Python должен обрабатывать такие ошибки.
У него существует 7 возможных значений:
- ‘strict’ — выбрасывать исключение UnicodeError при ошибке (по умолчанию)
- ‘ignore’ — игнорировать ошибки
- ‘replace’ — заменять некорректные символы на знаки вопроса «�»
- ‘backslashreplace’ — заменять некорректные символы на их escape-последовательности
- ‘surrogateescape’ — заменять коды некорректных байтов на их суррогатные коды
- ‘xmlcharrefreplace’ — заменять некорректные символы на их XML-сущности (например, &#NNNN;). Применимо только для записи
- ‘namereplace’ — заменять некорректные символы на их Unicode-имена (например, \N{CHARACTER NAME})
Обработка символов новой строки
В разных операционных системах используются разные символы для обозначения новой строки:
- Unix/Linux/macOS: \n (перевод строки)
- Windows: \r\n (возврат каретки и перевод строки)
- Старые Mac OS: \r (возврат каретки)
Здесь, наверное, стоит пояснить, что такое каретка и куда мы её возвращаем. Термин «каретка» обозначает металлическую рамку, к которой крепится бумага в пишущих машинках. При наборе текста она постепенно смещалась так, чтобы текст набирался посимвольно слева направо.
Когда же место на строке заканчивалось, оператор нажимал клавишу «Возврат каретки», которая вызывала перемещение каретки в начальное положение, но уже на строку ниже.
Вообще, многие термины, связанные с текстом, которыми мы пользуемся и по сей день, произошли именно от пишущих машинок. В эту категорию можно отнести и термины табуляция (Tab), возврат назад (Backspace), сдвиг (Shift), фиксация верхнего регистра (Caps Lock) и отмена (Escape).
Да и клавиша Enter раньше называлась Return, что является сокращением от рассмотренного выше «Carriage Return» — возврат каретки.
Привычная всем раскладка клавиатуры QWERTY также произошла от пишущих машинок. Эта раскладка позволяла избежать сцеплений рычагов друг с другом, когда подряд набираемые буквы находятся близко к друг другу.
На этом закончим исторический экскурс и вернёмся к параметру newline, который и отвечает за обработку символов новой строки.
При чтении файлов вы можете использовать следующие значения newline:
- Если newline=None (по умолчанию), Python автоматически преобразует все символы новой строки (\n, \r\n, \r) в \n.
- Если newline=”, символы новой строки не преобразуются, и они остаются такими, как есть в файле.
- Если newline задан конкретным символом (например, \n или \r\n), Python использует его для разделения строк.
При записи файлов значения могут быть такими:
- Если newline=None (по умолчанию), Python использует системный символ новой строки (например, \r\n на Windows и \n на Unix).
- Если newline=”, символы новой строки записываются без изменений (как есть).
- Если newline задан конкретным символом (например, \n или \r\n), Python использует именно его для записи новой строки.
Файловый дескриптор
Следующий параметр функции open() — closefd — определяет, нужно ли закрывать файловый дескриптор при закрытии файла. Принимает логическое значение.
Разберёмся, что такое файловый дескриптор. Если сильно упростить, то файловый дескриптор — это число, которое присваивается файлу и используется операционной системой для отслеживания открытых файлов.
В Unix-подобных системах файловые дескрипторы обычно начинаются с 0, 1 и 2, которые соответствуют стандартным потокам ввода (stdin), вывода (stdout) и ошибок (stderr). То есть эти три номера всегда заняты и ваш файл будет иметь номер 3.
Можете убедиться в этом сами (в Python файловый дескриптор можно получить с помощью метода fileno()):
import sys
print(sys.stdin.fileno())
print(sys.stdout.fileno())
print(sys.stderr.fileno())
>>>0
>>>1
>>>2
Жизненный цикл файлового дескриптора можно представить так:
- Открытие файла:
- Когда вы открываете файл с помощью функции open() в Python, операционная система создает файловый дескриптор для этого файла
- Файловый дескриптор связывается с этим файлом на диске
- Использование файлового дескриптора:
- Все операции с файлом (чтение, запись и т.д.) выполняются через файловый дескриптор.
- Операционная система отслеживает, какие файловые дескрипторы принадлежат какому процессу.
- Закрытие файла:
- Когда файл закрывается, файловый дескриптор освобождается.
- Операционная система может повторно использовать этот дескриптор для других файлов.
В пункте 2 происходит работа с файлом. Эта работа сопровождается системными вызовами, такими как:
- open(): Открытие файла и получение файлового дескриптора.
- read(): Чтение данных из файла по файловому дескриптору.
- write(): Запись данных в файл по файловому дескриптору.
- close(): Закрытие файла и освобождение файлового дескриптора.
- lseek(): Перемещение позиции чтения/записи в файле.
Системные вызовы (syscalls) — это программный интерфейс, предоставляемый операционной системой, который позволяет приложениям (вашим программам) запрашивать услуги ядра операционной системы. Можно сказать, что системный вызов — это некий мост между вашим кодом и ядром операционной системы.
Зачем этот мост нужен? Дело в том, что программы не могут напрямую взаимодействовать с оборудованием (например, жёстким диском или сетевой картой) из соображений безопасности и абстракции — это и делает ядро через системные вызовы по запросу приложения.
Надо было это сказать раньше, но дошли до файловых дескрипторов мы только сейчас. Суть в том, что функция open()
не считывает файл «по-настоящему», она лишь подготавливает параметры, которые передаются процессору, и совершает системный вызов.
Номер системного вызова, например, 0 для чтения, передаётся ядру через специальный регистр. Ядро смотрит на номер вызова в таблице системных вызовов (syscall table) и передаёт управление функции ядра, связанной с чтением файлов.
Далее ядро проверяет:
- Действителен ли файловый дескриптор? Связан ли он с уже открытым файлом?
- Имеет ли процесс права на чтение этого файла (доступен ли вообще файл для чтения)? Эта информация берётся из системной файловой таблицы (SFT — System File Table)
- Доступен ли буфер в памяти процесса (чтобы избежать ошибок сегментации)?
Если всё в порядке, ядро обращается к файловой системе:
- Находит файл на диске (через таблицу сущностей — INode Table).
- С помощью специального драйвера читает данные с диска (HDD или SSD) в свой внутренний системный буфер (если они ещё не в кеше).
- Копирует данные из внутреннего буфера ядра в буфер вашей программы.
Ну а дальше мы спокойно работаем с данными из этого буфера, а ядро записывает в регистр (например, rax) количество прочитанных байт (bytes_read) или код ошибки (например, -1, если что-то пошло не так).
Работа системных вызовов с таблицей дескрипторов, системной файловой таблицей и таблицей сущностей проиллюстрирована ниже.

Здесь мы видим, что дескрипторы 3 и 4 связаны с одним файлом — file_1.txt. Причем первый отвечает за запись в файлы, а второй — за чтение из этого файла. Дескриптор под номером 5 связан уже с другим файлом — file_2.txt — и отвечает сразу за чтение и запись.
То есть если несколько процессов запрашивают доступ к одному и тому же файлу (здесь процессы с дескрипторами 3 и 4 запрашивают доступ к файлу file_1.txt), то каждый из этих процессов получит собственный элемент системной файловой таблицы (SFT), несмотря на то что они будут работать с одним и тем же файлом.
Если вкратце, то это все, что нужно знать про системные вызовы на данном этапе.
Теперь вернёмся к параметру closefd. Если мы в функцию передаём имя файла, то значения этого параметра всегда будет True. Попытка присвоить ему значение False приведёт к возникновению ошибки (ValueError: Cannot use closefd=False with file name).
Присвоить параметру closefd значение False можно только тогда, когда вместо имени файла передаётся сам файловый дескриптор. В таком случае при закрытии файла, его дескриптор останется открытым.
В следующем примере файл открывается напрямую средствами модуля os и преобразуется в объект файла:
import os
# Получаем дескриптор файла
fd = os.open('file.txt', os.O_RDONLY)
print(fd) # 3
# Открываем файл
file = open(fd)
# Закрываем файл
file.close()
# Получаем дескриптор файла (тот же)
fd = os.open('file.txt', os.O_RDONLY)
print(fd) # 3
>>>3
>>>3
При таком открытии существующего дескриптора файла метод close() возвращаемого файла закроет используемый дескриптор. Новое открытие файла вернёт нам все тот же дескриптор 3.
Во избежание этого мы передаём в функцию open() аргумент closefd=False. В таком случае новое открытие файла вернёт другой дескриптор (4).
import os
# Получаем дескриптор файла
fd = os.open('file.txt', os.O_RDONLY)
print(fd) # 3
# Открываем файл, closefd=False
file = open(fd, closefd=False)
# Закрываем файл
file.close()
# Получаем дескриптор файла (новый)
fd = os.open('file.txt', os.O_RDONLY)
print(fd) # 4
>>>3
>>>4
Про метод close() и варианты закрытия файла мы подробно поговорим в следующей статье.
Пользовательская функция
И последний необязательный параметр функции open()
— это opener. Он позволяет указать пользовательскую функцию для открытия файла, которая может выполнить дополнительные действия при открытии: установить права доступа или использовать нестандартные механизмы открытия.
Пример такой функции уже был выше, сейчас мы подробнее разберем её аргументы.
Эта функция должна принимать два аргумента:
- file— путь к файлу (строка).
- flags— флаги, которые указывают режим открытия файла (например, O_RDONLY для чтения, os.O_WRONLY для записи и т.д.).
Функция должна возвращать файловый дескриптор.
После вызова пользовательской функции opener Python использует возвращенный файловый дескриптор для создания файлового объекта.
Рассмотрим такой пример: допустим, вы хотите открыть файл с определенными правами доступа (например, 0o644 — чтение и запись для владельца, только чтение для остальных), а также добавить логирование действий с файлом.
Вы можете создать пользовательскую функцию для этого:
import os
# Пользовательская функция для открытия файла с логированием
def custom_opener(file, flags):
print(f'Открываем файл: {file} с флагами: {flags}')
fd = os.open(file, flags, 0o644)
return fd
# Открываем файл с использованием custom_opener
with open('example.txt', 'w', opener=custom_opener) as f:
f.write('Привет!') # Записываем данные в файл
После запуска этого кода на экран будет выведена строка «Открываем файл: example.txt с флагами: 33665». Ну и, само собой, создастся файл «example.txt» со строкой «Привет!» внутри и заданными правами доступа.
На этом мы закончим разбор темы про открытие файлов в Python. В следующей статье рассмотрим функции для чтения, записи и изменения данных в файле и научимся правильно закрывать файлы без утечек памяти.