【问题标题】:Bidirectional data structure conversion in PythonPython中的双向数据结构转换
【发布时间】:2018-04-26 11:20:24
【问题描述】:

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

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

例如消息:

{
    "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 的了解还不足以尝试移植。

【问题讨论】:

  • 这是一个有趣的问题。我不相信这只是对工具/库的推荐。有一个问题是如何存储/反向转换逻辑无需单独编写正向和反向版本

标签: python data-structures


【解决方案1】:

这实际上是一个非常有趣的问题。您可以定义一个转换列表,例如(key1, func_1to2, key2, func_2to1) 或类似格式,其中key 可以包含分隔符来指示字典的不同级别,例如"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}}

【讨论】:

  • 很好,而且只有几行!谢谢!唯一让我难过的是,如果特定的子转换(例如 name 结构 ←→ 一对键 lastnamefirstname)也出现在消息的另一部分中,则它们不能被重用。
  • @AndréParamés 好点。这也可能是可能的,使用递归。例如。您可以只为键 "Person" 定义一个映射,并作为函数指定一个函数,该函数再次使用不同的映射调用 convert。但在这种情况下,这很困难,因为还有其他键可以“切换”字典级别。
【解决方案2】:

托拜厄斯回答得很好。如果您正在寻找一个能够动态确保模型转换的库,那么您可以探索 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"}}

【讨论】:

  • 谢谢,听起来很有趣;你知道这里有类似案例的例子吗?我不得不承认,我在阅读该页面时感到有些失落。
  • @AndréParamés 是的,我知道这可能非常复杂。这就是为什么我明确提到它基于模型驱动设计工程。我找不到专门针对您的案例或类似情况的实际示例,但我发现了一些对您来说可能很有趣的简单内容。我在答案中提到。
  • 谢谢,JsonBender 看起来确实很有趣。但它是双向的吗?
  • 你可以创建双向映射 :) 我已经为你做了一个方向映射。看起来很干净的黑盒东西。如果需要帮助,请继续联系我。
  • 当然,但我的目标是避免两者都写:D
【解决方案3】:

这是我对此的看法(转换器 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)

这些好处是您可以重用转换器(甚至可以转换不同的文档结构)并且您只需要定义非平凡的转换。缺点是,每对类型都必须始终使用相同的转换(也许可以扩展以添加可选的替代转换)。

【讨论】:

    【解决方案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)
    ]
    

    还有一个小函数来转换(很像 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'}
    >>> 
    

    【讨论】:

    • 谢谢,但我的目标是避免为每个方向编写转换,我只想写一个“双向”的转换。
    猜你喜欢
    • 2023-03-27
    • 1970-01-01
    • 2021-10-26
    • 1970-01-01
    • 2014-03-21
    • 2023-04-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多