Работаем с файлами#
В предыдущем разделе мы автоматизировали решение квадратного уравнения и написали интерфейс пользователя. Но что, если нам нужно решить десяток (и более) квадратных уравнений? В этом нам поможет файл с исходными данными. Да, сейчас задача предельно просто, и файл будет хранить по три коэффициента каждого уравнения. Но в реальных задачах исходные данные могут иметь сложную структуру и большой объём. Задавать их как через командную строку, так и через графический интерфейс бывает просто невозможно.
Давайте разберёмся, какие инструменты имеются в Python для работы с различными файлами.
Распространённые типы файлов#
Файлы бывают:
бинарными — содержат биты (байты). Открыв бинарный файл в текстовом редакторе, вы увидите что-то такое. Прочитать такой файл легко, но для получения хранимых данных нужно знать алгоритм их записи (хранимую структуру данных). Соответственно, для каждого такого файла нужно написать собственный парсер, который переведёт данные из файла в данные программы. В действительности любой файл является бинарным. В Python вы вряд ли будете часто напрямую работать с байтами в файлах, поскольку существуют более удобные форматы;
текстовыми — это бинарный файл, в котором байты интерпретируются текстовым редактором как коды символов. Какому конкретно символу отвечает последовательность байтов (и какова её длина) зависит от используемой кодировки (ASCII, UTF-8 и т.д.);
исполняемыми — бинарный файл, хранящий в себе машинный код программы. Вызывая такой файл на исполнение, вы, тем самым, вызываете программу;
графическими — бинарный файл, интерпретируемый как изображение;
звуковыми — бинарный файл, интерпретируемый как последовательность звуковых частот;
архивными и другими.
При решении учебных задач вы будете сталкиваться в основном с текстовыми, графическими и архивными файлами.
Часто встречающиеся в практике Python текстовые файлы: txt
, csv
, yaml
, json
.
Есть также возможность чтения и записи данных в таблицы Excel.
С графическими файлами вы столкнётесь при работе с библиотекой Matplotlib и ей подобными.
Вы сможете сохранять различные графики в файлы png
, jpg
, tiff
и другие.
В архивные файлы вы сможете сохранять многомерные массивы NumPy (архивы npz
).
В этом разделе познакомимся с тем, как в Python работать с текстовыми файлами.
Работа с текстовыми файлами#
Пусть у нас есть файл some_text.txt
со следующим содержанием:
Hello, world, from text file!
Numbers 1 2 3
Another line
Строка на русском хоба!
そして日本語で??
Прочитаем его! Для этого:
Откроем файл:
f = open("some_text.txt")
Прочитаем содержимое в переменную
content
:
content = f.read()
Закроем файл:
f.close()
Посмотрим, что мы прочитали из файла:
print(content)
Hello, world, from text file!
Numbers 1 2 3
Another line
Строка на русском хоба!
гЃќгЃ—гЃ¦ж—Ґжњ¬иЄћгЃ§пјџпјџ
Вот так незамысловато!
Но при работе с открытым файлом может случиться ошибка, из-за которой программа аварийно прекратит работу.
При этом строка f.close()
выполнена не будет, и файл останется открытым для операционной системы.
Это нехорошо, поскольку может помешать корректной работе с этим файлом из той де или любой другой программы.
Чтобы это избежать, используют менеджер контекста with
:
with open("some_text.txt") as f:
content = f.read()
print(content)
Hello, world, from text file!
Numbers 1 2 3
Another line
Строка на русском хоба!
гЃќгЃ—гЃ¦ж—Ґжњ¬иЄћгЃ§пјџпјџ
Его польза заключается в том, что файл открыт лишь внутри тела with
.
При выходе из менеджера контекста, в том числе аварийном, файл будет автоматически закрыт.
Note
Если вы пока не можете понять, как устроен этот менеджер, не переживайте. Это значит лишь, что вам пока не хватает опыта программирования.
Функция open
помимо имени файла (или пути к нему) принимает множество других параметров.
Одним из важнейших является режим работы с файлом.
Режим может быть следующим:
чтение
r
;запись
w
(если файл существует, то он будет перезаписан; если не существует, то будет создан);запись в конец файла
a
(если файл существует, то запись будет производиться в его конец;если файл не существует, то будет создан).
Режимы можно смешивать, открывая файл, например, для чтения и записи в конец ra
.
Note
Подробнее о режимах работы с файлами можете почитать здесь.
В рассмотренном случае тот же результат мы получим, если откроем файл только для чтения:
with open("some_text.txt", "r") as f:
print(f.read())
Hello, world, from text file!
Numbers 1 2 3
Another line
Строка на русском хоба!
гЃќгЃ—гЃ¦ж—Ґжњ¬иЄћгЃ§пјџпјџ
Выше было отмечено, что текстовый (да и любой) файл является бинарным. Можем ли мы прочитать текстовый файл как бинарный? Конечно! Вот как:
with open("some_text.txt", "rb") as f:
bin_content = f.read()
print(bin_content)
b'Hello, world, from text file!\r\nNumbers 1 2 3\r\nAnother line\r\n\xd0\xa1\xd1\x82\xd1\x80\xd0\xbe\xd0\xba\xd0\xb0 \xd0\xbd\xd0\xb0 \xd1\x80\xd1\x83\xd1\x81\xd1\x81\xd0\xba\xd0\xbe\xd0\xbc \xd1\x85\xd0\xbe\xd0\xb1\xd0\xb0!\r\n\xe3\x81\x9d\xe3\x81\x97\xe3\x81\xa6\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e\xe3\x81\xa7\xef\xbc\x9f\xef\xbc\x9f'
В функции open
мы указали режим работы с файлом: r
означает “чтение”, b
— “бинарный режим”.
Заметьте, что изменился результат.
Теперь в переменной bin_content
содержится байт-строка.
Байт-строку мы можем создать и сам. Вот пример:
byte_str = b"\xd0\x91\xd0\xb0\xd0\xb9\xd1\x82\xd1\x8b!"
Здесь \xd0
, например, есть (шестнадцатеричное) обозначение байта D0
.
Аналогично с остальными 9 байтами.
“И что?” — спросите вы.
А то, что мы можем интерпретировать эти байты, как нам вздумается.
Предположим, что в byte_str
на самом деле содержится какое-то слово, записанное в кодировке UTF-8.
Тогда мы можем получить что-то привычное для нас, если декодируем байт-строку, указав кодировку:
byte_str.decode("utf-8")
'Байты!'
Ого!
Мы угадали!
Оказывается так b”\xd0\x91\xd0\xb0\xd0\xb9\xd1\x82\xd1\x8b!” выглядит последовательность байтов, кодирующих фразу Байты!
.
И заметьте, что букв 5, а байтов 10, то есть каждый символ закодирован двумя байтами.
Чтобы понять, почему это так, стоит знать о том, что как кодируются символы.
Но ту же байт-строку byte_str
мы можем декодировать и с другой кодировкой, например:
byte_str.decode("cp1251")
'Байты!'
Получили “кракозябры”… Но получили же! Никакой ошибки не было, просто мы интерпретировали те же байты по-другому.
Мы можем не только декодировать, но и кодировать строки:
"Привет всем!".encode("utf-8")
b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82 \xd0\xb2\xd1\x81\xd0\xb5\xd0\xbc!'
Короче говоря, вы уже можете с лёгкостью написать скрипт по переводу текста из одной известной кодировки в другую (но не делайте, ведь уже есть готовые инструменты в любом уважающем себя редакторе).
К чему был разговор о кодировках?
Функции open
можно передать кодировку текстового файла, чтобы не получить белиберду (или чтобы получить):
with open("some_text.txt", "r", encoding="cp1251") as f:
print(f.read())
Hello, world, from text file!
Numbers 1 2 3
Another line
Строка на русском хоба!
гЃќгЃ—гЃ¦ж—Ґжњ¬иЄћгЃ§пјџпјџ
Мы нарочно открыли файл с указанием не той кодировки (его кодировка по умолчанию в моём редакторе UTF-8). И что мы видим? Английский текст прочитался как ни в чём не бывало. А вот русский и японский нет… Дело в том, что ряд кодировок (среди них CP1251 и UTF-8) совместимы с базовой кодировкой ASCII, поддерживающей только латиницу, цифры и основные знаки пунктуации.
Note
ASCII была разработана в США для англоязычных компьютерщиков на заре компьютерной эры.
Укажем правильную кодировку:
with open("some_text.txt", "r", encoding="utf-8") as f:
print(f.read())
Hello, world, from text file!
Numbers 1 2 3
Another line
Строка на русском хоба!
そして日本語で??
Вот теперь всё прочиталось верно. Сила UTF-8 как раз и заключается в поддержке огромного количества символов языков, фактически, всего мира. Платой за это является больший “вес” строки (число используемых байт). ASCII, к примеру, является однобайтовой кодировкой (один байт на один символ), но позволяет кодировать всего 256 символов.
Улучшаем решатель квадратного уравнения#
Вспомним, как выглядит наш код на данный момент (без интерфейса):
from argparse import ArgumentParser
from cmath import sqrt as csqrt
from math import sqrt
def solve(a, b, c):
discriminant = b*b - 4*a*c
if discriminant >= 0:
return (
(-b - sqrt(discriminant)) / (2*a),
(-b + sqrt(discriminant)) / (2*a)
)
return (
(-b - csqrt(complex(discriminant))) / (2*a),
(-b + csqrt(complex(discriminant))) / (2*a)
)
Нехитро. Пока не будем делать интерфейс пользователя. Начнём с добавления функции чтения коэффициентов из текстового файла:
def from_file(path):
with open(path, "r") as f:
abc = f.read()
a, b, c = abc.split()
return float(a), float(b), float(c)
Важно сказать, что эта функция крайне не хороша.
В ней есть предположение о структуре данных в файле, а также нам приходится считывать текст, разбивать его (в предположении, что три числа разделены пробелами) и преобразовывать текст в float
.
Всё это чревато ошибками.
Для примера, разделяйте числа в файле запятыми.
Программа скажет вам “пока” и рухнет.
Ну и запустим корректный пример:
a, b, c = from_file("abc.txt")
x1, x2 = solve(a, b, c)
print("x1 =", x1)
print("x2 =", x2)
x1 = -3.0
x2 = 1.0
Работает, но как отмечено выше — крайне ненадёжно. Текстовые файлы ввиду своей универсальности плохо подходят для использования в качестве файлов данных. Слишком много с ними возни, связанной с проверкой корректности, преобразованиями типов данных и прочим. Гораздо лучше подходят файлы JSON и YAML. Посмотрим на них.
Данные из файла YAML#
YAML — это специальный язык для структурированной записи информации, обладающий простым синтаксисом.
Вот как с его помощью можно записать наши исходные данные в файле abc.yml
:
a: 1
b: 2
c: -3
Для работы с yml
-файлами в стандартной библиотеке имеется пакет yaml
:
import yaml
И теперь мы можем переписать функцию from_file
:
def from_file(path):
with open(path, "r") as f:
abc = yaml.safe_load(f)
return abc["a"], abc["b"], abc["c"]
Проверяем:
a, b, c = from_file("abc.yml")
x1, x2 = solve(a, b, c)
print("x1 =", x1)
print("x2 =", x2)
x1 = -3.0
x2 = 1.0
Теперь нам не нужно заботиться о структуре файла данных. Достаточно знать только, какие именно данные там хранятся. А узнать это легко, просто посмотрев глазами содержимое файла.
Данные из файла JSON#
JSON — стандартный текстовый формат для хранения и передачи структурированных данных. Он основан на синтаксисе объекта в языке программирования JavaScript, но нисколько к нему не привязан.
JSON сильно напоминает стандартные словари Python.
Вок так мы можем записать наши коэффициенты в json
-файле:
{
"a": 1,
"b": -2,
"c": 3
}
Note
Коэффициенты отличаются от коэффициентов в файле YAML.
Для работы с JSON в стандартной библиотеке есть пакет json
:
import json
В остальном код тот же:
def from_file(path):
with open(path, "r") as f:
data = json.load(f)
return data["a"], data["b"], data["c"]
И работает аналогично:
a, b, c = from_file("abc.json")
x1, x2 = solve(a, b, c)
print("x1 =", x1)
print("x2 =", x2)
x1 = (1-1.4142135623730951j)
x2 = (1+1.4142135623730951j)
Давайте разделим функции чтения данных из файлов различных форматов: from_yaml
для файлов YAML и from_json
для файлов JSON.
При этом эти функции будут вызываться из функции верхнего уровня from_file
в зависимости от расширения заданного файла.
Выглядит это так:
def from_file(path):
if path.endswith(".yml"):
return from_yaml(path)
if path.endswith(".json"):
return from_json(path)
# else - вызываем стандартное исключение (ошибку)
raise ValueError("unused file format")
def from_yaml(path):
with open(path, "r") as f:
data = yaml.safe_load(f)
return data["a"], data["b"], data["c"]
def from_json(path):
with open(path, "r") as f:
data = json.load(f)
return data["a"], data["b"], data["c"]
Теперь можно работать так:
a, b, c = from_file("abc.json")
x1, x2 = solve(a, b, c)
print("Данные брались из JSON")
print("x1 =", x1)
print("x2 =", x2)
a, b, c = from_file("abc.yml")
x1, x2 = solve(a, b, c)
print("Данные брались из YAML")
print("x1 =", x1)
print("x2 =", x2)
Данные брались из JSON
x1 = (1-1.4142135623730951j)
x2 = (1+1.4142135623730951j)
Данные брались из YAML
x1 = -3.0
x2 = 1.0
Функция чтения файла данных вызывалась одна (from_file
).
И именно в ней реализовано поведение, зависящее от формата файла данных.
Удобно, понятно и надёжно.
О файлах CSV#
CSV — это текстовый формат для представления табличных данных. Строка таблицы соответствует строке текста, которая содержит поля, разделенные запятыми. Тип файлов предназначен для передачи объемных текстовых данных между различными программами и сервисами.
Note
Аббревиатура CSV расшифровывается как Comma Separated Values (значения, разделённые запятой).
В стандартной библиотеке найдётся пакет csv
для работы и с такими файлами.
Однако по сравнению с YAML и JSON в CSV по умолчанию все строки — это именно строки, то есть текст.
И числа нужно преобразовывать в коде “вручную”.
Наверняка, есть решения этой проблемы.
Поищите в интернете.
Пока же отметим, что табличные данные не настолько универсальны, как формат YAML или JSON, с помощью которых можно описывать данные любой иерархии.
В вычислительных программах довольно часто приходится сталкиваться с таблицами.
Однако вместо того, чтобы использовать модуль csv
, лучше рассмотреть библиотеку Pandas, реализующую, по сути, весь функционал Excel, но в Python.
Pandas, среди прочего, может работать и с файлами CSV.
На данный момент полная программа выглядит так:
import json
import yaml
from argparse import ArgumentParser
from cmath import sqrt as csqrt
from math import sqrt
def solve(a, b, c):
discriminant = b*b - 4*a*c
if discriminant >= 0:
return (
(-b - sqrt(discriminant)) / (2*a),
(-b + sqrt(discriminant)) / (2*a)
)
return (
(-b - csqrt(complex(discriminant))) / (2*a),
(-b + csqrt(complex(discriminant))) / (2*a)
)
def from_file(path):
if path.endswith(".yml"):
return from_yaml(path)
if path.endswith(".json"):
return from_json(path)
# else - вызываем стандартное исключение (ошибку)
raise ValueError("unused file format")
def from_yaml(path):
with open(path, "r") as f:
data = yaml.safe_load(f)
return data["a"], data["b"], data["c"]
def from_json(path):
with open(path, "r") as f:
data = json.load(f)
return data["a"], data["b"], data["c"]
if __name__ == "__main__":
a, b, c = from_file("abc.yml")
x1, x2 = solve(a, b, c)
print("Данные брались из YAML")
print("x1 =", x1)
print("x2 =", x2)
a, b, c = from_file("abc.json")
x1, x2 = solve(a, b, c)
print("Данные брались из JSON")
print("x1 =", x1)
print("x2 =", x2)
Осталось сделать пользовательский интерфейс, позволяющий вводить данные как через командную строку, так и через указание файла данных (YAML или JSON).
Интерфейс пользователя#
Пусть флаг -l
или --line
отвечает за задание коэффициентов через командную строку, а флаг -f
или --from-file
— за чтение данных из заданного файла.
Тогда интерфейс можно описать так:
if __name__ == "__main__":
parser = ArgumentParser(
"solve_quadric_eq",
description="Let you solve any quadric equation a*x^2 + b*x + c = 0"
)
parser.add_argument(
"-l",
"--line",
dest="line",
type=float,
nargs="?",
help="Input coefficients using command line"
)
parser.add_argument(
"-a",
type=float,
default=1,
help="The `a` coefficient (default 1)"
)
parser.add_argument(
"-b",
type=float,
default=0,
help="The `b` coefficient (default 0)"
)
parser.add_argument(
"-c",
type=float,
default=0,
help="The `c` coefficient (default 0)"
)
parser.add_argument(
"-f",
"--from-file",
dest="file_path",
help="Read coefficients from the data file (JSON or YAML)"
)
args = parser.parse_args()
if args.file_path is not None:
a, b, c = from_file(args.file_path)
elif "line" in args:
a = args.a
b = args.b
c = args.c
else:
raise ValueError("unknown command line format")
x1, x2 = solve(a, b, c)
print("x1 =", x1)
print("x2 =", x2)
Таким образом, сделали следующий интерфейс:
$ ./solve_quadric_eq.py -h
usage: solve_quadric_eq [-h] [-l [LINE]] [-a A] [-b B] [-c C] [-f FILE_PATH]
Let you solve any quadric equation a*x^2 + b*x + c = 0
options:
-h, --help show this help message and exit
-l [LINE], --line [LINE]
Input coefficients using command line
-a A The `a` coefficient (default 1)
-b B The `b` coefficient (default 0)
-c C The `c` coefficient (default 0)
-f FILE_PATH, --from-file FILE_PATH
Read coefficients from the data file (JSON or YAML)
Теперь можно вызывать наш решатель и передавать ему данные из файла YAML или JSON:
$ ./solve_quadric_eq.py -f abc.yml
x1 = -3.0
x2 = 1.0
$ ./solve_quadric_eq.py -f abc.json
x1 = (1-1.4142135623730951j)
x2 = (1+1.4142135623730951j)
Или через аргументы командной строки:
$ ./solve_quadric_eq.py -l -a 1 -b 2 -c -3
x1 = -3.0
x2 = 1.0
$ ./solve_quadric_eq.py -l -a 1 -b -2 -c 3
x1 = (1-1.4142135623730951j)
x2 = (1+1.4142135623730951j)
Но файлы можно не только читать. В них можно и записывать информацию.
Сохранение результата в файл#
Просто замените последние два вызова print
на что-либо из следующего:
запись в простой текстовый файл:
with open("roots.txt", "w") as output:
output.write(f"x1 = {x1}\nx2 = {x2}\n")
сохранение в YAML файл:
with open("roots.yml", "w") as output:
yaml.safe_dump(
{"x1": x1, "x2": x2},
output,
allow_unicode=True
)
сохранение в файл JSON:
with open("roots.json", "w") as output:
json.dump(
{"x1": x1, "x2": x2},
output
)
В результате вы получите файлы соответствующих форматов, в которых сохранены корни x1
и x2
.