web-dev-qa-db-fra.com

Conversion de structure de données bidirectionnelle en Python

Note: c'est pas une simple carte à double sens; la conversion est la partie importante. 

J'écris une application qui va envoyer et recevoir des messages avec une certaine structure, que je dois convertir depuis et vers une structure interne.

Par exemple, le message:

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

Cela doit être converti en:

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

Et vice versa.

Ces messages contiennent de nombreuses informations. Par conséquent, je souhaite éviter d’écrire manuellement les convertisseurs dans les deux sens. Y at-il un moyen en Python de spécifier le mappage une fois et de l’utiliser dans les deux cas?

Dans mes recherches, j'ai trouvé une bibliothèque intéressante de Haskell appelée JsonGrammar qui permet cela (c'est pour JSON, mais ce n'est pas pertinent pour le cas). Mais ma connaissance de Haskell n'est pas suffisante pour tenter un port.

15
André Paramés

C'est en fait un problème assez intéressant. Vous pouvez définir une liste de transformation, par exemple sous la forme (key1, func_1to2, key2, func_2to1), ou un format similaire, où key pourrait contenir des séparateurs pour indiquer différents niveaux du dict, comme "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)]

Ensuite, parcourez les éléments de cette liste et transformez les entrées du dictionnaire selon que vous voulez passer du formulaire A au formulaire B ou inversement. Vous aurez également besoin d'une fonction d'assistance pour accéder aux clés des dictionnaires imbriqués à l'aide de ces clés séparées par des points.

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

Exemple: 

>>> 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}}
11
tobias_k

Tobias a très bien répondu. Si vous recherchez une bibliothèque qui assure la transformation de modèle de manière dynamique, vous pouvez explorer la bibliothèque de transformation de modèle de Python PyEcore

PyEcore vous permet de gérer des modèles et des métamodèles (modèle de données structuré) et vous donne la clé dont vous avez besoin pour créer des outils basés sur ModelDrivenEngineering et d'autres applications basées sur un modèle de données structuré. Il prend en charge les éléments suivants:

Héritage de données, Gestion des relations bilatérales (références opposées), Sérialisation (.) XMI, JSON (dés) sérialisation, etc.

Modifier

J'ai trouvé quelque chose de plus intéressant pour vous avec un exemple similaire au vôtre, consultez 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))

Sortie:

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

Vous pouvez utiliser des listes pour décrire les chemins d'accès aux valeurs dans des objets dotés de fonctions de conversion de type, par exemple:

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

et une petite fonction à convertir de et vers (un peu comme le suggère tobias mais sans séparation des chaînes et en utilisant reduce pour obtenir les valeurs de 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

tester:

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'}
>>> 
1
ndpu

Voici mon point de vue sur ceci (lambdas de convertisseur et idée de notation basée sur des points tirée de 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)

Cela vous permet de réutiliser des convertisseurs (même pour convertir différentes structures de document) et de définir des conversions non triviales. L'inconvénient est que, dans l'état actuel des choses, chaque paire de types doit toujours utiliser la même conversion (elle pourrait peut-être être étendue pour ajouter d'autres conversions facultatives).

1
jdehesa