2010-09-26 16 views
21

Estoy construyendo un código Python para leer y manipular dicts profundamente anidados (en última instancia, para interactuar con servicios JSON, sin embargo, sería genial tenerlo para otros fines) I ' Estoy buscando una forma de leer/configurar/actualizar fácilmente los valores en profundidad dentro del diccionario, sin necesidad de una gran cantidad de código.Python: Acceda fácilmente a Dict profundamente anidados (obtener y establecer)

@ver también Python: Recursively access dict via attributes as well as index access? - La solución "DotDictify" de Curt Hagenlocher es bastante elocuente. También me gusta lo que Ben Alman presenta para JavaScript en http://benalman.com/projects/jquery-getobject-plugin/ Sería genial combinar los dos.

edificio fuera de Curt Hagenlocher y ejemplos de Ben Alman, sería grande en Python para tener una capacidad como:

>>> my_obj = DotDictify() 
>>> my_obj.a.b.c = {'d':1, 'e':2} 
>>> print my_obj 
{'a': {'b': {'c': {'d': 1, 'e': 2}}}} 
>>> print my_obj.a.b.c.d 
1 
>>> print my_obj.a.b.c.x 
None 
>>> print my_obj.a.b.c.d.x 
None 
>>> print my_obj.a.b.c.d.x.y.z 
None 

Cualquier idea si esto es posible, y si es así, cómo hacer para modificar el DotDictify solución?

Alternativamente, se podría hacer que el método get acepte una notación de puntos (y se agregue un método de conjunto complementario), sin embargo, la notación del objeto es más clara.

>>> my_obj = DotDictify() 
>>> my_obj.set('a.b.c', {'d':1, 'e':2}) 
>>> print my_obj 
{'a': {'b': {'c': {'d': 1, 'e': 2}}}} 
>>> print my_obj.get('a.b.c.d') 
1 
>>> print my_obj.get('a.b.c.x') 
None 
>>> print my_obj.get('a.b.c.d.x') 
None 
>>> print my_obj.get('a.b.c.d.x.y.z') 
None 

Este tipo de interacción sería genial para manejar dictados profundamente anidados. ¿Alguien sabe otra estrategia (o fragmento de código de muestra/biblioteca) para probar?

Respuesta

33

Árbol de atributos

El problema con su primera especificación es que Python no puede decir en __getitem__ si, al my_obj.a.b.c.d, lo próximo se procederá más abajo en un árbol que no existe, en cuyo caso se necesita para volver un objeto con un método __getitem__ por lo que no obtendrá un AttributeError arrojado sobre usted, o si desea un valor, en cuyo caso debe devolver None.

Yo diría que en todos los casos que tenga arriba, debe esperar arrojar un KeyError en lugar de devolver None. El motivo es que no puede decir si None significa "sin clave" o "alguien actualmente almacena None en esa ubicación". Por este comportamiento, todo lo que tiene que hacer es tomar dotdictify, eliminar , y reemplazar con __getitem__:

def __getitem__(self, key): 
    return self[key] 

Porque lo que realmente quiere es un dict con __getattr__ y __setattr__.

Puede haber una manera de eliminar __getitem__ por completo y decir algo como __getattr__ = dict.__getitem__, pero creo que esto puede ser sobre-optimización, y será un problema si más adelante decide que quiere __getitem__ para crear el árbol, ya que va como dotdictify originalmente hace, en cuyo caso debe cambiarlo a:

def __getitem__(self, key): 
    if key not in self: 
     dict.__setitem__(self, key, dotdictify()) 
    return dict.__getitem__(self, key) 

no me gusta el negocio en el original dotdictify.

compatibilidad con rutas

La segunda especificación (anulación get() y set()) es que una normal de dict tiene un get() que opera de manera diferente de lo que usted describe, y ni siquiera tiene un set (aunque tiene un setdefault() que es una operación inversa al get()). La gente espera que get tome dos parámetros, el segundo es un valor predeterminado si no se encuentra la clave.

Si desea ampliar __getitem__ y __setitem__ para manejar la notación de puntos-clave, tendrá que modificar doctictify a:

class dotdictify(dict): 
    def __init__(self, value=None): 
     if value is None: 
      pass 
     elif isinstance(value, dict): 
      for key in value: 
       self.__setitem__(key, value[key]) 
     else: 
      raise TypeError, 'expected dict' 

    def __setitem__(self, key, value): 
     if '.' in key: 
      myKey, restOfKey = key.split('.', 1) 
      target = self.setdefault(myKey, dotdictify()) 
      if not isinstance(target, dotdictify): 
       raise KeyError, 'cannot set "%s" in "%s" (%s)' % (restOfKey, myKey, repr(target)) 
      target[restOfKey] = value 
     else: 
      if isinstance(value, dict) and not isinstance(value, dotdictify): 
       value = dotdictify(value) 
      dict.__setitem__(self, key, value) 

    def __getitem__(self, key): 
     if '.' not in key: 
      return dict.__getitem__(self, key) 
     myKey, restOfKey = key.split('.', 1) 
     target = dict.__getitem__(self, myKey) 
     if not isinstance(target, dotdictify): 
      raise KeyError, 'cannot get "%s" in "%s" (%s)' % (restOfKey, myKey, repr(target)) 
     return target[restOfKey] 

    def __contains__(self, key): 
     if '.' not in key: 
      return dict.__contains__(self, key) 
     myKey, restOfKey = key.split('.', 1) 
     target = dict.__getitem__(self, myKey) 
     if not isinstance(target, dotdictify): 
      return False 
     return restOfKey in target 

    def setdefault(self, key, default): 
     if key not in self: 
      self[key] = default 
     return self[key] 

    __setattr__ = __setitem__ 
    __getattr__ = __getitem__ 

Código de ensayo:

>>> life = dotdictify({'bigBang': {'stars': {'planets': {}}}}) 
>>> life.bigBang.stars.planets 
{} 
>>> life.bigBang.stars.planets.earth = { 'singleCellLife' : {} } 
>>> life.bigBang.stars.planets 
{'earth': {'singleCellLife': {}}} 
>>> life['bigBang.stars.planets.mars.landers.vikings'] = 2 
>>> life.bigBang.stars.planets.mars.landers.vikings 
2 
>>> 'landers.vikings' in life.bigBang.stars.planets.mars 
True 
>>> life.get('bigBang.stars.planets.mars.landers.spirit', True) 
True 
>>> life.setdefault('bigBang.stars.planets.mars.landers.opportunity', True) 
True 
>>> 'landers.opportunity' in life.bigBang.stars.planets.mars 
True 
>>> life.bigBang.stars.planets.mars 
{'landers': {'opportunity': True, 'vikings': 2}} 
+0

Muchas gracias, Mike. Agregué una función get que acepta la notación de punto (y un valor predeterminado, como lo anotaron). Creo que esta nueva clase dotdictify hará que la vida sea mucho más fácil al tratar con dicts profundamente anidados. Muchas gracias. – Hal

+0

¿Necesita una función 'get()'? ¿Qué hace que el 'get()' existente no lo haga? El 'get()' que describiste en la pregunta es equivalente al 'get (key, None)' que obtienes gratis de 'dict'. –

+0

Cuando utilicé la clase dotdictify "tal cual" con mi instalación de Python 2.5 (Google App Engine SDK), la función get no manejó las solicitudes de notación de puntos por algún motivo. Así que escribí un contenedor rápido para la función get() para verificar la notación de puntos y, de ser así, pasé a __getattr__ (devolviendo el valor predeterminado en excepción), de lo contrario pasaría a dict.get (self, key, default) – Hal

2

He usado algo similar para construir somithing similar Trie para una aplicación. Espero que ayude.

class Trie: 
    """ 
    A Trie is like a dictionary in that it maps keys to values. 
    However, because of the way keys are stored, it allows 
    look up based on the longest prefix that matches. 

    """ 

    def __init__(self): 
     # Every node consists of a list with two position. In 
     # the first one,there is the value while on the second 
     # one a dictionary which leads to the rest of the nodes. 
     self.root = [0, {}] 


    def insert(self, key): 
     """ 
     Add the given value for the given key. 

     >>> a = Trie() 
     >>> a.insert('kalo') 
     >>> print(a) 
     [0, {'k': [1, {'a': [1, {'l': [1, {'o': [1, {}]}]}]}]}] 
     >>> a.insert('kalo') 
     >>> print(a) 
     [0, {'k': [2, {'a': [2, {'l': [2, {'o': [2, {}]}]}]}]}] 
     >>> b = Trie() 
     >>> b.insert('heh') 
     >>> b.insert('ha') 
     >>> print(b) 
     [0, {'h': [2, {'a': [1, {}], 'e': [1, {'h': [1, {}]}]}]}] 

     """ 

     # find the node to append the new value. 
     curr_node = self.root 
     for k in key: 
      curr_node = curr_node[1].setdefault(k, [0, {}]) 
      curr_node[0] += 1 


    def find(self, key): 
     """ 
     Return the value for the given key or None if key not 
     found. 

     >>> a = Trie() 
     >>> a.insert('ha') 
     >>> a.insert('ha') 
     >>> a.insert('he') 
     >>> a.insert('ho') 
     >>> print(a.find('h')) 
     4 
     >>> print(a.find('ha')) 
     2 
     >>> print(a.find('he')) 
     1 

     """ 

     curr_node = self.root 
     for k in key: 
      try: 
       curr_node = curr_node[1][k] 
      except KeyError: 
       return 0 
     return curr_node[0] 

    def __str__(self): 
     return str(self.root) 

    def __getitem__(self, key): 
     curr_node = self.root 
     for k in key: 
      try: 
       curr_node = curr_node[1][k] 
      except KeyError: 
       yield None 
     for k in curr_node[1]: 
      yield k, curr_node[1][k][0] 

if __name__ == '__main__': 
    a = Trie() 
    a.insert('kalo') 
    a.insert('kala') 
    a.insert('kal') 
    a.insert('kata') 
    print(a.find('kala')) 
    for b in a['ka']: 
     print(b) 
    print(a) 
1

Para los compañeros de Google: nos ahora tiene addict:

pip install addict 

y

mapping.a.b.c.d.e = 2 
mapping 
{'a': {'b': {'c': {'d': {'e': 2}}}}} 

Lo usé extensamente.

Para trabajar con caminos de puntos, encontré dotted:

obj = DottedDict({'hello': {'world': {'wide': 'web'}}}) 
obj['hello.world.wide'] == 'web' # true 
Cuestiones relacionadas