Опубликована библиотека urm для Python

Доступна библиотека 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, и если является — проходящая по указанному в нём пути и использующее полученное значение.

Источник: http://www.opennet.ru/opennews/art.shtml?num=54400