Em python, como faço para converter um objeto de classe para um dicionário

89

Digamos que tenho uma classe simples de 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}

Posso convertê-lo em um número inteiro muito facilmente

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

O que é ótimo! Mas, agora eu quero convertê-lo para um dict de uma maneira semelhante

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

O que preciso definir para que isso funcione? Tentei substituir ambos __dict__e dictpor __what_goes_here__, mas dict(w)resultou em um TypeError: Wharrgarbl object is not iterableem ambos os casos. Não acho que simplesmente tornar a classe iterável resolverá o problema. Eu também tentei muitos googles com tantas formulações diferentes de "python cast object to dict" quanto eu poderia pensar, mas não consegui encontrar nada relevante: {

Além disso! Observe como chamar w.__dict__não fará o que eu quero porque conterá w.versione w.sum. Quero personalizar o elenco dictda mesma maneira que posso personalizar o elenco intusando def int(self).

Eu sei que poderia simplesmente fazer algo assim

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

Mas estou assumindo que existe uma maneira pítônica de fazer dict(w)funcionar, uma vez que é o mesmo tipo de coisa que int(w)ou str(w). Se não existe uma forma mais python, tudo bem também, só pensei em perguntar. Oh! Eu acho que já que é importante, isso é para o python 2.7, mas super pontos de bônus para uma solução 2.4 antiga e quebrada também.

Há outra questão Sobrecarregando __dict __ () na classe Python que é semelhante a esta, mas pode ser diferente o suficiente para garantir que não seja uma duplicata. Eu acredito que OP está perguntando como converter todos os dados em seus objetos de classe como dicionários. Estou procurando uma abordagem mais personalizada, já que não quero que tudo __dict__incluído no dicionário retornado por dict(). Algo como variáveis ​​públicas x privadas pode ser suficiente para explicar o que estou procurando. Os objetos estarão armazenando alguns valores usados ​​nos cálculos e tal que não preciso / quero que apareçam nos dicionários resultantes.

ATUALIZAÇÃO: Escolhi seguir o asdictcaminho sugerido, mas foi uma escolha difícil selecionar o que eu queria ser a resposta à pergunta. Tanto @RickTeachey quanto @ jpmc26 forneceram a resposta que vou apresentar, mas o primeiro tinha mais informações e opções e também obteve o mesmo resultado e foi mais votado, então eu aceitei. Votos positivos por todos e obrigado pela ajuda. Eu espreitei por muito tempo e duramente no overflow e estou tentando colocar meus pés na água mais.

inclinação
fonte
1
você já viu isso? docs.python.org/3/reference/…
Garrett R
@GarrettR É útil, mas realmente não revela muitos insights sobre esse problema específico.
Zizouz212
Possível duplicata de Overloading __dict __ () na classe Python
Rick oferece suporte a Monica de
@RickTeachey certamente é semelhante à questão Sobrecarga ..., embora eu esteja procurando uma representação de dicionário mais personalizável. Por exemplo, o objeto pode armazenar coisas que não me interessa ver no dicionário.
penchant de
Sua pergunta foi formulada de maneira enganosa, você não deseja converter ou lançar seu objeto para uma representação de dicionário (de forma que você poderia recuperar completamente o objeto se convertesse de volta). Você só deseja criar uma visualização de representação de dicionário para (digamos) variáveis ​​de instância pública.
smci de

Respostas:

124

Existem pelo menos cinco seis maneiras. A forma preferida depende de qual é o seu caso de uso.

Opção 1: basta adicionar um asdict()método.

Com base na descrição do problema, eu consideraria bastante a asdictmaneira de fazer as coisas sugerida por outras respostas. Isso ocorre porque não parece que seu objeto seja realmente uma coleção:

class Wharrgarbl(object):

    ...

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

Usar as outras opções abaixo pode ser confuso para os outros, a menos que seja muito óbvio exatamente quais membros do objeto seriam e não seriam iterados ou especificados como pares chave-valor.

Opção 1a: Herdar sua classe 'typing.NamedTuple'(ou o equivalente principalmente 'collections.namedtuple') e usar o _asdictmétodo fornecido para você.

from typing import NamedTuple

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

Usar uma tupla nomeada é uma maneira muito conveniente de adicionar muitas funcionalidades à sua classe com um mínimo de esforço, incluindo um _asdictmétodo . No entanto, uma limitação é que, conforme mostrado acima, o NT incluirá todos os seus membros _asdict.

Se houver membros que você não deseja incluir em seu dicionário, você precisará modificar o _asdictresultado:

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

Outra limitação é que o NT é somente leitura. Isso pode ou não ser desejável.

Opção 2: Implementar __iter__.

Assim, por exemplo:

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

Agora você pode apenas fazer:

dict(my_object)

Isso funciona porque o dict()construtor aceita um iterável de (key, value)pares para construir um dicionário. Antes de fazer isso, pergunte a si mesmo se iterar o objeto como uma série de pares de chave e valor dessa maneira - embora seja conveniente para criar um dict- pode realmente ser um comportamento surpreendente em outros contextos. Por exemplo, pergunte a si mesmo "qual deveria ser o comportamento de list(my_object)...?"

Além disso, observe que acessar valores diretamente usando a obj["a"]sintaxe get item não funcionará e a descompactação de argumento de palavra-chave não funcionará. Para eles, você precisa implementar o protocolo de mapeamento.

Opção 3: implemente o protocolo de mapeamento . Isso permite o comportamento de acesso por chave, lançando para um dictsem usar __iter__, e também fornece comportamento de descompactação ( {**my_obj}) e comportamento de descompactação de palavra-chave se todas as chaves forem strings ( dict(**my_obj)).

O protocolo de mapeamento requer que você forneça (no mínimo) dois métodos juntos: keys()e __getitem__.

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

Agora você pode fazer coisas 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 mencionado acima, se você estiver usando uma versão nova o suficiente de python, você também pode descompactar seu objeto de protocolo de mapeamento em uma compreensão de dicionário como esta (e neste caso não é necessário que suas chaves sejam strings):

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

Observe que o protocolo de mapeamento tem precedência sobre o __iter__método ao lançar um objeto para um dictdiretamente (sem usar o desempacotamento kwarg, isto é dict(m)). Portanto, é possível - e às vezes conveniente - fazer com que o objeto tenha um comportamento diferente quando usado como um iterável (por exemplo, list(m)) vs. quando convertido para a dict( dict(m)).

DESTACADO : Só porque você PODE usar o protocolo de mapeamento, NÃO significa que você DEVE fazê-lo . Faz sentido que seu objeto seja transmitido como um conjunto de pares de valores-chave ou como argumentos e valores de palavras-chave? Acessar por chave - assim como um dicionário - realmente faz sentido?

Se a resposta a essas perguntas for sim , provavelmente é uma boa ideia considerar a próxima opção.

Opção 4: tente usar o 'collections.abcmódulo ' .

Herdar sua classe de 'collections.abc.Mappingou 'collections.abc.MutableMappingsinaliza para outros usuários que, para todos os efeitos, sua classe é um mapeamento * e pode-se esperar que se comporte dessa maneira.

Você ainda pode lançar seu objeto para um dictconforme necessário, mas provavelmente haveria poucos motivos para isso. Por causa da digitação de pato , incomodar-se em lançar seu objeto de mapeamento para a dictseria apenas uma etapa adicional desnecessária na maioria das vezes.

Essa resposta também pode ser útil.

Como observado nos comentários abaixo: vale a pena mencionar que fazer isso da maneira abc essencialmente transforma sua classe de objeto em uma dictclasse semelhante (assumindo que você usa MutableMappinge não a Mappingclasse base somente leitura ). Tudo com o que você seria capaz de fazer dict, você poderia fazer com seu próprio objeto de classe. Isso pode ser, ou não, desejável.

Considere também observar os abcs numéricos no numbersmódulo:

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

Como você também está lançando seu objeto para um int, pode fazer mais sentido essencialmente transformar sua classe em um completo intpara que o lançamento não seja necessário.

Opção 5: tente usar o dataclassesmódulo (somente Python 3.7), que inclui um asdict()método utilitário 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)

Agora você pode fazer isso:

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

Opção 6: uso typing.TypedDict, que foi adicionado ao python 3.8 .

NOTA: a opção 6 provavelmente NÃO é o que o OP ou outros leitores com base no título desta pergunta estão procurando. Veja comentários adicionais abaixo.

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

Usando esta opção, o objeto resultante é umadict (ênfase: é não um Wharrgarbl). Não há razão alguma para "convertê-lo" em um dicionário (a menos que você esteja fazendo uma cópia).

E como o objeto é adict , a assinatura de inicialização é idêntica à de dicte, como tal, só aceita argumentos de palavra-chave ou outro dicionário.

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

Enfatizado : a "classe" acima Wharrgarblnão é realmente uma classe nova. É simplesmente um açúcar sintático para criar dictobjetos digitados com campos de tipos diferentes para o verificador de tipo.

Como tal, essa opção pode ser muito conveniente para sinalizar aos leitores de seu código (e também a um verificador de tipo como mypy) que dictse espera que esse objeto tenha chaves específicas com tipos de valor específicos.

Mas isso significa que você não pode, por exemplo, adicionar outros métodos, embora possa tentar:

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

... mas não vai 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'

* "Mapeamento" tornou-se o "nome" padrão do dicttipo de pato semelhante

Rick apóia a monica
fonte
1
Esta é uma maneira de obter o efeito que o OP deseja, mas talvez você deva esclarecer que tem outros efeitos, por exemplo for k, v in my_object, também será ativado.
zwol
@RickTeachey Vejo o que você está querendo dizer com relação ao asdict. Certamente não preciso que os objetos sejam iteráveis.
penchant de
Por que podemos simplesmente usar a função vars conforme mencionado em outro linkhttps: //stackoverflow.com/questions/61517/python-dictionary-from-an-objects-fields. Estou faltando alguma coisa nesta pergunta?
usuário
@user A pergunta do OP queria apenas membros a, be cno dict. A varsfunção retornará um dicionário com TODOS os membros do objeto incluindo aqueles que não eram desejados: sume version. Em alguns casos varsfuncionará, mas não é realmente python idiomática na minha experiência. Normalmente as pessoas são mais explícitos, dizendo: "Eu agora estou lançando meu objeto de um dicionário", fazendo, por exemplo obj.asdict(). Com vars, geralmente é algo mais parecido com obj_kwargs = vars(obj)seguido por MyObject(**obj_kwargs). Em outras palavras: varsé usado principalmente para copiar objetos.
Rick apoia Monica de
que tal adicionar um método que retorna self.__dict__?
user32882
19

Não existe um método mágico que fará o que você quiser. A resposta é simplesmente nomeá-lo apropriadamente. asdicté uma escolha razoável para uma conversão simples para dict, inspirada principalmente pornamedtuple . No entanto, seu método obviamente conterá uma lógica especial que pode não ser imediatamente óbvia a partir desse nome; você está retornando apenas um subconjunto do estado da classe. Se você puder sugerir um nome um pouco mais prolixo que comunique os conceitos claramente, tanto melhor.

Outras respostas sugerem o uso __iter__, mas a menos que seu objeto seja verdadeiramente iterável (represente uma série de elementos), isso realmente faz pouco sentido e constitui um abuso estranho do método. O fato de você querer filtrar parte do estado da classe torna essa abordagem ainda mais duvidosa.

jpmc26
fonte
sim, retornar um subconjunto de todo o estado da classe, os valores de relevância, é o que estou tentando alcançar. Eu tenho alguns valores "ocultos" armazenados que tornam as coisas mais rápidas para cálculos / comparações etc, mas eles não são nada que um ser humano gostaria de ver ou brincar e também não representam realmente o objeto em si.
penchant
@ tr3buchet Em seu código de exemplo, esses valores são passados ​​para o inicializador . É assim que funciona no seu código real? Parece estranho que os cálculos em cache sejam passados ​​como argumentos.
jpmc26
de forma alguma, eu estava fazendo isso apenas para dar um exemplo
tendência de
Só quero observar que os documentos parecem tolerar o uso __iter__como outras respostas sugeriram: "Para mapeamentos, ele deve iterar sobre as chaves do contêiner e também deve ser disponibilizado como o método iterkeys ()."
dbenton
@dbenton Não é isso que o OP está fazendo ou o que as outras respostas recomendam. O objeto do OP não é um "container", que neste contexto está sendo usado basicamente como sinônimo de coleção. O objeto do OP também não é o que geralmente se entende por mapeamento. Um mapeamento seria algo que possui um número arbitrário de chaves e valores e deveria estender a Mappingclasse base. Um objeto que possui atributos nomeados específicos não se enquadra nesta categoria.
jpmc26
11

algo assim provavelmente funcionaria

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
fonte
oh interessante! um iterável das tuplas, não considerei isso. Vou fazer um teste rápido
tendência de
Além disso, o retorno de um iter de uma lista de tuplas captura instantâneos de todos os valores de x, y e z para a lista na qual o iter é baseado. No caso das 3 declarações de rendimento, é possível (uma pequena janela, mas não obstante) que executá-lo em um ambiente encadeado pode resultar em outro encadeamento modificando o atributo y ou o atributo z, de modo que x, y e z não são consistentes.
PaulMcG
@PaulMcGuire: CPython garante que todos serão consistentes mesmo com o código de Joran? Eu teria pensado que outro encadeamento poderia ser agendado, por exemplo, após a xcriação da primeira tupla com , mas antes da segunda tupla com y. Estou errado e, na verdade, a compreensão da lista é atômica em relação ao agendamento de threads?
Steve Jessop
Agora que você perguntou, eu percebi que mesmo a compreensão da lista não é garantida como atômica, já que o switcher de thread do Python faz isso no nível de bytecode, não no nível de origem. Mas sua exposição é apenas durante a criação da lista - uma vez que a lista foi criada, quaisquer atualizações nas propriedades não importarão (a menos que os valores das propriedades sejam objetos mutáveis). Melhor usar o dismódulo para ver os bytecodes reais.
PaulMcG de
11

Eu acho que isso vai funcionar para você.

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)

Resultado

{'a': 1, 'c': 3, 'b': 2, 'soma': 6, 'versão': 5}

Garrett R
fonte
o que aconteceu com soma e versão? como isso é diferente de apenas acessar __dict__como a outra resposta sugerida?
Joran Beasley
não é dict. a chamada para iteritems retorna um iterador
Garrett R
1
Eu gosto desta resposta ... mas ela precisa de um pouco mais de trabalho antes de atender aos requisitos do OP ...
Joran Beasley
essa resposta realmente não é diferente de @JoranBeasley. A diferença é que o dele é seletivo, ao passo que você pega o todo self.__dict__que contém itens que não quero ter na versão do dicionário do objeto.
penchant
6

Como muitos outros, eu sugeriria implementar uma função to_dict () em vez de (ou além de) permitir a conversão para um dicionário. Acho que fica mais óbvio que a classe suporta esse tipo de funcionalidade. Você poderia implementar facilmente um 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
fonte
1

É difícil dizer sem conhecer todo o contexto do problema, mas eu não iria ignorar __iter__.

Gostaria de implementar __what_goes_here__na aula.

as_dict(self:
    d = {...whatever you need...}
    return d
noisewaterphd
fonte
Não, é nisso que as variáveis ​​são armazenadas.
Zizouz212
3
Eu apontei especificamente que w.__dict__não faz o que eu preciso.
penchant
Mas __dict__poderia ser anulado, não é?
noisewaterphd
@noisewaterphd não, é um método mágico (talvez você pudesse substituí-lo, mas as implicações são assustadoras para dizer o mínimo)
Joran Beasley
1
Por que não herdar de dict, ou talvez melhor ainda, apenas criar um método na classe para retorná-lo como um dict.
noisewaterphd de
0

Estou tentando escrever uma classe que seja "ao mesmo tempo" a listou a dict. Eu quero que o programador seja capaz de "lançar" este objeto para um list(soltar as chaves) oudict (com as chaves).

Observando a maneira como o Python atualmente faz o dict()elenco: ele chama Mapping.update()com o objeto que é passado. Este é o código do repositório 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

O último subcaso da instrução if, onde está iterando, otheré o que a maioria das pessoas tem em mente. Porém, como vê, também é possível ter um keys()imóvel. Isso, combinado com um, __getitem__()deve facilitar que uma subclasse seja devidamente convertida em um dicionário:

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)

Então isso vai funcionar:

>>> w = Wharrgarbl('one', 'two', 'three', 6)
>>> dict(w)
{'a': 'one', 'c': 'three', 'b': 'two'}
Jérémie
fonte
Você implementou o protocolo de mapeamento. Veja a opção 3 na minha resposta acima. Se você também deseja fazer um listdrop drop das chaves, você pode adicionar um __iter__método que itera através dos valores, e a conversão para dictcontinuará a funcionar. Observe que, normalmente, com dicionários, se você lançar para uma lista, ele retornará as CHAVES, e não os valores que você precisa. Isso pode ser surpreendente para outras pessoas que usam seu código, a menos que seja óbvio por que isso aconteceria.
Rick apoia Monica em