En Python, ¿cómo lanzo un objeto de clase a un dictado?

85

Digamos que tengo una clase simple en Python

class Wharrgarbl(object):
    def __init__(self, a, b, c, sum, version='old'):
        self.a = a
        self.b = b
        self.c = c
        self.sum = 6
        self.version = version

    def __int__(self):
        return self.sum + 9000

    def __what_goes_here__(self):
        return {'a': self.a, 'b': self.b, 'c': self.c}

Puedo convertirlo en un número entero muy fácilmente

>>> w = Wharrgarbl('one', 'two', 'three', 6)
>>> int(w)
9006

¡Lo cual es genial! Pero ahora quiero convertirlo en un dictado de manera similar.

>>> w = Wharrgarbl('one', 'two', 'three', 6)
>>> dict(w)
{'a': 'one', 'c': 'three', 'b': 'two'}

¿Qué necesito definir para que esto funcione? Intenté sustituir ambos __dict__y dictpor __what_goes_here__, pero dict(w)resultó en un TypeError: Wharrgarbl object is not iterableen ambos casos. No creo que simplemente hacer que la clase sea iterable resolverá el problema. También intenté muchos Google con tantas redacciones diferentes de "objeto de lanzamiento de python para dict" como pude pensar, pero no pude encontrar nada relevante: {

¡También! Observe cómo llamar w.__dict__no hará lo que quiero porque va a contener w.versiony w.sum. Quiero personalizar el reparto de dictla misma manera que puedo personalizar el reparto paraint usando def int(self).

Sé que podría hacer algo como esto

>>> w.__what_goes_here__()
{'a': 'one', 'c': 'three', 'b': 'two'}

Pero supongo que hay una forma pitónica de hacer que dict(w)funcione, ya que es el mismo tipo de cosas que int(w)ostr(w) . Si no hay una forma más pitónica, también está bien, pensé que preguntaría. Oh! Supongo que, dado que importa, esto es para Python 2.7, pero también puntos de súper bonificación para una solución 2.4 antigua y rota.

Hay otra pregunta sobrecarga de __dict __ () en la clase python que es similar a esta, pero puede ser lo suficientemente diferente como para justificar que no sea un duplicado. Creo que OP está preguntando cómo convertir todos los datos en sus objetos de clase como diccionarios. Estoy buscando un enfoque más personalizado en el sentido de que no quiero que todo __dict__incluido en el diccionario sea devuelto por dict(). Algo como variables públicas vs privadas puede ser suficiente para explicar lo que estoy buscando. Los objetos almacenarán algunos valores utilizados en los cálculos y que no necesito / quiero aparecer en los diccionarios resultantes.

ACTUALIZACIÓN: Elegí seguir la asdictruta sugerida, pero fue una decisión difícil seleccionar lo que quería que fuera la respuesta a la pregunta. Tanto @RickTeachey como @ jpmc26 proporcionaron la respuesta con la que voy a seguir, pero el primero tenía más información y opciones y también obtuvo el mismo resultado y recibió más votos, así que lo seguí. Sin embargo, todos votaron a favor y gracias por la ayuda. He estado al acecho largo y tendido en el desbordamiento de la pila y estoy tratando de meter más los dedos de los pies en el agua.

inclinación
fuente
1
has visto esto ya? docs.python.org/3/reference/…
Garrett R
@GarrettR Es útil, pero en realidad no revela mucha información sobre este tema en particular.
Zizouz212
Posible duplicado de Overloading __dict __ () en la clase Python
Rick apoya a Monica
@RickTeachey ciertamente es similar a la pregunta Sobrecarga ..., aunque estoy buscando una representación de dict más personalizable. Por ejemplo, el objeto puede estar almacenando cosas que no me importa ver en el dict.
inclinación
Su pregunta está redactada de manera engañosa, no desea convertir o convertir su objeto a una representación dictada (de modo que pueda recuperar completamente el objeto si lo convierte de nuevo). Solo desea crear una vista de representación dictada para (digamos) variables de instancia pública.
smci

Respuestas:

117

Hay al menos cinco seis formas. La forma preferida depende de cuál sea su caso de uso.

Opción 1: simplemente agregue un asdict()método.

Según la descripción del problema, consideraría mucho la asdictforma de hacer las cosas sugerida por otras respuestas. Esto se debe a que no parece que su objeto sea realmente una gran colección:

class Wharrgarbl(object):

    ...

    def asdict(self):
        return {'a': self.a, 'b': self.b, 'c': self.c}

El uso de las otras opciones a continuación podría resultar confuso para los demás, a menos que sea muy obvio exactamente qué miembros del objeto se iterarían y no se especificarían como pares clave-valor.

Opción 1a: herede su clase de 'typing.NamedTuple'(o la mayoría equivalente'collections.namedtuple' ) y use el _asdictmétodo proporcionado para usted.

from typing import NamedTuple

class Wharrgarbl(NamedTuple):
    a: str
    b: str
    c: str
    sum: int = 6
    version: str = 'old'

Usar una tupla con nombre es una forma muy conveniente de agregar muchas funcionalidades a su clase con un mínimo de esfuerzo, incluido un_asdict método . Sin embargo, una limitación es que, como se muestra arriba, el NT incluirá a todos los miembros en su_asdict .

Si hay miembros que no desea incluir en su diccionario, deberá modificar el _asdict resultado:

from typing import NamedTuple

class Wharrgarbl(NamedTuple):
    a: str
    b: str
    c: str
    sum: int = 6
    version: str = 'old'

    def _asdict(self):
        d = super()._asdict()
        del d['sum']
        del d['version']
        return d

Otra limitación es que NT es de solo lectura. Esto puede ser deseable o no.

Opcion 2: Implementar __iter__.

Así, por ejemplo:

def __iter__(self):
    yield 'a', self.a
    yield 'b', self.b
    yield 'c', self.c

Ahora puedes simplemente hacer:

dict(my_object)

Esto funciona porque el dict()constructor acepta un iterable de(key, value) pares para construir un diccionario. Antes de hacer esto, hágase la pregunta de si iterar el objeto como una serie de pares clave-valor de esta manera, aunque es conveniente para crear un dict, podría ser un comportamiento sorprendente en otros contextos. Por ejemplo, hágase la pregunta "¿cuál debería ser el comportamiento delist(my_object) ...?"

Además, tenga en cuenta que acceder a los valores directamente utilizando el elemento obtener obj["a"] sintaxis no funcionará y el desempaquetado de argumentos de palabras clave no funcionará. Para esos, necesitaría implementar el protocolo de mapeo.

Opción 3: implementar el protocolo de mapeo . Esto permite el comportamiento de acceso por clave, emitiendo a un dictsin usar__iter__ , y también proporciona un comportamiento de desempaquetado ( {**my_obj}) y un comportamiento de desempaquetado de palabras clave si todas las claves son cadenas ( dict(**my_obj)).

El protocolo de mapeo requiere que proporcione (como mínimo) dos métodos juntos: keys()y __getitem__.

class MyKwargUnpackable:
    def keys(self):
        return list("abc")
    def __getitem__(self, key):
        return dict(zip("abc", "one two three".split()))[key]

Ahora puedes hacer cosas como:

>>> m=MyKwargUnpackable()
>>> m["a"]
'one'
>>> dict(m)  # cast to dict directly
{'a': 'one', 'b': 'two', 'c': 'three'}
>>> dict(**m)  # unpack as kwargs
{'a': 'one', 'b': 'two', 'c': 'three'}

Como se mencionó anteriormente, si está utilizando una versión lo suficientemente nueva de Python, también puede descomprimir su objeto de protocolo de mapeo en un diccionario de comprensión como tal (y en este caso no es necesario que sus claves sean cadenas):

>>> {**m}
{'a': 'one', 'b': 'two', 'c': 'three'}

Tenga en cuenta que el protocolo de mapeo tiene prioridad sobre el __iter__método cuando se lanza un objeto a un dictdirectamente (sin usar el desempaquetado de kwarg, es decir dict(m)). Por lo tanto, es posible, y a veces conveniente, hacer que el objeto tenga un comportamiento diferente cuando se usa como iterable (por ejemplo, list(m)) frente a cuando se lanza a dict( dict(m)).

DESTACADO : El hecho de que PUEDA usar el protocolo de mapeo, NO significa que DEBE hacerlo . ¿Tiene realmente sentido que su objeto se transmita como un conjunto de pares clave-valor o como argumentos y valores de palabras clave? ¿Tiene sentido acceder a él mediante una tecla, al igual que un diccionario?

Si la respuesta a estas preguntas es afirmativa , probablemente sea una buena idea considerar la siguiente opción.

Opción 4: considere el uso del 'collections.abcmódulo ' .

Heredar su clase de 'collections.abc.Mappingo le 'collections.abc.MutableMappingindica a otros usuarios que, para todos los efectos, su clase es un mapeo * y se puede esperar que se comporte de esa manera.

Aún puede lanzar su objeto a un dicttal como lo necesite, pero probablemente haya pocas razones para hacerlo. Debido al tipo de pato , molestarse en convertir su objeto de mapeo en un dictsería un paso adicional innecesario la mayoría de las veces.

Esta respuesta también puede ser útil.

Como se indica en los comentarios a continuación: vale la pena mencionar que hacer esto de la forma abc esencialmente convierte su clase de objeto en una dictclase similar (asumiendo que usa MutableMappingy no la de solo lecturaMapping clase base de ). Todo lo que podría hacer con él dict, podría hacerlo con su propio objeto de clase. Esto puede ser deseable o no.

También considere mirar los abcs numéricos en el numbers módulo:

https://docs.python.org/3/library/numbers.html

Dado que también está lanzando su objeto a un int, podría tener más sentido convertir esencialmente su clase en unint para que el lanzamiento no sea necesario.

Opción 5: Analice el uso del dataclassesmódulo (solo Python 3.7), que incluye un asdict()método de utilidad conveniente .

from dataclasses import dataclass, asdict, field, InitVar

@dataclass
class Wharrgarbl(object):
    a: int
    b: int
    c: int
    sum: InitVar[int]  # note: InitVar will exclude this from the dict
    version: InitVar[str] = "old"

    def __post_init__(self, sum, version):
        self.sum = 6  # this looks like an OP mistake?
        self.version = str(version)

Ahora puedes hacer esto:

    >>> asdict(Wharrgarbl(1,2,3,4,"X"))
    {'a': 1, 'b': 2, 'c': 3}

Opción 6: uso typing.TypedDict, que se ha agregado en python 3.8 .

NOTA: Es probable que la opción 6 NO sea lo que buscan el OP u otros lectores según el título de esta pregunta. Consulte los comentarios adicionales a continuación.

class Wharrgarbl(TypedDict):
    a: str
    b: str
    c: str

El uso de esta opción, el objeto resultante es undict (énfasis: se no unaWharrgarbl ). No hay ninguna razón para "convertirlo" en un dictado (a menos que esté haciendo una copia).

Y dado que el objeto es adict , la firma de inicialización es idéntica a la de dicty, como tal, solo acepta argumentos de palabras clave u otro diccionario.

    >>> w = Wharrgarbl(a=1,b=2,b=3)
    >>> w
    {'a': 1, 'b': 2, 'c': 3}
    >>> type(w)
    <class 'dict'>

Enfatizado : la "clase" anterior Wharrgarblno es en realidad una clase nueva en absoluto. Es simplemente azúcar sintáctico para crear tiposdict objetos con campos de diferentes tipos para el verificador de tipos.

Como tal, esta opción puede ser bastante conveniente para señalar a los lectores de su código (y también a un verificador de tipos como mypy) que dictse espera que dicho objeto tenga claves específicas con tipos de valores específicos.

Pero esto significa que no puede, por ejemplo, agregar otros métodos, aunque puede probar:

class MyDict(TypedDict):
    def my_fancy_method(self):
        return "world changing result"

... pero no funcionará:

>>> MyDict().my_fancy_method()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
AttributeError: 'dict' object has no attribute 'my_fancy_method'

* "Mapeo" se ha convertido en el "nombre" estándar del dicttipo -como pato

Rick apoya a Monica
fuente
1
Esta es una forma de obtener el efecto que desea el OP, pero quizás debería aclarar que tiene otros efectos, por ejemplo for k, v in my_object, también estará habilitado.
zwol
@RickTeachey Veo a lo que estás conduciendo con el punto de asdict Ciertamente no necesito que los objetos sean iterables.
inclinación
Por qué podemos simplemente usar la función vars como se menciona en otro enlace /programming/61517/python-dictionary-from-an-objects-fields. ¿Me falta algo en esta pregunta?
usuario
@user La pregunta del PO quería sólo los miembros a, by cen el dict. La varsfunción devolverá un diccionario con TODOS los miembros del objeto, incluidos los que no se querían: sumy version. En algunos casos varsfuncionará, pero en mi experiencia no es realmente una pitón idiomática. Por lo general, la gente es más explícita, diciendo "ahora yo echo mi objeto a un diccionario", haciendo por ejemplo obj.asdict(). Con vars, suele ser algo más como obj_kwargs = vars(obj)seguido de MyObject(**obj_kwargs). En otras palabras: varsse usa principalmente para copiar objetos.
Rick apoya a Monica el
¿ self.__dict__Qué tal agregar un método que regrese ?
user32882
17

No existe un método mágico que haga lo que quieres. La respuesta es simplemente nombrarlo apropiadamente. asdictes una opción razonable para una conversión simple dict, inspirada principalmente en namedtuple. Sin embargo, su método obviamente contendrá una lógica especial que podría no ser inmediatamente obvia por ese nombre; está devolviendo solo un subconjunto del estado de la clase. Si puede encontrar un nombre un poco más detallado que comunique los conceptos con claridad, mucho mejor.

Otras respuestas sugieren usar __iter__, pero a menos que su objeto sea verdaderamente iterable (representa una serie de elementos), esto realmente tiene poco sentido y constituye un abuso incómodo del método. El hecho de que desee filtrar parte del estado de la clase hace que este enfoque sea aún más dudoso.

jpmc26
fuente
sí, devolver un subconjunto del estado completo de la clase, los valores de relevancia, es lo que estoy tratando de lograr. Tengo algunos valores "ocultos" almacenados que hacen las cosas más rápidas para los cálculos / comparaciones, etc., pero no son nada que un humano quiera ver o jugar y tampoco representan realmente el objeto en sí.
inclinación
@ tr3buchet En su código de ejemplo, esos valores se pasan al inicializador . ¿Es así en tu código real? Parece extraño que los cálculos almacenados en caché se pasen como argumentos.
jpmc26
en absoluto, solo lo estaba haciendo para dar un ejemplo
inclinación
Solo quiero tener en cuenta que los documentos parecen aprobar el uso __iter__como han sugerido otras respuestas: "Para las asignaciones, debe iterar sobre las claves del contenedor y también debe estar disponible como el método iterkeys ()".
dbenton
@dbenton Eso no es lo que está haciendo el OP o lo que recomiendan las otras respuestas. El objeto del OP no es un "contenedor", que en este contexto se usa básicamente como sinónimo de colección. El objeto del OP tampoco es lo que generalmente se entiende por mapeo. Un mapeo sería algo que tiene un número arbitrario de claves y valores y debería extender la Mappingclase base. Un objeto que tiene atributos con nombre específicos no se incluye en esta categoría.
jpmc26
11

algo como esto probablemente funcionaría

class MyClass:
    def __init__(self,x,y,z):
       self.x = x
       self.y = y
       self.z = z
    def __iter__(self): #overridding this to return tuples of (key,value)
       return iter([('x',self.x),('y',self.y),('z',self.z)])

dict(MyClass(5,6,7)) # because dict knows how to deal with tuples of (key,value)
Joran Beasley
fuente
¡Oh, interesante! un iterable de las tuplas, no consideré esto. Haré una prueba rápida
inclinación
Además, devolver un iter de una lista de tuplas instantáneas de todos los valores de x, y, z en la lista en la que se basa el iter. En el caso de las 3 declaraciones de rendimiento, es posible (una ventana pequeña, pero no obstante) que ejecutar esto en un entorno de subprocesos podría dar como resultado que otro subproceso modifique el atributo y o el atributo z de manera que los informes x, y y z no son consistentes.
PaulMcG
@PaulMcGuire: ¿CPython garantiza que todos serán consistentes incluso con el código de Joran? Hubiera pensado que se podría programar otro hilo, por ejemplo, después de que xse crea la primera tupla con , pero antes de la segunda tupla con y. ¿Me equivoco y, en realidad, la comprensión de la lista es atómica con respecto a la programación de subprocesos?
Steve Jessop
Ahora que preguntas, me doy cuenta de que ni siquiera la comprensión de la lista es atómica garantizada, ya que el conmutador de subprocesos de Python lo hace a nivel de código de bytes, no a nivel de fuente. Pero su exposición es solo durante la creación de la lista; una vez que se ha creado la lista, las actualizaciones de las propiedades no importan (a menos que los valores de las propiedades sean objetos mutables). Es mejor usar el dismódulo para ver los códigos de bytes reales.
PaulMcG
11

Creo que esto funcionará para ti.

class A(object):
    def __init__(self, a, b, c, sum, version='old'):
        self.a = a
        self.b = b
        self.c = c
        self.sum = 6
        self.version = version

    def __int__(self):
        return self.sum + 9000

    def __iter__(self):
        return self.__dict__.iteritems()

a = A(1,2,3,4,5)
print dict(a)

Salida

{'a': 1, 'c': 3, 'b': 2, 'suma': 6, 'versión': 5}

Garrett R
fuente
¿Qué pasó con la suma y la versión? ¿En qué se diferencia esto de simplemente acceder __dict__como se sugiere la otra respuesta?
Joran Beasley
no es dict. la llamada a iteritems devuelve un iterador
Garrett R
1
Me gusta esta respuesta ... pero necesita un poco más de trabajo antes de que cumpla con los requisitos del OP ...
Joran Beasley
esta respuesta realmente no es diferente de @JoranBeasley. La diferencia es que el suyo es selectivo, mientras que usted toma el conjunto self.__dict__que contiene elementos que no quiero tener en la versión de diccionario del objeto.
inclinación
6

Como muchos otros, sugeriría implementar una función to_dict () en lugar de (o además) permitir la conversión a un diccionario. Creo que hace más obvio que la clase admite ese tipo de funcionalidad. Podría implementar fácilmente un método como este:

def to_dict(self):
    class_vars = vars(MyClass)  # get any "default" attrs defined at the class level
    inst_vars = vars(self)  # get any attrs defined on the instance (self)
    all_vars = dict(class_vars)
    all_vars.update(inst_vars)
    # filter out private attributes
    public_vars = {k: v for k, v in all_vars.items() if not k.startswith('_')}
    return public_vars
tvt173
fuente
1

Es difícil de decir sin conocer todo el contexto del problema, pero no anularía __iter__ .

Lo implementaría __what_goes_here__en la clase.

as_dict(self:
    d = {...whatever you need...}
    return d
ruido waterphd
fuente
No, en eso se almacenan las variables.
Zizouz212
3
Señalé específicamente que w.__dict__no hace lo que necesito.
inclinación
Pero __dict__podría anularse, ¿no?
noisewaterphd
@noisewaterphd no, es un método mágico (tal vez podría anularlo, pero sus implicaciones dan miedo por decir lo menos)
Joran Beasley
1
¿Por qué no heredar de dict, o quizás mejor aún, simplemente crear un método en la clase para devolverlo como dict?
noisewaterphd
0

Estoy tratando de escribir una clase que sea "tanto" listcomo a dict. Quiero que el programador pueda "lanzar" este objeto a list(soltando las teclas) o dict(con las teclas).

Mirando la forma en que Python actualmente hace el dict()lanzamiento: llama Mapping.update()con el objeto que se pasa. Este es el código del repositorio de Python :

def update(self, other=(), /, **kwds):
    ''' D.update([E, ]**F) -> None.  Update D from mapping/iterable E and F.
        If E present and has a .keys() method, does:     for k in E: D[k] = E[k]
        If E present and lacks .keys() method, does:     for (k, v) in E: D[k] = v
        In either case, this is followed by: for k, v in F.items(): D[k] = v
    '''
    if isinstance(other, Mapping):
        for key in other:
            self[key] = other[key]
    elif hasattr(other, "keys"):
        for key in other.keys():
            self[key] = other[key]
    else:
        for key, value in other:
            self[key] = value
    for key, value in kwds.items():
        self[key] = value

El último subcaso de la instrucción if, en el que se repite, otheres el que la mayoría de la gente tiene en mente. Sin embargo, como puede ver, también es posible tener una keys()propiedad. Eso, combinado con un, __getitem__()debería facilitar que una subclase se transmita correctamente a un diccionario:

class Wharrgarbl(object):
    def __init__(self, a, b, c, sum, version='old'):
        self.a = a
        self.b = b
        self.c = c
        self.sum = 6
        self.version = version

    def __int__(self):
        return self.sum + 9000

    def __keys__(self):
        return ["a", "b", "c"]

    def __getitem__(self, key):
        # have obj["a"] -> obj.a
        return self.__getattribute__(key)

Entonces esto funcionará:

>>> w = Wharrgarbl('one', 'two', 'three', 6)
>>> dict(w)
{'a': 'one', 'c': 'three', 'b': 'two'}
Jérémie
fuente
Ha implementado el protocolo de mapeo. Vea la opción 3 en mi respuesta anterior. Si también desea listsoltar las claves, puede agregar un __iter__método que itera a través de los valores, y la conversión a dictseguirá funcionando. Tenga en cuenta que, por lo general, con los diccionarios, si envía a una lista, devolverá las CLAVES y no los valores que necesita. Esto podría ser sorprendente para otras personas que usen su código a menos que sea obvio por qué sucedería esto.
Rick apoya a Monica el