Доступна библиотека urm.py с реализацией URM (UnRelational Mapper) для языка Python. Проект может оказаться полезным, когда требуется сохранить какие-нибудь данные не в реляционной базе данных, а в нереляционном хранилище, таком как файловая система, архив, облачное хранилище, NoSQL-база.
Примеры библиотек, в которых необходимы подобные манипуляции:
- Библиотека UniGrammarRuntime должна уметь читать с диска несколько вариантов (сгенерированных разными генераторами, поддержка которых реализована в UniGrammar в виде бэкендов) парсера для одной и той же грамматики. Парсер может состоять либо из одного файла (файл грамматики в DSL-виде, для parsimonious и tatsu), либо из нескольких (лексер + парсер в транспилированном виде (ANTLR4, CoCo/Py), или 1 файл с транспилированным кодом + 1 файл с байт-кодом (parglare), или, если грамматика модульная, то все модули (пока не реализовано в UniGrammar), причём раскладка по диску должна быть такова, чтобы корректно импортировалось ). Также желательно хранить результаты измерений их производительности. Пример 1, пример 2.
- В артефактах библиотеки AutoXGBoost необходимо хранить пары файлов, один из которых – модель в одном из поддерживаемых форматов (а их на данный момент 3 – бинарный, транспилированный в AST python с помощью pyxgboost (можно проводить inference без установленного xgboost) и json), а второй – информация о колонках, необходимая для правильного проведения inference и прочие метаданные. И ещё есть файл с гиперпараметрами, который имеет решающее значение при повторной тренировке модели. Ещё планируется добавить туда LightGBM и CatBoost и конвертацию между форматами моделей.
В описанных примерах есть общее:
- Данные имеют смысл как файлы. Их можно вырвать из контейнера и использовать без него. Но это будет неудобно – контейнер предоставляет дополнительные данные для унифицирующей обвязки.
- Для гибкости требуется хранить дополнительную информацию. Эта информация должна быть связана с файлами, а хранилище у нас нереляционное.
- Дополнительную информацию приходится серилизовывать. Нет бесплатного лэнча, есть разные форматы сериализации с разными достоинствами и недостатками. Например YAML красиво выглядит, JSON компактнее и при этом совместимее, бинарные форматы по типу JSON ещё компактнее и не нуждаются в “спасении” строк. Нужно иметь свободу выбирать тот формат, какой наиболее подходит для конкретной задачи. И легко переключать, при изменении требований.
- Не всегда можно контролировать раскладку файлов. В архитектуру сторонних программ могут быть встроены предположения о том, как файлы лежат на диске. Например если файлы – модули какого-либо ЯП, то импортируемые модули должны лежать относительно импортирующих по известным путям. Можно извратиться с загрузчиками, это приведёт к проблемам (для отладки которых пришлось даже разработать FrozenTable).
Соответственно, имеется 3 критерия:
- явно-неявно – понятность при взгляде на код, возможность наличия не очевидного с первого взгляда для человека не знакомого с работой системы поведения и необходимость разбираться в системе;
- легко-трудоёмко – затраты труда и времени на написание и сопровождение;
- гибко-негибко – будем ли реализовывать всё многообразие.
с предпочтением “явно, легко, гибко”, и треугольник “выбери два”:
- явно, легко, негибко – выбрать и закодить 1 вариант вручную полностью;
- явно, трудоёмко, гибко – закодить всё вручную, а потом удалить весь этот код, потому что поддерживать и отлаживать такое нет никакого желания;
- неявно, легко, гибко – использовать композицию.
В некоторых случаях может быть целесообразно выбрать третий вариант, особенно если кодить его вручную не надо, а нужно только освоить готовую библиотеку – urm.py. urm – значит UnRelational Mapper, созвучно с ORM.
Сначала определимся с хранением и сериализацией. И то, и другое заэнкапсулировано в библиотеку. Всё тривиально: есть интерфейс и несколько классов, один метод разжимает и парсит, другой сериализует и жмёт, и можно объединять в конвейеры “разжать из LZMA2 -> декодировать в UTF-8 -> распарсить JSON” (и в обратную сторону тоже работает). Кроме urm эта библиотека используется в Cache.py.
Набор частей данных, связанных друг с другом, хранимый в хранилище, назовём bundle (“свёрток”). Ему соответствует класс ProtoBundle (который основан на метаклассе, создающем нужные дескрипторы), от которого нужно унаследовать свой класс. А в нём есть дескрипторы, получая и устанавливая значения в которые мы читаем/пишем в хранилище.
Хранилищ 2 вида, “горячие” и “холодные”, холодные для хранения, горячие – для кеширования. Пример холодного хранилища: файл на диске, хранящий данные, сериализованные конкретной схемой сериализации. Пример горячего хранилища – префиксное дерево.
2 вида свойств: 0-мерные (скалярные) и многомерные. К скалярным обращаться нужно через само свойство, к многомерным – через оператор [].
Между дескриптором, свёртком и хранилищами у нас располагается стратегия. Стратегии две:
- холодная – читает и пишет в холодное хранилище незамедлительно;
- кешированная – читает из кеша, если кеш пуст – из холодного хранилища, при установке значения пишет в кеш, запись в холодное хранилище производится только тогда, когда вызывается ‘save’ у “свёртка”. Есть возможность вызвать ‘save’ для отдельного свойства.
Стратегии параметризуются просто отображателями (Mapper). Просто отображатели бывают холодными (ColdMapper) и горячими (HotMapper). Каждый отображатель – это как минимум пара связанных объектов:
- отображатель ключей, преобразующий “ключи”, родные для холодного хранилища, в ключи, удобные для доступа в приложении (используются в горячем хранилище).
- хранитель, сохраняющий данные в холодные и горячие хранилища.
Холодный отображатель получает ещё конвейер преобразователей. Горячий, соответственно, не получает, так как горячее хранит данные в формате, удобном для хранения.
Для выражения отношений между хранимыми сущностями через Dynamic задаётся путь по именам свойств с корнем в объекте-“свёртке”. В остальных классах присутствует логика, проверяющая, является ли параметр или его компонент Dynamic, и если является – проходящая по указанному в нём пути и использующее полученное значение.