¿Accediendo a las claves de dict como un atributo?

Me parece más conveniente acceder a las claves de dict como obj.foo lugar de obj['foo'] , así que escribí este fragmento:

 class AttributeDict(dict): def __getattr__(self, attr): return self[attr] def __setattr__(self, attr, value): self[attr] = value 

Sin embargo, asumo que debe haber alguna razón por la que Python no proporciona esta funcionalidad de manera inmediata. ¿Cuáles serían las advertencias y las trampas de acceder a las claves de dict de esta manera?

La mejor manera de hacer esto es:

 class AttrDict(dict): def __init__(self, *args, **kwargs): super(AttrDict, self).__init__(*args, **kwargs) self.__dict__ = self 

Algunos pros:

  • ¡Realmente funciona!
  • Ningún método de clase de diccionario está sombreado (por ejemplo, .keys() funciona bien)
  • Los atributos y elementos siempre están sincronizados
  • Intentar acceder a una clave no existente como un atributo correctamente genera AttributeError lugar de KeyError

Contras:

  • Métodos como .keys() no funcionarán bien si se sobrescriben con los datos entrantes
  • Causa una pérdida de memoria en Python <2.7.4 / Python3 <3.2.3
  • Pylint se vuelve loco con E1123(unexpected-keyword-arg) y E1103(maybe-no-member)
  • Para los no iniciados parece pura magia.

Una breve explicación de cómo funciona esto.

  • Todos los objetos de Python almacenan internamente sus atributos en un diccionario que se llama __dict__ .
  • No hay ningún requisito de que el diccionario interno __dict__ deba ser “simplemente un dict simple”, por lo que podemos asignar cualquier subclase de dict() al diccionario interno.
  • En nuestro caso, simplemente asignamos la instancia AttrDict() que estamos creando (como estamos en __init__ ).
  • Al llamar al método __init__() , nos aseguramos de que (ya) se comporte exactamente como un diccionario, ya que esa función llama a todo el código de creación de instancias del diccionario .

Una de las razones por las que Python no proporciona esta funcionalidad fuera de la caja

Como se indica en la lista de “contras”, esto combina el espacio de nombres de las claves almacenadas (¡que puede provenir de datos arbitrarios y / o no confiables!) Con el espacio de nombres de los atributos del método construido en dict. Por ejemplo:

 d = AttrDict() d.update({'items':["jacket", "necktie", "trousers"]}) for k, v in d.items(): # TypeError: 'list' object is not callable print "Never reached!" 

Puede usar todos los caracteres de cadena legales como parte de la clave si usa la notación de matriz. Por ejemplo, obj['!#$%^&*()_']

De esta otra pregunta SO, hay un gran ejemplo de implementación que simplifica su código existente. Qué tal si:

 class AttributeDict(dict): __getattr__ = dict.__getitem__ __setattr__ = dict.__setitem__ 

Mucho más conciso y no deja ningún espacio para el acceso adicional a las funciones __getattr__ y __setattr__ en el futuro.

Donde contesto la pregunta que fue hecha

¿Por qué Python no lo ofrece fuera de la caja?

Sospecho que tiene que ver con el Zen de Python : “Debe haber una, y preferiblemente solo una, forma obvia de hacerlo”. Esto crearía dos formas obvias de acceder a los valores de los diccionarios: obj['key'] y obj.key .

Advertencias y escollos

Estos incluyen la posible falta de claridad y confusión en el código. es decir, lo siguiente podría ser confuso para otra persona que va a mantener su código en una fecha posterior, o incluso para usted, si no va a volver a usarlo por un tiempo. De nuevo, desde el Zen : “¡La legibilidad cuenta!”

 >>> KEY = 'spam' >>> d[KEY] = 1 >>> # Several lines of miscellaneous code here... ... assert d.spam == 1 

Si d se crea una instancia o se define KEY o d[KEY] se asigna lejos de donde se usa d.spam , puede generar confusión sobre lo que se está haciendo, ya que este no es un lenguaje de uso común. Sé que tendría el potencial de confundirme.

Además, si cambia el valor de KEY siguiente manera (pero no cambia d.spam ), ahora obtiene:

 >>> KEY = 'foo' >>> d[KEY] = 1 >>> # Several lines of miscellaneous code here... ... assert d.spam == 1 Traceback (most recent call last): File "", line 2, in  AttributeError: 'C' object has no attribute 'spam' 

OMI, no vale la pena el esfuerzo.

Otros elementos

Como han señalado otros, puedes usar cualquier objeto hashable (no solo una cadena) como clave de dictado. Por ejemplo,

 >>> d = {(2, 3): True,} >>> assert d[(2, 3)] is True >>> 

es legal, pero

 >>> C = type('C', (object,), {(2, 3): True}) >>> d = C() >>> assert d.(2, 3) is True File "", line 1 d.(2, 3) ^ SyntaxError: invalid syntax >>> getattr(d, (2, 3)) Traceback (most recent call last): File "", line 1, in  TypeError: getattr(): attribute name must be string >>> 

no es. Esto le da acceso a toda la gama de caracteres imprimibles u otros objetos hashable para sus claves de diccionario, que no tiene cuando accede a un atributo de objeto. Esto hace posible tal magia como una metaclase de objetos en caché, como la receta del Libro de cocina de Python (Capítulo 9) .

En donde yo editorialize

Prefiero la estética del spam.eggs sobre el spam['eggs'] (creo que se ve más limpio), y realmente empecé a desear esta funcionalidad cuando me reuní con la namedtuple . Pero la conveniencia de poder hacer lo siguiente lo supera.

 >>> KEYS = 'spam eggs ham' >>> VALS = [1, 2, 3] >>> d = {k: v for k, v in zip(KEYS.split(' '), VALS)} >>> assert d == {'spam': 1, 'eggs': 2, 'ham': 3} >>> 

Este es un ejemplo simple, pero con frecuencia me encuentro usando dicts en situaciones diferentes de las que usaría la notación obj.key (es decir, cuando necesito leer las preferencias de un archivo XML). En otros casos, cuando me siento tentado a crear una instancia de una clase dinámica y abofetear algunos atributos por razones estéticas, continúo usando un dict para la coherencia con el fin de mejorar la legibilidad.

Estoy seguro de que el OP desde hace mucho tiempo lo resolvió para su satisfacción, pero si aún desea esta funcionalidad, le sugiero que descargue uno de los paquetes de pypi que lo proporciona:

  • Bunch es con el que estoy más familiarizado. Subclase de dict , para que tengas toda esa funcionalidad.
  • AttrDict también parece ser bastante bueno, pero no estoy tan familiarizado con él y no he revisado la fuente con tanto detalle como Bunch .
  • Como se señala en los comentarios de Rotareti, Bunch ha quedado en desuso, pero hay una bifurcación activa llamada Munch .

Sin embargo, para mejorar la legibilidad de su código, le recomiendo encarecidamente que no mezcle sus estilos de notación. Si prefiere esta notación, simplemente debe crear una instancia de un objeto dynamic, agregarle sus atributos deseados y llamarlo día:

 >>> C = type('C', (object,), {}) >>> d = C() >>> d.spam = 1 >>> d.eggs = 2 >>> d.ham = 3 >>> assert d.__dict__ == {'spam': 1, 'eggs': 2, 'ham': 3} 

Dónde actualizo, para responder a una pregunta de seguimiento en los comentarios

En los comentarios (abajo), Elmo pregunta:

¿Y si quieres profundizar más? (refiriéndose al tipo (…))

Aunque nunca he usado este caso de uso (nuevamente, tiendo a usar dict nested, por coherencia), el siguiente código funciona:

 >>> C = type('C', (object,), {}) >>> d = C() >>> for x in 'spam eggs ham'.split(): ... setattr(d, x, C()) ... i = 1 ... for y in 'one two three'.split(): ... setattr(getattr(d, x), y, i) ... i += 1 ... >>> assert d.spam.__dict__ == {'one': 1, 'two': 2, 'three': 3} 

Advertencia: por algunas razones, clases como esta parecen romper el paquete de multiprocesamiento. Solo luché con este error por un tiempo antes de encontrar este SO: Encontrando una excepción en el multiprocesamiento de python

¿Qué __eq__ si desea una clave que sea un método, como __eq__ o __getattr__ ?

Y no podría tener una entrada que no comenzara con una letra, así que usar 0343853 como clave no está disponible.

¿Y si no quisieras usar una cuerda?

Puede extraer una clase de contenedor conveniente de la biblioteca estándar:

 from argparse import Namespace 

Para evitar tener que copiar alrededor de bits de código. Sin acceso estándar al diccionario, pero fácil de recuperar si realmente lo desea. El código en argparse es simple,

 class Namespace(_AttributeHolder): """Simple object for storing attributes. Implements equality by attribute names and values, and provides a simple string representation. """ def __init__(self, **kwargs): for name in kwargs: setattr(self, name, kwargs[name]) __hash__ = None def __eq__(self, other): return vars(self) == vars(other) def __ne__(self, other): return not (self == other) def __contains__(self, key): return key in self.__dict__ 

Las tuplas se pueden utilizar para las teclas dict. ¿Cómo accederías a la tupla en tu construcción?

Además, nameduuple es una estructura conveniente que puede proporcionar valores a través del atributo de acceso.

No funciona en general. No todas las claves de dict válidas hacen atributos direccionables (“la clave”). Por lo tanto, tendrás que tener cuidado.

Los objetos de Python son básicamente diccionarios. Así que dudo que haya mucho rendimiento u otra sanción.

Esto no aborda la pregunta original, pero debería ser útil para las personas que, como yo, terminan aquí cuando buscan una biblioteca que proporciona esta funcionalidad.

Addict es una gran biblioteca para esto: https://github.com/mewwts/addict se ocupa de muchas de las preocupaciones mencionadas en las respuestas anteriores.

Un ejemplo de la documentación:

 body = { 'query': { 'filtered': { 'query': { 'match': {'description': 'addictive'} }, 'filter': { 'term': {'created_by': 'Mats'} } } } } 

Con adicto:

 from addict import Dict body = Dict() body.query.filtered.query.match.description = 'addictive' body.query.filtered.filter.term.created_by = 'Mats' 

Aquí hay un breve ejemplo de registros inmutables que usan collections.namedtuple integradas.

 def record(name, d): return namedtuple(name, d.keys())(**d) 

y un ejemplo de uso:

 rec = record('Model', { 'train_op': train_op, 'loss': loss, }) print rec.loss(..) 

No es necesario que escribas tu propio ya que setattr () y getattr () ya existen.

La ventaja de los objetos de clase probablemente entra en juego en la definición de clase y la herencia.

He creado esto basado en la entrada de este hilo. Sin embargo, necesito usar odict, así que tuve que anular get y set attr. Creo que esto debería funcionar para la mayoría de los usos especiales.

El uso se ve así:

 # Create an ordered dict normally... >>> od = OrderedAttrDict() >>> od["a"] = 1 >>> od["b"] = 2 >>> od OrderedAttrDict([('a', 1), ('b', 2)]) # Get and set data using attribute access... >>> od.a 1 >>> od.b = 20 >>> od OrderedAttrDict([('a', 1), ('b', 20)]) # Setting a NEW attribute only creates it on the instance, not the dict... >>> od.c = 8 >>> od OrderedAttrDict([('a', 1), ('b', 20)]) >>> od.c 8 

La clase:

 class OrderedAttrDict(odict.OrderedDict): """ Constructs an odict.OrderedDict with attribute access to data. Setting a NEW attribute only creates it on the instance, not the dict. Setting an attribute that is a key in the data will set the dict data but will not create a new instance attribute """ def __getattr__(self, attr): """ Try to get the data. If attr is not a key, fall-back and get the attr """ if self.has_key(attr): return super(OrderedAttrDict, self).__getitem__(attr) else: return super(OrderedAttrDict, self).__getattr__(attr) def __setattr__(self, attr, value): """ Try to set the data. If attr is not a key, fall-back and set the attr """ if self.has_key(attr): super(OrderedAttrDict, self).__setitem__(attr, value) else: super(OrderedAttrDict, self).__setattr__(attr, value) 

Este es un patrón bastante bueno ya mencionado en el hilo, pero si solo desea tomar un dictado y convertirlo en un objeto que funcione con autocompletar en un IDE, etc.

 class ObjectFromDict(object): def __init__(self, d): self.__dict__ = d 

Al parecer, ahora hay una biblioteca para esto, https://pypi.python.org/pypi/attrdict , que implementa esta funcionalidad exacta más la fusión recursiva y la carga json. Podría valer la pena un vistazo.

Solo para agregar algo de variedad a la respuesta, sci-kit learn ha implementado esto como un Bunch :

 class Bunch(dict): """ Scikit Learn's container object Dictionary-like object that exposes its keys as attributes. >>> b = Bunch(a=1, b=2) >>> b['b'] 2 >>> bb 2 >>> bc = 6 >>> b['c'] 6 """ def __init__(self, **kwargs): super(Bunch, self).__init__(kwargs) def __setattr__(self, key, value): self[key] = value def __dir__(self): return self.keys() def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(key) def __setstate__(self, state): pass 

Todo lo que necesita es obtener los métodos setattr y getattr , los controles getattr para las claves dict y los movimientos para verificar los atributos reales. El setstaet es un arreglo para la corrección de los “racimos” de decapado y despiece de encurtidos: si se comprueba, https://github.com/scikit-learn/scikit-learn/issues/6196

¿Qué hay de Prodict , la pequeña clase de Python que escribí para gobernarlos a todos 🙂

Además, obtienes la finalización automática de código , la creación de instancias recursivas de objetos y la conversión automática de tipos .

Puedes hacer exactamente lo que pediste:

 p = Prodict() p.foo = 1 p.bar = "baz" 

Ejemplo 1: Tipo de insinuación

 class Country(Prodict): name: str population: int turkey = Country() turkey.name = 'Turkey' turkey.population = 79814871 

código de auto completado

Ejemplo 2: conversión automática de tipo

 germany = Country(name='Germany', population='82175700', flag_colors=['black', 'red', 'yellow']) print(germany.population) # 82175700 print(type(germany.population)) #  print(germany.flag_colors) # ['black', 'red', 'yellow'] print(type(germany.flag_colors)) #  

Permítame publicar otra implementación, que se basa en la respuesta de Kinvais, pero integra ideas del AttributeDict propuesto en http://databio.org/posts/python_AttributeDict.html .

La ventaja de esta versión es que también funciona para diccionarios nesteds:

 class AttrDict(dict): """ A class to convert a nested Dictionary into an object with key-values that are accessible using attribute notation (AttrDict.attribute) instead of key notation (Dict["key"]). This class recursively sets Dicts to objects, allowing you to recurse down nested dicts (like: AttrDict.attr.attr) """ # Inspired by: # http://stackoverflow.com/a/14620633/1551810 # http://databio.org/posts/python_AttributeDict.html def __init__(self, iterable, **kwargs): super(AttrDict, self).__init__(iterable, **kwargs) for key, value in iterable.items(): if isinstance(value, dict): self.__dict__[key] = AttrDict(value) else: self.__dict__[key] = value 

Puedes hacerlo usando esta clase que acabo de hacer. Con esta clase puede usar el objeto Map como otro diccionario (incluida la serialización json) o con la notación de puntos. Espero que te ayude:

 class Map(dict): """ Example: m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer']) """ def __init__(self, *args, **kwargs): super(Map, self).__init__(*args, **kwargs) for arg in args: if isinstance(arg, dict): for k, v in arg.iteritems(): self[k] = v if kwargs: for k, v in kwargs.iteritems(): self[k] = v def __getattr__(self, attr): return self.get(attr) def __setattr__(self, key, value): self.__setitem__(key, value) def __setitem__(self, key, value): super(Map, self).__setitem__(key, value) self.__dict__.update({key: value}) def __delattr__(self, item): self.__delitem__(item) def __delitem__(self, key): super(Map, self).__delitem__(key) del self.__dict__[key] 

Ejemplos de uso:

 m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer']) # Add new key m.new_key = 'Hello world!' print m.new_key print m['new_key'] # Update values m.new_key = 'Yay!' # Or m['new_key'] = 'Yay!' # Delete key del m.new_key # Or del m['new_key'] 
 class AttrDict(dict): def __init__(self): self.__dict__ = self if __name__ == '____main__': d = AttrDict() d['ray'] = 'hope' d.sun = 'shine' >>> Now we can use this . notation print d['ray'] print d.sun 

Como señaló Doug, hay un paquete Bunch que puede usar para lograr la funcionalidad obj.key . En realidad hay una versión más nueva llamada

NeoBunch

Sin embargo, tiene una gran característica que convierte su dictado en un objeto NeoBunch a través de su función neobunchify . Utilizo mucho las plantillas de Mako y al pasar los datos, ya que los objetos NeoBunch los hacen mucho más legibles, así que si terminas usando un dict normal en tu progtwig Python pero quieres la notación de puntos en una plantilla de Mako, puedes usarlo de esa manera:

 from mako.template import Template from neobunch import neobunchify mako_template = Template(filename='mako.tmpl', strict_undefined=True) data = {'tmpl_data': [{'key1': 'value1', 'key2': 'value2'}]} with open('out.txt', 'w') as out_file: out_file.write(mako_template.render(**neobunchify(data))) 

Y la plantilla de Mako podría verse como:

 % for d in tmpl_data: Column1 Column2 ${d.key1} ${d.key2} % endfor 

La solución es:

 DICT_RESERVED_KEYS = vars(dict).keys() class SmartDict(dict): """ A Dict which is accessible via attribute dot notation """ def __init__(self, *args, **kwargs): """ :param args: multiple dicts ({}, {}, ..) :param kwargs: arbitrary keys='value' If ``keyerror=False`` is passed then not found attributes will always return None. """ super(SmartDict, self).__init__() self['__keyerror'] = kwargs.pop('keyerror', True) [self.update(arg) for arg in args if isinstance(arg, dict)] self.update(kwargs) def __getattr__(self, attr): if attr not in DICT_RESERVED_KEYS: if self['__keyerror']: return self[attr] else: return self.get(attr) return getattr(self, attr) def __setattr__(self, key, value): if key in DICT_RESERVED_KEYS: raise AttributeError("You cannot set a reserved name as attribute") self.__setitem__(key, value) def __copy__(self): return self.__class__(self) def copy(self): return self.__copy__() 

Esta no es una respuesta “buena”, pero pensé que era ingeniosa (no maneja los dicts nesteds en la forma actual). Simplemente envuelva su dictado en una función:

 def make_funcdict(d={}, **kwargs) def funcdict(d={}, **kwargs): funcdict.__dict__.update(d) funcdict.__dict__.update(kwargs) return funcdict.__dict__ funcdict(d, **kwargs) return funcdict 

Ahora tienes una syntax ligeramente diferente. Para acceder a los elementos dict como atributos haz f.key . Para acceder a los elementos de dictado (y otros métodos de dictado) de la forma habitual, haga f()['key'] y podremos actualizar el dictado de manera conveniente llamando a f con argumentos de palabras clave y / o un diccionario.

Ejemplo

 d = {'name':'Henry', 'age':31} d = make_funcdict(d) >>> for key in d(): ... print key ... age name >>> print d.name ... Henry >>> print d.age ... 31 >>> d({'Height':'5-11'}, Job='Carpenter') ... {'age': 31, 'name': 'Henry', 'Job': 'Carpenter', 'Height': '5-11'} 

Y ahí está. Estaré feliz si alguien sugiere los beneficios e inconvenientes de este método.

¿Cuáles serían las advertencias y las trampas de acceder a las claves de dict de esta manera?

Como sugiere @Henry, una razón por la que el acceso por puntos no se puede usar en los dicts es que limita los nombres de las claves de dictado a las variables válidas para python, restringiendo así todos los nombres posibles.

Los siguientes son ejemplos de por qué el acceso por puntos no sería útil en general, dado un dictado, d :

Validez

Los siguientes atributos no serían válidos en Python:

 d.1_foo # enumerated names d./bar # path names d.21.7, d.12:30 # decimals, time d."" # empty strings d.john doe, d.denny's # spaces, misc punctuation d.3 * x # expressions 

Estilo

Las convenciones PEP8 impondrían una restricción suave en la denominación de atributos:

A. Nombres de palabras clave reservadas (o función incorporada):

 d.in d.False, d.True d.max, d.min d.sum d.id 

Si el nombre de un argumento de función choca con una palabra clave reservada, generalmente es mejor agregar un solo guión bajo …

B. La regla de caso sobre métodos y nombres de variables :

Los nombres de variables siguen la misma convención que los nombres de funciones.

 d.Firstname d.Country 

Utilice las reglas de nomenclatura de funciones: minúsculas con palabras separadas por guiones bajos según sea necesario para mejorar la legibilidad.


A veces, estas preocupaciones se plantean en bibliotecas como pandas , que permiten el acceso punteado de las columnas de DataFrame por nombre. El mecanismo predeterminado para resolver las restricciones de nombres también es la notación de matriz, una cadena entre paréntesis.

Si estas restricciones no se aplican a su caso de uso, hay varias opciones en las estructuras de datos de acceso punteado .