Вводные замечания о форматах конфигурационных файлов
Обычно, когда речь заходит о конфигурационных файлах, то на память приходят статичные текстовые файлы, содержащие описание различных настроек системы и выступающие главным образом в роли малозначительного элемента инфраструктуры кода. Такого рода файлы несложно превратить в динамический элемент инфраструктуры, который позволит управлять логикой работы приложения без необходимости вносить изменения в программный код.
К наиболее распространенным форматам конфигурационных файлов, которые находят применение в контексте управления приложениями на Python, можно отнести INI, JSON, TOML и YAML.
INI – самый простой формат из рассмотренных. С одной стороны чем проще читать конфигурационный файл, тем лучше, но с другой – файлы *.INI могут оперировать только одномерными структурами, т.е. структурами с простой одноуровневой иерархией. В большинстве прикладных задач, когда приходится иметь дело с моделями объектов, допускающих представление в виде списков, ассоциативных массивов и т.п., возможностей INI оказывается недостаточно.
JSON-файл выглядит как обычный словарь Python и может включать сложные иерархические зависимости, однако с точки зрения читаемости проигрывает и YAML, и TOML. Кроме того, JSON не поддерживает комментарии, а они часто могут значительно упростить сопровождение кода.
В отличие от предыдущих, формат TOML обладает несоизмеримой гибкостью и широтой арсенала поддерживаемых типов данных. TOML поддерживает простые пары «ключ-значение», массивы, классические и встроенные таблицы, массивы таблиц, булевы значения, а также локальные временные метки и временные метки со смещением.
Для сравнения рассмотрим одну и ту же модель объекта, описанного с помощью TOML и JSON.
Вот TOML-представление модели объекта:
view_obj.toml
[[fruits]] name = "apple" [fruits.physical] # подтаблица color = "red" shape = "round" [[fruits.varieties]] # вложенный массив таблиц name = "red delicious" [[fruits.varieties]] name = "granny smith" [[fruits]] name = "banana" [[fruits.varieties]] name = "plantain"
А вот JSON-представление:
view_obj.json
{ "fruits": [ { "name": "apple", "physical": { "color": "red", "shape": "round" }, "varieties": [ { "name": "red delicious" }, { "name": "granny smith" } ] }, { "name": "banana", "varieties": [ { "name": "plantain" } ] } ] }
Синтаксические особенности JSON – избыточные фигурные и квадратные скобки – делают сложноструктурные JSON-файлы «размазанными».
Формат YAML обладает схожими с форматом TOML возможностями (в смысле гибкости представления моделей объектов и разнообразия поддерживаемых типов данных), но на сложных структурах выглядит компактнее.
Вопрос выбора формата конфигурационного файла зависит больше от сложности описания модели объекта, но, в большинстве случаев для задач, связанных с управлением Python-приложениями, выбор падает на YAML.
Python-библиотеки для работы с конфигурационными файлами
Из всего многообразия предназначенных для работы с конфигурационными файлами библиотек – ориентированных на какой-то конкретный формат или «всеядных» – можно выделить следующие:
configparser: это элемент стандартной библиотеки Python, предназначенный для работы с INI-файлами Microsoft Windows. В распоряжении пользователя есть класс ConfigParser, который реализует базовые возможности библиотеки.
PyYAML: элемент не входит в стандартную библиотеку, поэтому его нужно установить с помощью менеджера пакетов pip pip install pyyaml
. В Python-сценарии обращение к библиотеке выглядит как import yaml
.
toml: этот элемент тоже представляет собой стороннее решение для работы с форматом TOML и требует установки с помощью pip install toml
.
dynaconf: это очень гибкая библиотека, которая 1) позволяет работать со всеми популярными форматами конфигурационных файлов (*.toml, *.yaml, *.json, *.ini, *.py), 2) поддерживает мультипрофили, т.е. конфигурационный файл может содержать несколько заголовков, относящихся к различным стадиям готовности программного продукта (например, default, development, production и т.д.), а нужный набор настроек затем вызывается с указанием соответствующего заголовка, 3) умеет работать с переменными окружения («работает из коробки» с библиотекой dotenv), 4) предлагает утилиту командной строки для выполнения операций общего назначения (init, list, write, validate и т.д.). Устанавливается библиотека с помощью менеджера пакетов pip pip install dynaconf
.
hydra: это, строго говоря, не просто библиотека, а полноценная платформа, предназначенная для решения широкого круга задач, связанных с конфигурацией сложных приложений. Установить hydra можно либо с помощью менеджера пакетов pip pip install hydra-core --upgrade
, либо с помощью менеджера пакетов conda conda install -c conda-forge hydra-core
.
В основном, выбор библиотеки определяется следующими аспектами:
сложностью задачи и ее особенностями. К примеру, конфигурация маршрута подготовки моделей машинного обучения для развертывания на облачной платформе может быть выполнена и с помощью PyYAML/toml, а вот управление сложным web-проектом скорее всего потребует продвинутых возможностей dynaconf или hydra;
требованиями к показателю переиспользования кода. С этой точки зрения преимущества на стороне библиотеки hydra;
гибкостью решения и одновременно простотой сопровождения кода. Здесь чаще используются библиотеки PyYAML и toml.
Обобщая сказанное выше и учитывая класс задач, которые призваны решать конфигурационные файлы в контексте управления Python-приложениями, далее будем использовать YAML-файлы и библиотеку PyYAML.
Несколько слов о синтаксисе YAML
Синтаксис YAML прост и лаконичен, но есть несколько особенностей. Практически каждый YAML-файл строится на базе списка. Каждый элемент списка это список пар «ключ-значение», который обычно называют словарем. То есть представление модели объекта с помощью YAML сводится к тому, чтобы описать эту модель в терминах списков и словарей.
Иногда YAML-файлы начинаются с тройного тире ---
и заканчиваются троеточием ...
, а иногда эти последовательности символов в начале и в конце файла опускают. PyYAML корректно работает в обоих случаях.
Все элементы списка располагаются на одном и том же уровне и начинаются с -
(тире и пробел):
start_end.yaml
--- # Начало файла # Список фруктов - Apple - Orange - Strawberry - Mango ... # Конец файла
Словари YAML представляют собой, как в и Python, наборы пар «ключ-значение» (за двоеточием должен следовать пробел):
dict.yaml
# Запись о сотруднике martin: name: Martin Johnson job: Developer skill: Elite
YAML поддерживает стандартные типы данных: целочисленный (int), вещественный (float), булев (boolean), строковый (string) и null:
types.yaml
integer: 25 float: 25.0 exponent: 12.3015e+05 boolean: Yes string: "25" infinity: .inf # бесконечность neginf: -.inf # минус бесконечность
При необходимости можно явно указывать тип данных значения с помощью конструкции !![тип данных]
, например:
flag: !!bool false
и т.д.
Булевы значения, могут иметь разные варианты записи, но рекомендуется использовать true
и false
, чтобы YAML-файл был совместим с настройками по умолчанию большинства YAML-линтеров:
supported_bools.yaml
create_key: yes needs_agent: no knows_oop: True likes_emacs: TRUE uses_cvs: false
В YAML необязательно заключать строковые константы в кавычки, но в ситуациях, когда требуется явно подчеркнуть строковую природу значения, кавычки не помешают. Допускается использовать как одинарные, так и двойные кавычки. Единственное отличие заключается в том, что двойные кавычки разрешают использовать управляющие коды строковых констант – их еще называют экранированными последовательностями – t
(символ горизонтальной табуляции), r
(символ возврата каретки), n
(символ перехода на новую строку) и т.д.
Для работы с длинными строками используют символы |
и >
multilines_and_one_long_line.yaml
include_newlines: | это действительно три отдельные строки fold_newlines: > а это на самом деле одна длинная строка, которая выглядит как три отдельные
Бывает, что отдельные фрагменты YAML-файла требуется повторить несколько раз. Для решения такого рода задач используются якоря &
и псевдонимы *
. Пример:
anchor_alias.yaml
hello: &hi 'hello' # вводится якорь hi greeting: audience: 'world' hello: *hi # применение якоря; hello получит строку 'hello'
Псевдонимы можно задавать и для блоков:
anchor_for_block.yaml
foo: bar: &bar # псевдоним для блока qux: 'quxqux' baz: 'bazbaz' greeting: audience: 'world' bar: *bar # *bar развернется в словарь с ключами qux и baz
С помощью ключа слияния <<:
можно «наследовать» и переопределять разделы:
merge_key.yaml
bar: &bar # вводится псевдоним блока qux: 'quxqux' baz: 'bazbaz' greeting: audience: 'world' bar: <<: *bar # *bar развернется в словарь qux и baz baz: 'notbaz' # НО baz перезапишется новым значением notbaz
Для редактирования YAML-файлов подойдет любой текстовый редактор, но важен следующий момент. Не всегда есть возможность использовать современный редактор с красивым графическим интерфейсом пользователя, а вот текстовый редактор Vim предустановлен на большинстве UNIX-подобных операционных систем.
Порог входа высок, но за счет огромного количества плагинов и продуманной концепции редактора эффективность разработки увеличивается многократно. Детали работы с редактором можно узнать, например, из книгиНейла, Дрю «Практическое использование Vim».
Проверка типов управляющих параметров конфигурационного файла
В простых сценариях использования, когда можно ограничиться контролем типа данных на уровне конфигурационного файла, будет достаточно использовать явное указание типа с помощью !![тип данных]
.
В случае сложной структуры модели объекта и в случае сценария, когда приложение должно уметь работать не только с конфигурационными файлами, подготовленными самим разработчиком, но и со сторонними конфигурационными файлами, с точки зрения устойчивости имеет смысл перенести проверку типов на уровень логики приложения.
Проверку типов можно организовать с помощью стандартной библиотеки Python dataclasses и сторонней библиотеки marshmallow_dataclass.
Рассмотрим простой пример использования marshmallow_dataclass, заимствованный со страницы проекта:
simple_example_marshmallow.py
from dataclasses import dataclass, field from typing import List, Optional import marshmallow_dataclass import marshmallow.validate @dataclass class Building: height: float = field( metadata = { "validate": marshmallow.validate.Range(min=0) } ) name: str = field(default="anonymous") @dataclass class City: name: Optional[str] buildings: List[Building] = field(default_factory=list) # создаем экземпляр схемы city_schema = marshmallow_dataclass.class_schema(City)() # city_schema => <City(many=False)> # загружаем словарь city = city_schema.load( { "name": "Paris", "buildings": [{ "name": "Eiffel Tower", "height": 324 }] } ) # city => City( name='Paris', buildings=[ Building( height=324.0, name='Eiffel Tower' ) ] ) city.buildings # => [Building(height=324.0, name='Eiffel Tower')] city.buildings[0].height # 324.0
Здесь класс Building
содержит всего два атрибута: вещественный height
и строковый name
. С помощью метода marshmallow.validate
выполняется проверка на минимальное значение атрибута height
, а с помощью field
атрибут name
получает значение по умолчанию.
Класс City
содержит опциональный атрибут name
, который ожидает получить либо объект строкового типа, либо None
. Атрибут buildings
ожидает принять список объектов типа Building
.
Далее создаем экземпляр схемы city_schema
на базе объекта класса City
, а затем загружаем словарь. Схема ожидает получить строку для атрибута name
и список объектов Building
, то есть список объектов, которые описываются строковым атрибутом name
и вещественными атрибутом height
.
Используя точечную нотацию и нотацию списков можно добраться до значений атрибутов.
Шаблон применения конфигурационных файлов как инструмента управления Python-приложениями
Разобравшись с основными понятиями и концепциями, перейдем к рассмотрению связки «конфигурационный файл + Python-приложение».
В самом простом случае связка состоит из одного конфигурационного файла и одного модуля Python. В общем случае для управления Python-приложением может использоваться несколько файлов конфигурации (различных форматов), а базовая логика приложения опирается на наборов модулей и пакетов.
Сильная сторона такого представления задачи заключается в возможности явным образом декомпозировать приложение на:
изменяющийся блок управляющих параметров, или другими словами на динамическую часть инфраструктуры кода, с которой разработчик будет взаимодействовать в будущем,
неизменяющийся блок базовой логики – статичную часть кода, которая не требует внесения изменений в программный код напрямую.
Высокоуровневая схема взаимодействия конфигурационного файла с Python-приложением
Упрощенно, шаблон использования конфигурационных файлов как текстового интерфейса к Python-приложению выглядит так: управляющие параметры выносятся в конфигурационный файл, а базовая логика описывается в Python-модуле.
Этот шаблон распространяется на приложения произвольной сложности, но у него есть естественные ограничения.
Если конфигурационный файл разрастается до размеров исключающих быстрый поиск нужных управляющих параметров или затрудняет реализацию сложной логики управления, то стоит задуматься о более эффективных приемах организации работы.
В этом вопросе могут помочь:
DearPyGui: библиотека для разработки настольных приложений с графическим интерфейсом пользователя;
Streamlit: мощная библиотека для прототипирования браузерных решений с графическим интерфейсом.
Важно понимать, что рассмотренный шаблон не может покрыть всех задач по-настоящему сложных приложений (например, с микросервисной архитектурой), но потенциально способен «закрыть» какое-то подмножество этих задач в пределах отдельно взятого микросервиса.
Пример связки «конфигурационный YAML-файл + Python-приложение»
В качестве примера возьмем задачу обнаружения выбросов в дискретной реализации стационарного гауссовского случайного процесса с различными типами корреляционных функций и ограничимся рассмотрением следующих их типов: экспоненциального, экспоненциально-косинусного, экспоненциально-косинусно-синусного (с плюсом) и экспоненциально-косинусно-синусного (с минусом).
Для простоты гипотеза о выбросах проверяется с помощью классической стандартизованной Z-оценки и модифицированной робастной Z-оценки (англ.) на медиане. Для моделирования псевдослучайных процессов с гауссовским распределением ординат и корреляционными функциями заданного типа использовались алгоритмы, заимствованные из книги Быкова, В.В. «Цифровое моделирование в статистической радиотехнике».
Полный код примера с пояснениями и деталями реализации доступен в github-репозитории.
Вот его структура:
struct_repo.md
. configs/ `- gauss_processes_acf.yaml figure_exapmples/ `- gauss_exp_acf.pdf `- ... figures/ `- *.pdf python_scripts `- main.py `- helper_funcs_and_class_schema.py README.md
В каталоге configs
располагается шаблон конфигурационного файла, который можно использовать в качестве отправной точки для разработки своих собственных более сложных конфигураций приложения.
gauss_proc_acf.yaml
--- # *** MENU *** # Types of ACF (for `kind_acf`): # 1: exp type, # 2: exp-cos type, # 3: exp-cos-sin type (plus), # 4: exp-cos-sin type (minus) # *** MAIN CONTROL PARAMETERS *** sigma: !!float 2 # standard deviation w_star: !!float 1.25 # model parameter w0: !!float 2.5 # model parameter alpha: !!float 0.05 # smoothing factor for EWMA window_width: !!int 14 # width of window in MA delta_t: !!float 0.05 # time step N: !!int 1000 # number of counts kind_acf: !!int 1 # ACF type # *** OTHERS APP SETTINGS *** visibility: ma_show: !!bool true colors: white: !!str &white "#FDFDFD" grey: !!str &grey "#52616B" blue_purple: !!str &blue_purple "#8785A2" terakotta: !!str &terakotta "#E84A5F" pearl_night: !!str &pearl_night "#112D4E" krayola_green: !!str &krayola_green "#1CAC78" figure_settings: height_main_fig: !!int 7 width_main_fig: !!int 18 left_xlim_acf: !!float 0. right_xlim_acf: !!float 4. left_xlim_pdf: !!float -9 right_xlim_pdf: !!float 9 ...
В figure_examples
хранятся демонстрационные примеры работы сценария python_scripts/main.py
, а прямые результаты его работы в – каталоге figures
.
Управлять типом корреляционной функции процесса можно с помощью параметра kind_acf
. Смысл остальных управляющих параметров (w_star
, w0
, alpha
и т.д.) должен быть понятен из комментариев.
Для запуска приложения следует перейти в корневой каталог проекта и выполнить:
start_app.sh
$ python python_scripts/main.py --config-path configs/gauss_processes_acf.yaml --output-fig-path figures/your_output_image_name.pdf
Функция cmd_line_parser
из модуля helper_funcs_and_class_schema.py
прочитает значения флагов (--config-path
, --output-fig-path
), а затем вернет путь до конфигурационного файла и путь до файла с результатами анализа псевдослучайного процесса.
Затем, функция read_yaml_file
прочитает конфигурационный файл и проверит типы управляющих параметров. Если типы управляющих параметров корректны и ошибок нет, то функция вернет объект типа Params
, к атрибутам которого можно будет обратиться с помощью точечной нотации. Остается только построить сводку с использованием функции draw_graph
.
Повысить эффективность работы с конфигурационными YAML-файлами можно с помощью различных утилит (например, с помощью легковесной yq), но потоковый редактор sed, как правило, «из коробки» доступен на большинстве операционных систем.
К примеру, изменить значение управляющего параметра, даже не открывая конфигурационный файл, а затем создать на базе этого конфигурационного файла новый можно так
sed_example.sh
$ sed 's/w0: !!float 3.0/w0: !!float 3.15' configs/gauss_processes_acf.yaml > configs/gauss_expacf_w0=3.15.yaml
Здесь sed ищет строку «w0: !!float 3.0
», заменяет ее строкой «w0: !!float 3.15
» и записывает результат (>
) в новый конфигурационный файл.
На рисунках ниже приведены результаты работы Python-приложения с различными комбинациями управляющих параметров.
Сводка по анализу выбросов стационарного гауссовского псевдослучайного процесса с корреляционной функцией экспоненциального типа
Сводка по анализу выбросов стационарного гауссовского псевдослучайного процесса с корреляционной функцией экспоненциально-косинусного типа
Сводка по анализу выбросов стационарного гауссовского псевдослучайного процесса с корреляционной функцией экспоненциально-косинусно-синусного типа (минус)
Сводка по анализу выбросов стационарного гауссовского псевдослучайного процесса с корреляционной функцией экспоненциально-косинусно-синусного типа (плюс)
Заключение
Из руководства вы узнали:
какие из существующих форматов конфигурационных файлов могут пригодится в роли текстового интерфейса к Python-приложениям;
какие существуют библиотеки для работы с конфигурационными файлами и где проходит граница области применимости каждой из них;
как использовать выразительный синтаксис YAML-файлов – якоря, псевдонимы, ключи слияния – для упрощения описания модели объектов;
в каких сценариях имеет смысл использовать связку «конфигурационный файл + Python-приложение» и в чем сильная сторона такого подхода;
как может выглядеть пример использования текстовых интерфейсов в задаче обнаружения выбросов;
а также, как «винтажные» инструменты – Vim и sed – могут повысить эффективность редактирования текстовых файлов.
Полезные источники:
From Novice to Expert: How to Write a Configuration file in Python
YAML Syntax with Ansible
Наглядно о том, как Vim рвёт в щепки Sublime, Atom, PyCharm
Кобзарь, А.И. Прикладная математическая статистика, 2012
Templating YAML in Kubernetes with real code
Github-репозиторий с кодом рассмотренного примера о выбросах случайного гауссовского процесса
Связные материалы с платформы Proglib:
Все, что нужно знать о декораторах
Преобразования Фурье для обработки сигналов с помощью Python