Создание и открытие файлов

Работа с файлами имеет большую связь с со многими внутренними частями операционных систем. Исторически был создан стандарт POSIX (читается как [позикс], как в слове positive позитивный) — это целая семья стандартов того как работают системные вызовы. Они описывают много вещей и среди прочего работу с файлами. Помимо POSIX еще есть Windows и другие операционные системы. В Python был взят за основу интерфейс языка программирования Си как раз из POSIX'а, потом постепенно он менялся для того чтобы стать единым вне зависимости от того на какой ОС вы вы исполняете свой файл. То есть базовый интерфейс работы с файлами на самом деле это несколько библиотек для разных платформ но вы работаете с ними через единый интерфейс, настолько насколько это возможно.

Особенности Windows

Поскольку работа с файлами все же связана с работой операционных систем, то нам придется обсуждать различия между ними. Если вы будете учиться работать с файлами в Windows и вам покажется, что это сложнее чем кажется, то ваши чувства вас не подводят. В POSIX-совместимых операционных системах таких как Ubuntu и другие Linux-ы или macOS, многие вещи гораздо удобнее и реже вызывают желание крушить или плакать в зависимости от вашего темперамента. Это нормально! К счастью Python на самом деле сильно упрощает работу там где это возможно.

В момент запуска Python проверяет на какой системе он работает и подставляет библиотеку для текущей операционной системы, интерфейсы у библиотек одинаковые, поэтому для программиста кажется, что работа с файлами одинакова везде. И ему не надо думать о деталях низкого уровня до тех пор пока они ему самому не понадобятся. Это сильно упростило работу, после этого начался и продолжается процесс добавления новых библиотек которые автоматизируют работу с файлами на высоком уровне. Многие регулярные задачи стали частью стандартной библиотеки языка совсем недавно. Одно из последних нововведений это добавление библиотеки pathlib и она получилась настолько удачной, что мы посвятим ей следующую главу.

Работа с файлами в разных форматах — это самая большая часть стандартной библиотеки языка. У меня не получится перечислить все, но давайте назовем некоторые: os, os.path, pathlib, shutil, tempfile. Для работы с архивами zip, bz2, tar, gzip, lzma и еще другие. Для работами со структурированными файлами ini/cfg, csv, json, множеством форматов хранения электронных писем (Maildir, mbox, MH, Babyl, MMDF), для работы со звуковыми файлами и работа с файловыми дескрипторами в асинхронном режиме. Плюс еще создание файловых объектов в памяти или работа с файлами как с потоками. И это далеко не все.

Поэтому давайте начнем с основ которые позволят вам получить верное представление о том, что такое файл с точки зрения компьютера и языка.

Что такое файл

На самом деле это не такой простой вопрос как кажется. Если у вас не было предварительного опыта программирования, то первое что придет в голову это объект который лежит на диске в папке и который можно скопировать или удалить. Если кликнуть на него в проводнике, то откроется программа которая умеет работать с этим файлом и в ней уже можно будет что-то отредактировать и изменить.

Даже если на основе этих небольших наблюдений собрать информацию вместе то можно собрать следующие характеристики файлов:

  • У файла есть имя.
  • У файлов есть тип. Это часть имени файла после последней точки, например .txt.
  • У каждого файла есть путь.

Расширение файла

В Windows тип файла обозначается иконкой, а расширение скрывается. В настройках проводника лучше всегда включать отображение расширения. Для программистов оно имеет гораздо большее значение чем для обычных пользователей. Специалисты по безопасности обоснованно критикуют практику скрытия расширения, ведь исполняемые файлы хранят иконку внутри себя, а значит файлу с раширением .exe можно добавить иконку безобидного текстового расширения и обманом заставить пользователя запустить вредоносную программу.

Если открыть картинку в редакторе текстов, то вместо графического представления вы получите кучу странных символов. Потому, что графическая информация упакована только для программ которые могут расшифровывать специальный формат. Разные программы по разному решают как хранить данные внутри файлов. Но для многих подобных файлов есть стандарты которые умеют распаковывать сразу много разных программ. Такие файлы называются бинарными, к ним относятся например все форматы картинок (png, jpeg, gif), видео (mp3, mp4, webp), на самом деле таких файлов слишком много даже для того чтобы перечислить их типы.

Файлы которые содержат только текст и которые можно открыть и редактировать без специальных редакторов называют текстовые. Все исходные коды на Python являются текстовыми файлами (расширение .py), кроме того в Python проектах будут попадаться текстовые файлы с расширениями txt, md (markdown), rst (restructured text). Файлы которые создаются в текстовых редакторах таких как Word на самом деле не являются текстовыми, они хранят информацию о тексте в бинарном виде, с ними работают с помощью специальных библиотек. Иногда случаются и непредвиденные ситуации когда текстовый документ открывается в нечинаемом виде, обычно это значит, что они открыты с неправильной кодировкой. Мы начнем работать с текстовыми файлами, потому что их проще понимать без помощи специализированных программ и попытаемся разобраться с ними как они работают. Подразумевается, что файлы мы будем создавать в кодировке UTF-8, это стандартная кодировка строки для Python и по умолчанию эта кодировка используется в macOS и Linux.

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

Стандартные файловые дескрипторы

В операционной системе всегда есть открытые хендлеры для ввода и вывода информации. Когда выполняется функция print, то она на самом деле отправляет данные в файловый хендлер stdout который всегда доступен всем программам. А функция input считывает информацию поступающую из stdin, вот почему когда мы работаем в интерактивном режиме с интерпретатором то traceback пишет, что ошибка в файле <stdin>:

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
...

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

Если вы хотите попробовать поэкспериментировать с выводом, то можете указать функции print в какой файл выводить информацию через параметр file, а файл взять из модуля sys:

import sys
print("Это ошибка", file=sys.stderr)

Открытие файла в Python

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

open('<путь>', '<режим>')

Параметры функции:

  • Путь — это строка которая содержит имя файла, но так же может содержать и путь. В зависимости от того где был запущен интерпретатор он будет считать что текущая директория есть его рабочее место. Если не указать полный или относительный путь к файлу, то он будет создан в текущей рабочей директории.
  • Режим — это специальная строка которая кодирует режим работы с файлом.

Возможные режимы складываются из комбинаций букв:

  • rread, читать
  • wwrite, писать
  • x — exclusive, эксклюзивная запись, если файл уже существует, то вернется ошибка
  • bbinary, бинарный режим записи
  • ttext, текстовый, этот режим по умолчанию
  • aappend, дописывать в конец
  • + — расширяет режим позволяя обновлять существующий файл, подходит и для для чтения и для записи одновременно, открытие файла с этим режимом не удаляет старое содержимое

Вот расшифровка сочетаний режимов открытия файлов и их значений:

Режим Значение
r, rt Открыть в текстовом режиме только для чтения. Указатель в начале файла
rb Открыть для чтения в двоичном формате. Указатель в начале файла
r+, rt+ Открыть в текстовом режиме для чтения и записи. Указатель в начале файла
rb+ Открыть для чтения и записи в двоичном формате. Указатель в начале файла
w, wt Открыть в текстовом режиме только для записи. Указатель в начале файла. Удаляет старое содержимое и создает новый файл
wb Открыть для записи в двоичном формате. Указатель в начале файла. Удаляет старое содержимое и создает новый файл
w+, wt+ Открыть в текстовом режиме для чтения и записи. Указатель в начале файла.
wb+ Открыть для чтения и записи в двоичном формате. Указатель в начале файла.
x, xt Эксклюзивно создать файл в текстовом режиме для записи. Если файл с таким именем существует, то вызовет исключение FileExistsError
xb Эксклюзивно создать файл для записи в двоичном формате. Если файл с таким именем существует, то вызовет исключение FileExistsError
a, at Открыть в текстовом режиме для добавления информации в файл. Указатель в конце файла. Создает новый файл, если такового не существовало
ab Открыть для добавления в двоичном формате. Указатель в конце файла. Создает новый файл, если такового не существовало
a+, at+ Открыть в текстовом режиме для добавления и чтения. Указатель в конце файла. Создает новый файл, если такового не существовало
ab+ Открыть для добавления и чтения в двоичном формате. Указатель в конце файла. Создает новый файл, если такового не существовало

После выполнения функция создает объект ввода-вывода, который взаимодействует с файловым хендлером операционной системы. Мы работаем с ним как с объектом Python у которого есть свои методы и свойства.

Свойства файлового объекта

Давайте откроем свой первый файл и посмотрим на полученный объект:

>>> f = open("test.txt", "wt")
>>> type(f)
<class '_io.TextIOWrapper'>
>>> dir(f)
['_CHUNK_SIZE', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable', '_finalizing', 'buffer', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'line_buffering', 'mode', 'name', 'newlines', 'read', 'readable', 'readline', 'readlines', 'reconfigure', 'seek', 'seekable', 'tell', 'truncate', 'writable', 'write', 'write_through', 'writelines']
>>> f.closed
False
>>> f.close()
>>> f.closed
True

У объекта файла есть множество методов и свойств. Самое главное, что надо знать, то что у файлового объекта есть состояние и оно проверяется с помощью флага f.closed, файл считается открытым до тех пор пока не будет явно вызван метод f.close().

Файлы обязательно надо закрывать

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

Помимо стандартного интерпретатора CPython еще есть множество других, например PyPy. Его автор в одном из комментариев на Stackoverflow сказал, что всегда есть шанс, что после завершения работы программы файл который не был закрыт принудительно до этого не будет закрыт правильно. И действительно, если убить процесс через менеджер процессов или командой kill, то события которые были запланированы чтобы быть исполненными перед выходом из программы могут не случиться.

Просто сделайте правилом всегда вызывать метод .close() когда работа с файлом окончена.

Чтобы не вдаваться в сложные подробности хочу обратить внимание, что на самом деле в Python есть множество типов объектов ввода вывода. Но все они последовательно создают объекты с теми же свойствами и теми же методами, или говоря языком программиста предоставляют один и тот же интерфейс взаимодействия. В программировании явление когда разные объекты ведут себя схожим образом в шутку называют Утиная Типизация. Давайте рассмотрим основные методы и свойства таких объектов:

Метод или свойство Описание
close() Очистка буфера и закрытие потока или файла. Если файл уже закрыт, то попытки записи будут вызывать ValueError, но сам метод можно вызывать множество раз
closed True если файл или поток закрыт
fileno() Возвращает номер связанного с объектом файлового дескриптора. Вернет OSError если объект не использует файловый дескриптор
flush() Вызывает запись буфера файла на диск. Ничего не делает если файл открыт для чтения или если запись не используется блоковая запись
isatty() True если поток является интерактивным, например если это интерфейс ввода пользователем (терминал или tty )
readable() True если поток позволяет чтение. Если эта функция возвращает False то попытка чтения с помощью read() вызовет OSError
readline(size=-1) Прочитать строку из файла. Если size указан, то будет прочитано не больше size байт. Если файл открыт в бинарном режиме, то для разделения строк ипользуетс b'\n'; для текстовых можно использовать параметр newline функции open() для указания маркера новой строки
readlines(hint=-1) Прочитать строки из файла и вернуть в ввиде списка. hint указыват сколько строк читать, в этом случае будет считано не более hint строк
seek(offset[, whence]) Переместить текущую позицию в файле на offset байт. Направление задается параметром whence, он может быть цифровым или использовать константы модуля io: 0/io.SEEK_SET — начало файла, 1/io.SEEK_CUR относительно текущей позиции и 2/io.SEEK_END относительно конца файла. Возвращает абсолютную позицию.
seekable() True если файл или поток позволяет свободное перемещение курсора. Если функция вернет False то операции seek(), tell() и truncate() вызовут OSError.
tell() Возвращает текущее положение курсора
truncate(size=None) Изменяет размер файла до указанного размера, если size не указан, то обрезает файл в текущей позиции курсора. Если после операции размер файла увеличился, то в зависимости от платформы новое место заполняется нулевыми байтами.
writable() True если поток позволяет запись. Если эта функция возвращает False то операции write() и truncate() вызовут исключение OSError
writelines(lines) Записать строки lines в поток. Разделитель строк не будет автоматически добавлен в файл, поэтому каждая строка должна заканчиваться символом разделителя строк (обычно это \n)
__del__() или del f Деструктор объекта f, вызов оператора del f вызовет операцию удаления объекта из памяти. По умолчанию вызывается метод close()

Для объектов которые связаны с чувствительными данными, например такими как файлы или соединения с базами данных, есть возможность сделать так называемые деструкторы. Это специальный магический метод с именем __del__ который вызывается в тот момент когда объект должен очиститься из памяти. Это случается в тот момент когда на объект больше не существует ссылок или когда явно вызывается оператор del с именем переменной которая ссылается на объект. Но существует множество ситуация во время которых __del__ так и не будет вызван. Поэтому всегда лучше явно вызывать close или пользоваться конструкцией with, речь о которой пойдет в следующей главе.

Смещение в файле

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

Если вы хотите переместить указатель текущего положения в файле, то вызовите метод .seek(offset, from_what):

  • offset — это смещение, для того чтобы перейти на начало файла используйте 0
  • from_what — откуда считать. 0 — от начала, 1 — от текущей позиции, 2 — от конца файла, в этом случае смещение обычно негативное

Для того чтобы прочитать данные из файла есть несколько методов, для бинарных данных удобно использовать метод .read(size), где size — это сколько байт надо прочитать. После вызова функция возвращает данные которые прочитает из файла, а текущее положение изменится.

Чтобы узнать свое текущее положение используйте метод .tell(). Он не изменяет положение курсора в файле.

Перемещение по файловому объекту

Как я говорил ранее не обязательно файловый объект привязан к файлу. Например, sys.stdout ведет себя как файл в который можно только писать данные, но нельзя считывать. Перемещать указатель в таком файле невозможно. Но даже если вы открыли файл на диске будет разница в том в каком режиме вы его открыли. В текстовых документах текст зависит от кодировки, и вам может показаться что вы записали 1 символ, а он займет 2 или больше байт на диске. Особенно это касается эмодзи.

Давайте рассмотрим пример, я буду использовать бинарный файл для более простой иллюстрации:

>>> f = open('test.dat', 'wb+')
>>> f.write(b'0123456789')
10
>>> f.seek(5)
5
>>> f.read(1)
b'5'
>>> f.tell()
6
>>> f.seek(-3, 2)
7
>>> f.tell()
7
>>> f.read(1)
b'7'
>>> f.close()

Мы открыли файл test.dat в режиме для записи в бинарном режиме. С помощью метода .write() добавили в него последовательность байт. Перемещение внутри файла дало нам возможность считывать символы в определенных позициях, причем считывание перемещало курсор.

Работа с текстовыми файлами

Работа с файлами в текстовом режиме имеет некоторые приятные особенности в Python. Прежде всего файл — это поток или последовательность строк и было бы естественно работать с файлом как со списком строк. Поэтому Python позволяет использовать файл как источник данных которые можно обрабатывать в цикле читая файл построчно, это эквивалентно скрытому вызову метода readlines(). Поскольку каждая строка возвращается как есть, то она будет содержать знаки новой строки в конце каждой строки. Функция print автоматически добавляет знак перевода строки, чтобы этого не было можно либо обрезать последний знак в строке, либо указать print не добавлять \n после каждого вывода print(line, end='').

Файл reader.py:

1
2
3
4
5
6
paper = open("turing_paper_1936.txt", "rt")

for line in paper:
    print(line[:-1])

paper.close()

Что выведет на экран содержимое файла turing_paper_1936.txt:

ON COMPUTABLE NUMBERS, WITH AN APPLICATION TO THE ENTSCHEIDUNGSPROBLEM

                            By A. M. TURING.

            [Received 28 May, 1936.—Read 12 November, 1936.]

The "computable" numbers may be described briefly as the real numbers whose 
expressions as a decimal are calculable by finite means. Although the subject
of this paper is ostensibly the computable numbers. it is almost equally easy 
to define and investigate computable functions of an integral variable or a 
real or computable variable, computable predicates, and so forth. The 
fundamental problems involved are, however, the same in each case, and I have 
chosen the computable numbers for explicit treatment as involving the least 
cumbrous technique. I hope shortly to give an account of the relations of the 
computable numbers, functions, and so forth to one another. This will include 
a development of the theory of functions of a real variable expressed in terms 
of com- putable numbers. According to my definition, a number is computable if 
its decimal can be written down by a machine.

Ссылки