Преобразование двунаправленной структуры данных в 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'}
>>>