Двунаправленное преобразование структуры данных в Python - PullRequest
0 голосов
/ 26 апреля 2018

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

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

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

{
    "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, но это не имеет значения для случая). Но мои знания о Хаскеле не достаточно хороши, чтобы попытаться выполнить порт.

Ответы [ 4 ]

0 голосов
/ 26 апреля 2018

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

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'}
>>> 
0 голосов
/ 26 апреля 2018

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

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

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

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

Я нашел для вас что-то более интересное на примере, похожем на ваш, посмотрите 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"}}
0 голосов
/ 26 апреля 2018

Вот мой взгляд на это (идея лямбда-преобразования и нотации на основе точек взята из 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)

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

0 голосов
/ 26 апреля 2018

Это на самом деле довольно интересная проблема. Вы можете определить список преобразований, например, в форме (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}}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...