Преобразование двунаправленной структуры данных в Python

Примечание: это не простая двухсторонняя карта; преобразование является важной частью.

Я пишу приложение, которое будет отправлять и получать сообщения с определенной структурой, которые я должен преобразовать из и во внутреннюю структуру.

Например, сообщение:

{
    "Person": {
        "name": {
            "first": "John",
            "last": "Smith"
        }
    },
    "birth_date": "1997.01.12",
    "points": "330"
}

Это необходимо преобразовать в:

{ 
    "Person": {
        "firstname": "John",
        "lastname": "Smith",
        "birth": datetime.date(1997, 1, 12),
        "points": 330
    }
}

И наоборот.

Эти сообщения содержат много информации, поэтому я хочу избежать необходимости вручную записывать конвертеры для обоих направлений. Есть ли способ в Python указать отображение один раз и использовать его для обоих случаев?

В своем исследовании я нашел интересную библиотеку Haskell под названием JsonGrammar, которая позволяет это (это для JSON, но это не имеет значения для случая). Но мои знания о Haskell недостаточно хороши, чтобы попробовать порт.

Ответ 1

Это на самом деле довольно интересная проблема. Вы можете определить список преобразований, например, в форме (key1, func_1to2, key2, func_2to1) или в аналогичном формате, где key может содержать разделители для указания разных уровней dict, например "Person.name.first".

noop = lambda x: x
relations = [("Person.name.first", noop, "Person.firstname", noop),
             ("Person.name.last", noop, "Person.lastname", noop),
             ("birth_date", lambda s: datetime.date(*map(int, s.split("."))),
              "Person.birth", lambda d: d.strftime("%Y.%m.%d")),
             ("points", int, "Person.points", str)]

Затем повторяйте элементы в этом списке и преобразуйте записи в словаре в соответствии с тем, хотите ли вы перейти от формы A к B или наоборот. Вам также понадобится вспомогательная функция для доступа к ключам в вложенных словарях с помощью этих разделенных точками ключей.

def deep_get(d, key):
    for k in key.split("."):
        d = d[k]
    return d

def deep_set(d, key, val):
    *first, last = key.split(".")
    for k in first:
        d = d.setdefault(k, {})
    d[last] = val

def convert(d, mapping, atob):
    res = {}
    for a, x, b, y in mapping:
        a, b, f = (a, b, x) if atob else (b, a, y)
        deep_set(res, b, f(deep_get(d, a)))
    return res

Пример:

>>> d1 = {"Person": { "name": { "first": "John", "last": "Smith" } },
...       "birth_date": "1997.01.12",
...       "points": "330" }
...
>>> print(convert(d1, relations, True))    
{'Person': {'birth': datetime.date(1997, 1, 12),
            'firstname': 'John',
            'lastname': 'Smith',
            'points': 330}}

Ответ 2

Тобиас ответил на это довольно хорошо. Если вы ищете динамическую модель, которая обеспечивает динамическое преобразование модели, вы можете изучить Python Model transform library PyEcore.

PyEcore позволяет обрабатывать модели и метамодели (структурированную модель данных) и дает ключ, необходимый для создания инструментов на основе ModelDrivenEngineering и других приложений на основе структурированной модели данных. Он поддерживает "готовые":

Наследование данных, управление двумя способами (противоположные ссылки), сериализация XMI (de), сериализация JSON (de) и т.д.

редактировать

Я нашел для вас что-то более интересное с примером, похожим на ваш, проверьте JsonBender.

import json
from jsonbender import bend, K, S

MAPPING = {
    'Person': {
        'firstname': S('Person', 'name', 'first'),
        'lastname': S('Person', 'name', 'last'),
        'birth': S('birth_date'),
        'points': S('points')
    }
}

source = {
    "Person": {
        "name": {
            "first": "John",
            "last": "Smith"
        }
        },
    "birth_date": "1997.01.12",
    "points": "330"
}

result = bend(MAPPING, source)
print(json.dumps(result))

Выход:

{"Person": {"lastname": "Smith", "points": "330", "firstname": "John", "birth": "1997.01.12"}}

Ответ 3

Вот мое взятие на себя (конвертер лямбда и точка-нотная идея, взятая из tobias_k):

import datetime

converters = {
    (str, datetime.date): lambda s: datetime.date(*map(int, s.split("."))),
    (datetime.date, str): lambda d: d.strftime("%Y.%m.%d"),
}
mapping = [
    ('Person.name.first', str, 'Person.firstname', str),
    ('Person.name.last', str, 'Person.lastname', str),
    ('birth_date', str, 'Person.birth', datetime.date),
    ('points', str, 'Person.points', int),
]

def covert_doc(doc, mapping, converters, inverse=False):
    converted = {}
    for keys1, type1, keys2, type2 in mapping:
        if inverse:
            keys1, type1, keys2, type2 = keys2, type2, keys1, type1
        converter = converters.get((type1, type2), type2)
        keys1 = keys1.split('.')
        keys2 = keys2.split('.')
        obj1 = doc
        while keys1:
            k, *keys1 = keys1
            obj1 = obj1[k]
        dict2 = converted
        while len(keys2) > 1:
            k, *keys2 = keys2
            dict2 = dict2.setdefault(k, {})
        dict2[keys2[0]] = converter(obj1)
    return converted

# Test
doc1 = {
    "Person": {
        "name": {
            "first": "John",
            "last": "Smith"
        }
    },
    "birth_date": "1997.01.12",
    "points": "330"
}
doc2 = {
    "Person": {
        "firstname": "John",
        "lastname": "Smith",
        "birth": datetime.date(1997, 1, 12),
        "points": 330
    }
}
assert doc2 == covert_doc(doc1, mapping, converters)
assert doc1 == covert_doc(doc2, mapping, converters, inverse=True)

Приятно то, что вы можете повторно использовать преобразователи (даже для преобразования различных структур документов), и вам нужно только определить нетривиальные преобразования. Недостатком является то, что каждая пара типов всегда должна использовать одно и то же преобразование (возможно, оно может быть расширено для добавления дополнительных альтернативных преобразований).

Ответ 4

Вы можете использовать списки для описания путей к значениям в объектах с функциями преобразования типов, например:

from_paths = [
    (['Person', 'name', 'first'], None),
    (['Person', 'name', 'last'], None),
    (['birth_date'], lambda s: datetime.date(*map(int, s.split(".")))),
    (['points'], lambda s: int(s))
]
to_paths = [
    (['Person', 'firstname'], None),
    (['Person', 'lastname'], None),
    (['Person', 'birth'], lambda d: d.strftime("%Y.%m.%d")),
    (['Person', 'points'], str)
]

и небольшая функция для скрытия от и до (как и тобиас, но без разделения строк и использования reduce для получения значений от dict):

def convert(from_paths, to_paths, obj):
    to_obj = {}
    for (from_keys, convfn), (to_keys, _) in zip(from_paths, to_paths):
        value = reduce(operator.getitem, from_keys, obj)
        if convfn:
            value = convfn(value)
        curr_lvl_dict = to_obj
        for key in to_keys[:-1]:
            curr_lvl_dict = curr_lvl_dict.setdefault(key, {})
        curr_lvl_dict[to_keys[-1]] = value
    return to_obj

тестовое задание:

from_json = '''{
    "Person": {
        "name": {
            "first": "John",
            "last": "Smith"
        }
    },
    "birth_date": "1997.01.12",
    "points": "330"
}'''
>>> obj = json.loads(from_json)
>>> new_obj = convert(from_paths, to_paths, obj)
>>> new_obj
{'Person': {'lastname': u'Smith',
            'points': 330,
            'birth': datetime.date(1997, 1, 12), 'firstname': u'John'}}
>>> convert(to_paths, from_paths, new_obj)
{'birth_date': '1997.01.12',
 'Person': {'name': {'last': u'Smith', 'first': u'John'}},
 'points': '330'}
>>>