Python中的双向数据结构转换

时间:2018-04-26 11:20:25

标签: python data-structures

注意:这是不是一个简单的双向地图;转换是重要的部分。

我正在编写一个应用程序来发送和接收具有特定结构的消息,我必须将其转换为内部结构。

例如,消息:

{
    "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中指定一次映射,并在两种情况下使用它?

在我的研究中,我发现了一个名为JsonGrammar的有趣的Haskell库,允许这样做(它适用于JSON,但这与案例无关)。但是我对Haskell的了解并不足以尝试一个端口。

4 个答案:

答案 0 :(得分:11)

这实际上是一个非常有趣的问题。您可以定义转换列表,例如以(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}}

答案 1 :(得分:6)

托比亚斯已经很好地回答了这个问题。如果您正在寻找一个可以动态确保模型转换的库,那么您可以浏览Python的模型转换库PyEcore

PyEcore允许您处理模型和元模型(结构化数据模型),并提供基于结构化数据模型构建基于ModelDrivenEngineering的工具和其他应用程序所需的密钥。它支持开箱即用:

数据继承, 双向关系管理(对面参考), XMI(de)序列化, 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"}}

答案 2 :(得分:1)

这是我对此的看法(转换器lambdas和基于点的符号构思取自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)

这个好东西是你可以重用转换器(甚至转换不同的文档结构),你只需要定义非平凡的转换。缺点是,实际上,每对类型必须始终使用相同的转换(可能会扩展为添加可选的替代转换)。

答案 3 :(得分:1)

您可以使用列表来描述具有类型转换函数的对象中的值的路径,例如:

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)
]

和一个隐藏的功能(很像tobias建议但没有字符串分离并使用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'}
>>>