2010-09-17 13 views
30

Soy nuevo en Python, y estoy sorprendido de que no pueda hacer esto.¿Qué hago cuando necesito un diccionario autorreferencial?

dictionary = { 
    'a' : '123', 
    'b' : dictionary['a'] + '456' 
} 

me pregunto cuál es la forma correcta Pythonic hacer esto en mi guión, porque siento que no soy el único que ha tratado de hacer esto.

EDIT: Suficiente gente se preguntaba qué estoy haciendo con esto, así que aquí hay más detalles para mis casos de uso. Digamos que quiero mantener los objetos del diccionario para mantener las rutas del sistema de archivos. Las rutas son relativas a otros valores en el diccionario. Por ejemplo, esto es lo que puede parecer uno de mis diccionarios.

dictionary = { 
    'user': 'sholsapp', 
    'home': '/home/' + dictionary['user'] 
} 

Es importante que en cualquier momento puedo cambiar de dictionary['user'] y tener todos los diccionarios valores reflejan el cambio. De nuevo, este es un ejemplo para el que lo estoy usando, por lo que espero que transmita mi objetivo.

De mi propia investigación, creo que tendré que implementar una clase para hacer esto.

+1

¿Podría hacer que la expresión sea floja para arreglar esto? – sholsapp

+0

¿Desea que el valor de 'dictionary ['b']' sea estático o dinámico? ¿Cómo se vería el 'diccionario' después de' dictionary ['a'] = '999''? –

+0

@Will: estaba a punto de editar mi publicación para responder a esta pregunta. Quiero que sea estático, y me doy cuenta de que la respuesta de g.d.d.c sería por dinámica. :/ – sholsapp

Respuesta

39

Sin miedo a la creación de nuevas clases - Usted puede tomar ventaja de las capacidades de formateo de cadenas de Python y simplemente hacer:

class MyDict(dict): 
    def __getitem__(self, item): 
     return dict.__getitem__(self, item) % self 

dictionary = MyDict({ 

    'user' : 'gnucom', 
    'home' : '/home/%(user)s', 
    'bin' : '%(home)s/bin' 
}) 


print dictionary["home"] 
print dictionary["bin"] 
+0

¿Son esos "% dictionary" en las declaraciones de impresión una supervisión accidental? No deberían ser necesarios. –

+1

En el más estricto, ya que este * está * creando una nueva clase, aunque es una derivada ... y sin embargo extremadamente inteligente en mi humilde opinión y casi transparente en la mayoría de los aspectos (otra cosa buena). – martineau

+1

@Martineau: Sé que esto es reating una nueva clase, y esto es lo que quise decir - fue una respuesta a la respuesta de Tony Veijalainen (abajo) que evitaba crear una nueva clase. – jsbueno

7
>>> dictionary = { 
... 'a':'123' 
... } 
>>> dictionary['b'] = dictionary['a'] + '456' 
>>> dictionary 
{'a': '123', 'b': '123456'} 

Funciona bien, pero cuando usted está tratando de utilizar dictionary no se ha definido todavía (porque tiene que evaluar ese diccionario literal primero).

Pero tenga cuidado porque este asigna a la tecla de 'b' el valor referenciado por la clave de 'a'en el momento de la asignación y no se va a hacer la búsqueda cada vez. Si eso es lo que estás buscando, es posible pero con más trabajo.

+0

Eso podría comenzar a parecer asqueroso después de unos cientos de entradas ... – sholsapp

+2

Podría usar 'dictionary.update ({'b': dictionary ['a'] + '456'})' y pasarlo tan grande como diccionario como lo desee, pero parece que gddc tiene el enfoque que desea, que es una búsqueda verdaderamente autorreferencial y dinámica y no solo una búsqueda "en tiempo de asignación". –

+0

Gracias por la información.Eso puede ser lo que termine haciendo, porque tenemos Python 2.4 instalado por defecto. – sholsapp

12

más cercano que se acercó sin hacer objeto:

dictionary = { 
    'user' : 'gnucom', 
    'home' : lambda:'/home/'+dictionary['user'] 
} 

print dictionary['home']() 
dictionary['user']='tony' 
print dictionary['home']() 
1

Escribir una clase, tal vez algo con propiedades:

class PathInfo(object): 
    def __init__(self, user): 
     self.user = user 

    @property 
    def home(self): 
     return '/home/' + self.user 

p = PathInfo('thc') 
print p.home # /home/thc 
4

Lo que está describiendo en su edición es cómo funciona un archivo de configuración INI. Python tiene una biblioteca incorporada llamada ConfigParser que debería funcionar para lo que está describiendo.

4

Esto es un problema interesante. Parece que Greg tiene un good solution. Pero eso no es divertido;)

jsbueno como very elegant solution pero eso solo se aplica a las cadenas (como lo solicitó).

El truco de un diccionario auto referencial "general" es utilizar un objeto sustituto. Se tarda unos pocos (subestimación) líneas de código de lograr, pero el uso es en lo que quiere:

S = SurrogateDict(AdditionSurrogateDictEntry) 
d = S.resolve({'user': 'gnucom', 
       'home': '/home/' + S['user'], 
       'config': [S['home'] + '/.emacs', S['home'] + '/.bashrc']}) 

El código para que esto suceda no es tan corto.Vive en tres clases:

import abc 

class SurrogateDictEntry(object): 
    __metaclass__ = abc.ABCMeta 
    def __init__(self, key): 
     """record the key on the real dictionary that this will resolve to a 
      value for 
     """ 
     self.key = key 

    def resolve(self, d): 
     """ return the actual value""" 
     if hasattr(self, 'op'): 
      # any operation done on self will store it's name in self.op. 
      # if this is set, resolve it by calling the appropriate method 
      # now that we can get self.value out of d 
      self.value = d[self.key] 
      return getattr(self, self.op + 'resolve__')() 
     else: 
      return d[self.key] 

    @staticmethod 
    def make_op(opname): 
     """A convience class. This will be the form of all op hooks for subclasses 
      The actual logic for the op is in __op__resolve__ (e.g. __add__resolve__) 
     """ 
     def op(self, other): 
      self.stored_value = other 
      self.op = opname 
      return self 
     op.__name__ = opname 
     return op 

A continuación viene la clase concreta. Suficientemente simple.

class AdditionSurrogateDictEntry(SurrogateDictEntry): 

    __add__ = SurrogateDictEntry.make_op('__add__') 
    __radd__ = SurrogateDictEntry.make_op('__radd__') 

    def __add__resolve__(self): 
     return self.value + self.stored_value 

    def __radd__resolve__(self): 
     return self.stored_value + self.value 

Aquí está la clase final

class SurrogateDict(object): 
    def __init__(self, EntryClass): 
     self.EntryClass = EntryClass 

    def __getitem__(self, key): 
     """record the key and return""" 
     return self.EntryClass(key) 

    @staticmethod 
    def resolve(d): 
     """I eat generators resolve self references""" 
     stack = [d] 
     while stack: 
      cur = stack.pop() 
      # This just tries to set it to an appropriate iterable 
      it = xrange(len(cur)) if not hasattr(cur, 'keys') else cur.keys() 
      for key in it: 
       # sorry for being a duche. Just register your class with 
       # SurrogateDictEntry and you can pass whatever. 
       while isinstance(cur[key], SurrogateDictEntry): 
        cur[key] = cur[key].resolve(d) 
       # I'm just going to check for iter but you can add other 
       # checks here for items that we should loop over. 
       if hasattr(cur[key], '__iter__'): 
        stack.append(cur[key]) 
     return d 

En respuesta a la pregunta de por qué gnucoms puse el nombre de las clases de la forma en que lo hice.

La palabra sustituta generalmente se asocia con sustituirse por otra cosa, por lo que parece apropiada porque eso es lo que hace la clase SurrogateDict: una instancia reemplaza las referencias "propias" en un literal de diccionario. Habiendo dicho eso, (aparte de ser simplemente estúpido), nombrar es probablemente una de las cosas más difíciles para mí sobre la codificación. Si usted (o cualquier otra persona) puede sugerir un nombre mejor, soy todo oídos.

Proporcionaré una breve explicación. A lo largo de S se refiere a una instancia de SurrogateDict y d es el diccionario real.

  1. Una referencia S[key] desencadena S.__getitem__ y SurrogateDictEntry(key) para ser colocado en el d.

  2. Cuando se construye S[key] = SurrogateDictEntry(key), almacena key. Este será el key en d para el valor que esta entrada de SurrogateDictEntry está actuando como un sustituto para.

  3. Después de que se devuelve S[key], se ingresa en el d, o se le realizan algunas operaciones. Si se realiza una operación en él, desencadena el método relativo __op__ que simplemente almacena el valor en el que se realiza la operación y el nombre de la operación y luego se devuelve a sí mismo. Realmente no podemos resolver la operación porque d no se ha construido aún.

  4. Después de construir d, se pasa al S.resolve. Este método recorre d buscando instancias de SurrogateDictEntry y reemplazándolos con el resultado de llamar al método resolve en la instancia.

  5. El método SurrogateDictEntry.resolve recibe ahora construido d como un argumento y puede utilizar el valor de key que almacena en el momento de la construcción para obtener el valor que está actuando como un sustituto para. Si se realizó una operación en él después de la creación, el atributo op se habrá establecido con el nombre de la operación que se realizó. Si la clase tiene un método __op__, entonces tiene un método __op__resolve__ con la lógica real que normalmente estaría en el método __op__. Así que ahora tenemos la lógica (auto op__resolve) y todos los valores necesarios (self.value, self.stored_value) para obtener finalmente el valor real de d[key]. Entonces devolvemos lo que paso 4 lugares en el diccionario.

  6. finalmente el método SurrogateDict.resolve devuelve d con todas las referencias resueltas.

That'a a rough sketch. Si tiene más preguntas, no dude en preguntar.

+0

¿Puede posiblemente discutir por qué nombró sus clases de la manera en que lo hizo? Intento desarrollar una comprensión profunda de la clase y, bueno, no entiendo cada parte de tu código. : P – sholsapp

+0

@gnucom, actualizado al final de mi publicación. HTH. no dude en hacer cualquier pregunta si algo lo está haciendo tropezar. Es un poco frágil ahora mismo. En particular, 'L [foo] + 4 + 4' podría fallar. 'L [3] + L [3] + 1' funciona bien. No estoy seguro de '1 + L [3] + l', pero creo que eso no funcionaría. Los dos (supuestos) fallos podrían corregirse haciendo que 'SurrogateDictEntry' implemente una pila de operaciones en lugar de simplemente almacenar un par de operación/valor. – aaronasterling

+0

@gnucom Además, no se enamore demasiado con esta solución. Los dos que he vinculado son __mucho__ mejor para su caso de uso. Principalmente lo hice solo para hacerlo. – aaronasterling

1

como una especie de versión extendida de @Tony's answer, se podría construir una subclase diccionario que llama a sus valores si son callables:

class CallingDict(dict): 
    """Returns the result rather than the value of referenced callables. 

    >>> cd = CallingDict({1: "One", 2: "Two", 'fsh': "Fish", 
    ...     "rhyme": lambda d: ' '.join((d[1], d['fsh'], 
    ...            d[2], d['fsh']))}) 
    >>> cd["rhyme"] 
    'One Fish Two Fish' 
    >>> cd[1] = 'Red' 
    >>> cd[2] = 'Blue' 
    >>> cd["rhyme"] 
    'Red Fish Blue Fish' 
    """ 
    def __getitem__(self, item): 
     it = super(CallingDict, self).__getitem__(item) 
     if callable(it): 
      return it(self) 
     else: 
      return it 

Por supuesto, esto sólo sería útil si usted no está realmente va a almacenar callables como valores. Si necesita poder hacer eso, podría ajustar la declaración lambda en una función que agregue algún atributo al lambda resultante, y verificarlo en CallingDict.__getitem__, pero en ese punto se está volviendo complejo y prolijo, suficiente para que podría ser más fácil usar una clase para sus datos en primer lugar.

3

Si, como yo vagando cómo hacer @jsbueno snippet trabajo con {} sustituciones de estilo, a continuación es el código de ejemplo (que probablemente no es mucho más eficiente, aunque):

import string 

class MyDict(dict): 
    def __init__(self, *args, **kw): 
     super(MyDict,self).__init__(*args, **kw) 
     self.itemlist = super(MyDict,self).keys() 
     self.fmt = string.Formatter() 

    def __getitem__(self, item): 
     return self.fmt.vformat(dict.__getitem__(self, item), {}, self) 


xs = MyDict({ 
    'user' : 'gnucom', 
    'home' : '/home/{user}', 
    'bin' : '{home}/bin' 
}) 


>>> xs["home"] 
'/home/gnucom' 
>>> xs["bin"] 
'/home/gnucom/bin' 

Traté de hacer que funcione con el simple reemplazo de % self con .format(**self) pero resulta que no funcionaría para expresiones anidadas (como 'bin' en la lista anterior, que hace referencia a 'home', que tiene su propia referencia a 'usuario') debido a la orden de evaluación (** la expansión se realiza antes de la llamada al formato real y no se demora como en la versión% original).

Cuestiones relacionadas