Урок 26 из 84 · Основы Python
Работа с файлами
Содержание урока
- Немного теории
- Не нужно пугаться
- Открытие и закрытие файлов
- Открываем файлы.
- Автоматическое закрытие файла.
- Запись и чтение
- Запись и чтение содержимого файла целиком.
- Позиция в файле.
- Позиция и текстовые файлы.
- Построчные чтение и запись
- Запись текста построчно.
- Чтение файла построчно.
- Потоковая обработка больших файлов.
- Менеджеры контекста
- Краткое введение в менеджеры контекста.
- Использование файлов как менеджеров контекста.
- Работа с бинарными файлами.
- Подведение итогов
Каждый программист постоянно сталкивается с необходимостью прочитать что-то из файла и записать что-то в файл. При этом может показаться, что файлы — это же так просто!
Немного теории
Однако в современных операционных системах файловый ввод-вывод устроен достаточно сложно. Для обеспечения максимального быстродействия чтения и записи в файлы, а также контроля за безопасностью этого процесса, большинство ОС не позволяют программам напрямую работать с диском. Да те и не могут этого сделать чисто физически: невозможно в одну программу заложить понимание всего того множества файловых систем (file system, FS — способ хранения данных в файлах на диске), которые существуют и используются в наше время! А ведь кроме поддержки разных FS, современная операционная система занимается ещё кэшированием операций с диском, проверкой прав доступа, и прочими служебными делами.
Программам же операционная система предоставляет специальные объекты — файловые дескрипторы (file descriptors). Имея файловый дескриптор, можно "просто писать" и "просто читать" данные, не задумываясь о файловой системе, кэшировании и прочих "низкоуровневых сущностях".
И вот тут-то и скрывается подводный камень: файловые дескрипторы удобны, но на создание каждого расходуется некоторое вполне ощутимое кол-во ресурсов. Поэтому у операционной системы есть общий лимит на количество одновременно использующихся файловых дескрипторов. И при этом каждая программа дополнительно ограничена — имеет свой собственный лимит. Как только программа исчерпает доступное ей количество дескрипторов, следующая же попытка открыть очередной файл закончится с ошибкой! Поэтому программисту важно следить за тем, сколько файлов программа открывает в каждый момент и закрывает ли она файлы своевременно.
Следить за закрытием файлов важно ещё и потому, что одновременная работа над одним файлом может привести к непредсказуемому результату. А ещё некорректно закрытый файл может быть сохранён на диск неправильно, что приведёт к потере данных.
Не нужно пугаться
Описанное выше может кого-то напугать сложностью. Однако Python — современный язык программирования. Он позволяет избежать многих проблем, возникающих при работе с файлами.
На этом занятии вы узнаете, как работать с текстовыми файлами. Однако основные приёмы и средства, с которыми вы познакомитесь, применимы к файлам с любым содержимым!
Открытие и закрытие файлов
Открываем файлы.
В Python файл открывается с помощью функции open, которой нужно передать путь до файла и режим. С путём всё понятно. А режим нужен для того, чтобы указать то, как мы хотим использовать файл: будем ли мы записывать или читать, будем работать с текстом или же с бинарными данными, хотим ли мы очистить файл перед записью в него. Пока мы будем работать с файлами в самых простых режимах: чтение и запись текста.
Чтобы что-то прочитать из файла, надо бы его создать и что-то в него записать. С этого и начнём. Откроем файл на запись:
>>> f = open('foo.txt', 'w')
>>> f
<_io.TextIOWrapper name='foo.txt' mode='w' encoding='UTF-8'>
Переменная f теперь ссылается на некий файловый объект. Пусть вас не пугает имя типа, пока вы можете обращать внимание только на параметры — name, mode и encoding. Имя и режим те, что мы указали при вызове open, а кодировка encoding выбрана умолчательная — UTF-8 (практически всегда именно она вам и будет нужна).
Итак, файл мы открыли. Закрывается он так:
…
>>> f.close()
>>> f.closed
True
closed здесь — атрибут объекта f. Атрибуты — это такие переменные внутри связанного объекта.
Автоматическое закрытие файла.
Вы уже знаете, что Python — язык с автоматическим управлением памятью. Зная это, можно догадаться, что среда исполнения закрывает файл, когда удаляется последняя ссылка на файловый объект. Легко и просто!
В простых скриптах можно не заниматься ручным закрытием файлов. Дело в том, что скрипты, как правило, не открывают большое количество файлов одновременно и выполняются за достаточно короткое время. Таким образом закрытие файлов по завершению выполнения скрипта вполне приемлемо.
А вот в больших программах, которые работают в течение продолжительного времени, уже нужно следить за тем, чтобы файлы закрывались вовремя, в идеале — сразу после того, как всё нужное было из файла прочитано или записано в него.
Запись и чтение
Запись и чтение содержимого файла целиком.
Давайте откроем файл на запись, запишем в него текст и закроем файл. Метод write записывает строку в файл без какой-либо дополнительной обработки:
>>> f = open("foo.txt", "w")
>>> f.write('Hello\nWorld!')
12
>>> f.close()
Здесь "w" означает режим "запись" (write), а 12 сообщает о количестве байт, которые мы записали в файл соответствующим вызовом write.
Теперь откроем файл в режиме чтения и прочитаем всё содержимое:
>>> f = open("foo.txt", "r") # "r" — чтение, read
>>> f.read()
'Hello\nWorld!'
>>> f.close()
Ура, мы прочитали ровно то, что записали до этого.
Позиция в файле.
Вызывать метод write можно несколько раз. Каждый раз мы получим в ответ кол-во символов, которое записали. Текст же будет накапливаться. Так происходит потому, что вызов метода write перемещает так называемую текущую позицию в файле в его (файла) конец.
read тоже учитывает текущую позицию: читает текст от неё и до конца файла. Методу можно явно сказать, сколько символов должно быть прочитано — в этом случае метод постарается прочитать указанное количество символов, если таковых хватает в файле. В любом случае вызов метода read перемещает позицию в то место, где закончилось чтение.
При открытии файла текущая позиция всегда указывает на первый символ текста (т.е. равна нулю). А при прочтении файла до конца с помощью вызова метода read без параметров позиция перемещается в конец файла и последующие чтения ничего не дают:
>>> f = open("foo.txt", "r") # "r" — чтение, read
>>> f.read()
'Hello\nWorld!'
>>> f.read()
''
>>> f.close()
Позицию в файле можно изменять и вручную. Для этого используется метод seek, о котором вы можете почитать в документации.
Позиция и текстовые файлы.
Чаще всего вам придётся работать с текстовыми файлами. В таких файлах позиция не столь важна, т.к. не учитывает разделение текста на строки. А вот при работе с файлами в двоичном режиме умение работать с позицией и смещениями очень важно. Советую поэкспериментировать с позиционированием внутри файлов и записью / чтением нетекстовых данных.
Построчные чтение и запись
Запись текста построчно.
Ранее я упоминал, что последовательные вызовы метода write дописывают текст в конец. Но часто мы имеем итератор, выдающий некий текст построчно. Можно, конечно, написать цикл, однако есть способ и получше: метод writelines. Работает он так:
>>> f = open("foo.txt", "w")
>>> f.writelines(["cat\n", "dog\n"])
>>> f.close()
>>> f = open("foo.txt", "r")
>>> print(f.read())
cat
dog
>>> f.close()
Как вы видите, все строчки записались в нужном порядке. Такой вариант записи предпочтителен, когда нужно записать большой объем текста, который вы получаете и обрабатываете строчка-за-строчкой. Можно предварительно накопить весь текст в одну большую строку, однако для этого может потребоваться большой объём памяти. Гораздо лучше записывать строчки по мере готовности и writelines для этого подходит идеально!
Чтение файла построчно.
Файл построчно можно не только писать, но и читать:
>>> f = open("foo.txt")
>>> f.readline()
'cat\n'
>>> f.readline()
'dog\n'
>>> f.readline()
''
>>> f.close()
Здесь Python сам понимает, что строчки в тексте нужно разделять по символу перевода строки. Вызов readline перемещает позицию к следующей строке и как только текст закончится, все последующие вызовы будут возвращать пустую строку.
Заметьте, строчки текста содержат и сами символы перевода строки.
Метод readline довольно удобен, когда мы хотим управлять процессом чтения из файла. Однако часто нужно просто прочитать все строчки текста. Для этого нужно всего навсего... проитерировать файловый объект! При этом вы получите итератор строчек, который можно читать в цикле:
>>> f = open("foo.txt")
>>> for l in f:
... print(l)
...
cat
dog
>>> f.close()
Если не указать режим, как я сделал в этот раз, то файл откроется на чтение.
Подумайте, почему напечатались лишние пустые строчки.
Итератор строчек файла, как и положено, ленив. Он вычитывает строки лишь по мере необходимости. А останавливается тогда, когда читать уже становится нечего.
Ленивость позволяет, в частности, не дочитать файл:
>>> f = open("foo.txt")
>>> for l in f:
... print(l)
... break
...
cat
>>> print(f.read())
dog
>>> f.close()
Если же нужно получить сразу все строчки текста в виде списка, то можно вызывать метод readlines и получить тот самый список.
Потоковая обработка больших файлов.
Использование итераторов очень удобно для потоковой обработки файлов. При потоковой обработке нет необходимости хранить весь файл в памяти, а значит обрабатываемые файлы могут быть очень большими! Вот так может выглядеть скрипт, который нумерует строчки входного файла и записывает в выходной:
input_file = open("input.txt", "r")
output_file = open("output.txt", "w")
for i, line in enumerate(input_file, 1):
output_file.write("{}) {}".format(i, line))
input_file.close()
output_file.close()
Сохраните этот скрипт в файл и посмотрите, как он работает.
Менеджеры контекста
Ручное закрытие файлов, а также отдача закрытия на откуп среде исполнения, обладают одним существенным недостатком: если между открытием файла на запись и его закрытием произойдёт ошибка, то в лучшем случае файл окажется открыт слишком долго (может остаться ссылка), а в худшем случае не сохранится часть данных.
Хочется иметь возможность автоматически закрывать файл сразу после окончания работы с ним и осуществлять закрытие даже при возникновении ошибки. Файловые объекты уже умеют работать в таком режиме, но для этого их нужно использовать как менеджеры контекста.
Краткое введение в менеджеры контекста.
Менеджер контекста (context manager) — это некий объект, реализующий одноимённый протокол (да, кругом протоколы!). Объекты, реализующие этот протокол, позволяют использовать следующий специальный синтаксис:
with object as foo:
# Здесь нам доступен ресурс foo.
# Это тело with-блока.
# А здесь ресурс foo уже освобождён,
# даже если в теле with-блока произошла ошибка.
Весь код в теле with-блока работает "в контексте". Чаще всего контекст подразумевает выделение некоего ресурса, например, файла. По выходу из контекста ресурс автоматически освобождается, даже если при выполнении блока возникло исключение. То, что нам нужно!
Использование файлов как менеджеров контекста.
Рассмотрим сразу комплексный пример. Для чего перепишем скрипт нумерации строк файла из примера выше:
with open("input.txt", "r") as input_file:
with open("output.txt", "w") as output_file:
for i, line in enumerate(input_file, 1):
output_file.write(
"{}) {}".format(i, line)
)
Явные вызовы close пропали, но теперь файлы закрываются надёжно и вовремя!
Стоить отметить, что именно файловый менеджер контекста чаще всего используется в повседневной жизни питониста. Учитесь использовать его, и ваши программы будут надёжными!
Работа с бинарными файлами.
В языке Python существуют средства для работы с бинарными или двоичными файлами. Бинарные файлы используют строки типа bytes. Это значит при чтении бинарных данных из файла возвращается объект типа bytes.
Примерами бинарных файлов являются как изображения в таких форматах, как JPEG или GIF, аудиофайлы в формате MP3, или бинарные форматы документов, такие как Word или PDF. Для работы с ними нужно просто добавить b к значению режима работы с файлом: rb (открытие на чтение бинарного файла), wb (открытие на запись в бинарный файл, содержимое файла удаляется, если файла не существует, создается новый), ab (открытие на дозапись в бинарный файл, информация добавляется в конец файла):
with open("first.jpg", "rb") as input_file:
with open("second.jpg", "wb") as output_file:
f_data = input_file.read()
output_file.write(f_data)
В этом примере мы копируем содержимое картинки first.jpg и копируем его в картинку second.jpg, иначе говоря мы копируем картинку и создаем новую картинку на ее основе.
Подведение итогов
В рамках этой темы вы узнали, что хоть работа с файлами и требует внимательности, но Python многое берёт на себя и упрощает жизнь разработчику.
Общие же принципы, вроде использования менеджеров контекста, вы будете встречать в чужом коде очень часто. Пусть мы и не рассмотрели принципы работы всей этой "магии" с автоматическим закрытием файла, но принять на вооружение сам подход вы уже можете!