Работа с конфигами в программах на Python

В старые добрые времена, когда в (не к ночи будь упомянуто!) Шиндовсе еще не был изобретен (прости-хоспади!) реестр, конфигурационные данные программ было принято сохранять в .INI-файлах. Формат этих файлов был весьма не притязательный, а поскольку файлы были текстовые, то в случае-чего их можно было легко поправить руками в любом текстовом редакторе. Кроме того, поскольку это были файлы (а не двоичные разделы реестра), то их можно было легко переносить с одного компа на другой. Давно канули влету те времена. Да и я тоже изменился — полностью и безвозвратно пересел на Линух. Но ностальгия по той простой и доступной технологии сохранения конфигурации программ каждый раз требует ренессанса. Поскольку я пишу (комповые) программы на Python-е, то речь пойдет об использовании технологии сохранения конфигурации в Python-программах. Кому интересно, прошу под кат!

0. Повторение забытого

Для начала давайте освежим знания относительно форматов конфигурационных файлов. В Виндовсе эти файлы обычно имели расширение имени .INI. В Linux тоже есть аналогичные файлы. Поскольку в Lnux-е нет такого понятия как расширение имени файла, а всё, что есть — это есть имя файла (ну, очень длинное имя!), то окончание имени (суффикс) таких конфигурационных файлов может быть любым, подходящим по смыслу. Например: .config, .conf, .cfg или .cnf. Линукс свои предположения по обработке тех или иных типов файлов строит не на основании расширения, а на основании содержимого этих файлов. Поэтому вы вольны давать имена файлам какие хотите! Как я уже поминал выше, конфигурационные файлы — это простые текстовые файлы — обычный плоский текст, без форматирования (шрифт жирный, наклонный, красным цветом и т.д.). Файлы можно быстро посмотреть в консоли с помощью команды cat и при необходимости быстро отредактировать любым текстовым редактором, например — vim, nano, mcedit, gedit, geany и т.д. Конфигурационные файлы имеют простую структуру. Данные разбиты на секции (разделы) и представляют собой пары: имя_параметра = значение. Например, секция, в которой определяется конфигурация последовательного порта:

[serial]
port = /dev/ttyS0
baud = 115200
bits = 8
parity = No
stopbits = 1
timeout = 0.5

Имя параметра и значение могут разделяться как знаком «=» (равно), так и знаком «:» (двоеточие). Секции отделяются друг от друга с помощью одной пустой строки. Кроме того, в файлы можно добавлять комментарии. Комментарии однострочные: начинаются со знака «#» (решётка) или «;» (точка с запятой) и заканчиваются символом новой строки. Вот пример конфига, в котором две секции:

[well]
# Скважина
firm = Институт Геофизики
number = 18
deep = 240   ; метров

[operator]
# Оператор, проводящий исследование
name = Крылов Сергей
mobile = 8 (922) 123-4567
email = sk@mail.ru

Я думаю, что по формату вопросов возникнуть не должно — формат очень простой! Задача конфигурационных файлов — хранить редко меняющиеся установки. Конфиги, если хотите, это комповые аналоги микросхем EEPROM. А наша (программистская) задача — уметь обращаться с данными в этих конфигах: читать, писать, изменять так, чтобы наша программа могла гибко подстраиваться под под нужную нам конфигурацию.

1. Импорт модуля

Для работы с такими конфигами в Python-е имеется модуль ConfigParser. (В третьей версии Python он называется configparser.) Модуль входит в стандартную комплектацию, то есть не требует дополнительных действий по установке. Обратите внимание на букву «s» в слове «parser»! Не знаю как вы, но меня постоянно сносит писать «parcer». Для работы с модулем нужно в начале программы его импортировать:

#!/usr/bin/env python
#coding:utf-8

import ConfigParser

2. Кафе «Три карася»

В состав модуля входят три класса для работы с конфигами и несколько классов для работы с ошибками, которые могут возникать в процессе работы. Работу с ошибками пока отложим, а вот классы для работы с конфигами рассмотрим более подробно. Все три класса находятся в тесном родственном отношении друг с другом. Нам нужно изучить работу только одного из них — базового класса, остальные два имеют несущественные отличия от него, а их методы почти полностью состоят из методов базового класса. Родительским (базовым) классом является класс RawConfigParser. От него растёт (наследует) класс ConfigParser, а из ConfigParser растёт SafeConfigParser. CP-3 По правилам ООП-графики стрелочки направлены от дочернего класса к родительскому.

3. Суть классов

Вся суть любого из этих трех классов сводится к тому, что конфигурация храниться в самом классе, то есть класс отвечает за конфигурацию нашей программы. Код нашей программы обращается к классу за тем или иным параметром, а задача класса сводится к выдаче запрошенного параметра. Ну и еще класс должен уметь сохранять конфигурацию в файле и уметь считывать ее обратно. То есть, все что нам нужно, — всё лежит в классе. CP-mate- Диаграмма не исчерпывает всех действий с классом. Она отражает только то, что класс находится по средине между файлом конфигурации и нашей программой. Принципиально лишь то, что наша программа не должна работать напрямую с файлом.

4. Методы класса для сохранения конфигурации в файле и её обратного считывания

Тут тоже ничего сложного нет. Важно лишь одно — прежде чем работать с классом, нужно создать объект класса:

conf = ConfigParser.RawConfigParser()

conf — это имя объекта класса. Это и есть наш конфиг. В принципе, совсем необязательно использовать именно это имя. Вы можете выбрать любое другое, даже сократить его до одной буквы, например «с». Далее, предположим, что у нас уже имеется конфигурационный файл. Пусть его имя будет carotage.conf. Тогда следующая команда произведет считывание информации в объект conf:

conf.read("carotage.conf")

И теперь, если в нашем файле имеется секция [operator] и в ней присутствует параметр name, как это было предложено выше, то следующая команда выведет на консоль строку Сергей Крылов:

print conf.get("operator", "name")

Сейчас важно обратить внимание на то, как нужно открывать файл конфигурации и считывать из него информацию. Про то, как и какими методами нужно получать значения параметров, — я расскажу чуть позже. Вот полный текст программы:

#!/usr/bin/env python
#coding:utf-8

import ConfigParser

conf = ConfigParser.RawConfigParser()
conf.read("carotage.conf")
print conf.get("operator", "name")

И я преднамеренно не оговариваю проблемы типа «а что будет, если окажется, что файл конфигурации отсутствует?», или «в файле нет такой секции», или «нет такого параметра». Об этом мы поговорим позже. Следующий момент — сохранение новой (или изменённой) информации файле. Допустим, в секции [operator] мы заменили имя оператора на Николай Шилко, а в секцию well добавили дату проведения работ. Вот этот код:

conf.set("operator", "name", "Николай Шилко")
conf.set("well", "date", "08.01.2015")

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

conf.write(open("carotage.conf", "w"))

, но можно и так:

with open("carotage.conf", "w") as config:
  conf.write(config)

Вот полный текст программы:

#!/usr/bin/env python
#coding:utf-8

import ConfigParser

conf = ConfigParser.RawConfigParser()

conf.read("carotage.conf")

conf.set("operator", "name", "Николай Шилко")
conf.set("well", "date", "08.01.2015")

with open("carotage.conf", "w") as config:
  conf.write(config)

Заметьте, что в тексте присутствует команда предварительного считывания файла конфигурации:

conf.read("carotage.conf")

Эта команда жизненно необходима! Не будь ее класс бы стер всё содержимое существующего файла и записал бы только два параметра. Всё правильно! Ведь класс не знает, нужно ли добавить/изменить содержимое файла или нужно создать файл заново. Всякий раз при сохранении конфигурации класс создает файл заново. Класс создает файл на основе той информации, и тех параметров и секций, которые присутствуют у него — смотри диаграмму выше. Но если уж говорить по правде, то без предварительного считывания файла первый же метод conf.set(…) вызовет исключение и дальнейшая работа программы будет откинута. Если предварительно не прочитать файл, то конфигурация будет девственно чистой — в конфигурации не будет ни одной секции. Чуть далее мы с вами затронем вопрос, что создавать/изменять параметры можно только в существующих секциях. Иначе говоря, для работы с параметрами секции должны уже быть! Секции можно либо создать с «чистого листа», либо считывать их из файла. В первом случае мы создаем нашу конфигурацию с нуля, а во втором случае — пользуемся готовой. В общем, когда нам нужно изменить конфигурацию, мы сначала должны считать её из файла, затем внести изменения в неё, а потом обратно сохранить её в файл. Пугаться того, что выполняется много работы по передаче большого объема данных не следует! Это раньше, во времена DOS запись или чтение одного мегабайта информации напрягали и программистов, и компьютеры. А сейчас эти же действия выполняются практически мгновенно, а расход памяти вы не сможете даже уловить! А тем более, что конфигурационные файлы очень редко превышают размеры 10 килобайт. Если размер конфига приближается к нескольким десяткам килобайт, то скорее всего вы используете не те технологии, или делаете немножко не то.

5. Методы работы с секциями

Если секция не создана, то бесполезно работать с её параметрами. Но если секция существует, то можно создавать новые и изменять старые параметры. Для создания новой секции существует метод add_section(section). Вот пример создания новой секции [curveinformation]:

conf.add_section("curveinformation")

Метод has_section(section) позволяет узнать — существует ли секция. Вот один из возможных вариантов использования этих двух методов:

if not conf.has_section("curveinformation"):
  conf.add_section("curveinformation")

Этот вариант, конечно, не совсем идеологически правильный с точки зрения ООП, но он точно так же чётко работает, как и идеологически правильный вариант с перехватом исключений, о котором, я мы поговорим чуть позже. Страшный метод remove_section(section) удалит секцию вместе со всем её содержимым (со всеми параметрами и комментариями). Пример применения приводить не буду, итак всё понятно! Еще один метод — sections(), этот метод позволяет получить список всех секций, которые имеются в конфиге. В списке будут перечислены только наименования секций, но не их содержимое. Вот, собственно, это всё, что нам необходимо для работы с секциями.

6. Методы работы с параметрами

Теперь самая интересная часть — то, ради чего всё это затевалось. Я буду исходить из того, что нужная нам для работы секция уже существует. В противном случае будут возникать исключения. С методом создания нового параметра или изменения значения у существующего вы уже знакомы — set(section, option, value). Пример приводить не буду, смотрите код выше. Метод получения значения заданного параметра мы тоже уже затрагивали — get(section, option). Я лишь хочу заострить ваше внимание вот на каком аспекте. По сути, конфиги (и сами файлы конфигурации) — это текстовая информация. Я хочу задать вам вопрос — понимаете ли вы разницу между числом 123 и строкой «123»? Числа можно складывать. Например, 123 + 123 даст новое число 246. А если мы сложим строку «123» с ней же самой, то получим новую строку «123123». Поскольку конфиги — это текст, то все числа в них хранятся в текстовом виде. Иначе говоря, чисто 123 хранится в виде строки «123». И если мы сложим значение параметра с ним же самим, то мы получим строку «123123», а не число 246. Улавливаете проблему? — Представление числа 123 в конфиге и представление строки «123» выглядят абсолютно одинаково! Как же их тогда различать!? К счастью, класс предоставляет нам несколько методов для работы с разными типами параметров. Допустим, в нашем конфиге имеется секция [data] с двумя параметрами:

[data]
dozen = 12
pi = 3.14

Тогда в результате применения методов get(«data», «dozen») и get(«data», «pi») мы получим строки «12» и «3.14», соответственно. Но нам нужно получить не строки, а числа, с которыми можно выполнять математические действия. Чтобы получить числа, мы должны использовать методы getint(«data», «dozen») и getfloat(«data», «pi»). Только в этом случае мы получим в качестве значений параметров числа. В принципе, можно получить сначала строки, а затем вызвать штатные Питоновские функции по конвертации полученных текстовых значений в числа — int() и float(). Никто не запрещает. Но зачем это делать, если есть готовый к использованию сервис? На этом конвертация текста в другие виды не исчерпывается. В программах очень широко используются логические переменные. Как вы уже наверно догадались, для конвертации в логические значения применяется метод getbool(section, option). Забавно отметить, что этот метод «всеядный», Он легко переваривает такие строки как «true», «yes», «on», «1» в логическую Истину, а строки «false», «no», «off». «0» — в логическю Ложь. Метод has_option(section, option) позволяет узнать существует ли в конфиге заданный параметр. Метод похож на метод проверки существования заданной секции и в принципе может использоваться точно так же:

if conf.has_option("data", "pi"):
  pi = conf.getfloat("data", "pi")

Метод remove_option(section, option) удалят указанный параметр. Метод options(section) позволяет получить список параметров указанной секции. Наверно бывает это полезно, когда приходится перебирать все параметры секции. А вот метод items(section) возвращает словарь, состоящий из названий параметров и их значений. Его применение наверно тоже где-то полезно. Добавлю еще пару слов относительно того, как писать наименования секций и имена параметров. Я не могу охватить все случаи жизни — специфику использования Питона в разных операционных системах. Я скажу лишь про Линукс. Имена секций чувствительны к регистру. То есть секции [person], [Person] и [PERSON] — это три разные секции, которые могут находится в одном конфиге не вступая в конфликт друг с другом. А вот имена параметров name и Name в секции выливаются в один и тот же параметр. Более того, как бы вы не старались, класс преобразует имя параметра в нижний регистр, и, таким образом, в конфигурационном файле вы всегда будет получать имя name (всё буквы строчные, в нижнем регистре) вне зависимости от того, в каком регистре вы напишите это имя в методе set(section, option, value). Вообще у класса есть метод optionxform(option), через который все имена параметров внутри класса передаются на обработку.

7. Обработка ошибок/исключений

Честно говоря, исключения в ООП — это большая самостоятельная тема. Применительно к конфигам ее сложно как-то доходчиво описать. Суть исключений состоит в том, что научиться их правильно перехватывать и обрабатывать. Опираясь на технологию исключений вы не должны писать программу так, как это принято в процедурных алгоритмических языках типа Си. В этих языках прежде чем открыть файл нужно убедиться, что он существует. Прежде чем записать файл нужно убедиться, что запись не вызовет ошибок. Ну и тому подобные вещи. В ООП принято писать программы так, как будто весь мир идеален. Файл существует. Файл доступен для записи. И так далее. А в случае если что-то пойдет не так, то в программе возникает исключение. Выполнение программы будет прервано, и управление будет передано на первый ближайший оператор перехвата исключения. Но исключение исключению — рознь! Откуда нам заранее знать почему возникло прерывание — то ли это было отсутствие секции, то ли отсутствие параметра? На каждый «чих» будет вызван соответствующий тип исключения, который передается в виде параметра в операторе except. Если нет желания, то можно не «сортировать» эти исключения, и обрабатывать их все единообразно. А может на каждый «чих» реагировать индивидуально. Так вот, относительно «чихов» на тему ошибок в конфигах — там примерно с десяток разных типов исключений. Можете сами почитать документашку. А можете поступить так же, как делаю я. Я просто пишу прогу, а потом тестирую ее на те или иные проблемы. Когда случается исключений, прога вываливается в консоль, где кратко описывается место и причина возникновения исключения. Тогда я охватываю «проблемный» участок программы операторами try и exception и вылавливаю это исключение. Я знаю, что это несколько не научно. Зато с практической точки зрения можно получить колоссальный опыт.

8. Так в чем же состоит различие трех классов для работы с конфигами?

Всё было бы хорошо, если бы конфиги не позволяли производить подстановку параметров. Ну или наоборот, всё было бы плохо, если бы … Я пока не нашел острой необходимости применения подстановок, поэтому я использую класс RawConfigParser. Подстановки в конфигах — это более тяжелый материал. Поскольку я до конца не понял назначение этих подстановок, и не могу красиво, а тем более, — правильно рассказать вам о них, то я вынужден отправить вас в Интернет. Я воспринимаю подстановки как забавную, но абсолютно ненужную причуду. Я считаю, что подстановки только запутывают код и вносят в алгоритм много сложностей. Зачем? Будь проще, и к тебе потянуться люди! (с)

Advertisements

2 responses to “Работа с конфигами в программах на Python

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

    • Да. Всё это так. Но есть ещё один аспект.

      Дело в том, что парадигма UNIX/Linux-систем требует хранить исполняемые модули (программы) в одном месте, а настройки пользователя в другом.

      Место, где находятся программы, защищено от изменений пользователями с обычными правами. Программы может изменять (удалять, устанавливать, …) только root. Программы обычно находятся в директориях /usr/bin, /usr/local/bin и других.

      А вот наличие возможности изменять настройки программы под свои предпочтения — это как раз прерогатива пользователя. Поэтому конфиги хранятся в домашнем директории пользователя.

      Таким образом, максимум плохого, что может сотворить обычный пользователь — это «изуродовать» только СВОЮ конфигурацию программы. И есть это всё-таки случилось, то с моей стороны последует стандартная рекомендация — удалить файл настроек программы. И тогда программа примет конфигурацию по умолчанию.

      Ну и кроме того, я предполагаю, что в настройки программы (в конфиги) полезут пользователи, которые не очень-то знают Python. Ну, по крайней мере у меня на работе так. Моим пользователям проще освоить синтаксис конфига, чем хотя бы часть языка программирования Python.

      Где и как хранить конфигурацию программы — это очень неоднозначный вопрос. Возьмите для антипримера Виндовс! Многие программы хранят свои настройки в Реестре. Другие программы хранят настройки в отдельном файле вообще рядом с ЕХЕ-шкой — /Program Files/Firma/Proga или вообще в корне файловой системы. (А что, система ж позволяет!). Третьи программы — хранят настройки в папке пользователя. Короче — зоопарк и проблемы!

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s