In python, come si esegue il cast di un oggetto di classe su un dict


89

Diciamo che ho una semplice classe in 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 lanciarlo a un numero intero molto facilmente

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

Che è grandioso! Ma ora voglio trasmetterlo a un dict in modo simile

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

Cosa devo definire affinché funzioni? Ho provato sostituendo sia __dict__e dictper __what_goes_here__, ma dict(w)ha determinato un TypeError: Wharrgarbl object is not iterablein entrambi i casi. Non credo che semplicemente rendere iterabile la classe risolverà il problema. Ho anche provato molti googles con tante diverse diciture di "python cast object to dict" che potevo pensare ma non sono riuscito a trovare nulla di rilevante: {

Anche! Nota come la chiamata w.__dict__non farà quello che voglio perché conterrà w.versione w.sum. Desidero personalizzare il cast dictnello stesso modo in cui posso personalizzare il cast intutilizzando def int(self).

So che potrei fare qualcosa del genere

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

Ma presumo che esista un modo pitonico di dict(w)lavorare poiché è lo stesso tipo di cose di int(w)o str(w). Se non c'è un modo più pitonico, va bene anche questo, ho solo pensato di chiedere. Oh! Immagino che dal momento che sia importante, questo è per Python 2.7, ma anche punti super bonus per una soluzione 2.4 vecchia e rotta.

C'è un'altra domanda sull'overload di __dict __ () su una classe python che è simile a questa ma potrebbe essere abbastanza diversa da garantire che non sia un duplicato. Credo che OP chieda come eseguire il cast di tutti i dati nei suoi oggetti di classe come dizionari. Sto cercando un approccio più personalizzato in quanto non voglio che tutto __dict__incluso nel dizionario venga restituito da dict(). Qualcosa come le variabili pubbliche o private può essere sufficiente per spiegare cosa sto cercando. Gli oggetti memorizzeranno alcuni valori usati nei calcoli e tali che non ho bisogno / voglio essere visualizzati nei dizionari risultanti.

AGGIORNAMENTO: Ho scelto di seguire il asdictpercorso suggerito ma è stata una scelta difficile selezionare quella che volevo fosse la risposta alla domanda. Sia @RickTeachey che @ jpmc26 hanno fornito la risposta con cui andrò a rotolare, ma il primo aveva più informazioni e opzioni e ha ottenuto lo stesso risultato ed è stato votato di più, quindi l'ho seguito. Voti positivi ovunque e grazie per l'aiuto. Sono rimasto in agguato a lungo e duramente su Stackoverflow e sto cercando di mettere le dita dei piedi nell'acqua di più.



@GarrettR È utile, ma in realtà non rivela molte informazioni su questo particolare problema.
Zizouz212


@ RickTeachey è certamente simile alla domanda Overloading ..., anche se sto cercando una rappresentazione dei dict più personalizzabile. Ad esempio, l'oggetto potrebbe memorizzare cose che non mi interessa vedere nel dict.
inclinazione

La tua domanda è formulata in modo fuorviante, non vuoi convertire o lanciare il tuo oggetto per dettare la rappresentazione (in modo tale da poter recuperare completamente l'oggetto se lo riconvertivi). Vuoi solo creare una vista di rappresentazione dict per (diciamo) variabili di istanza pubbliche.
smci

Risposte:


124

Ci sono almeno cinque sei modi. Il modo preferito dipende dal tuo caso d'uso.

Opzione 1: aggiungi semplicemente un asdict()metodo.

Sulla base della descrizione del problema, prenderei in considerazione molto il asdictmodo di fare le cose suggerito da altre risposte. Questo perché non sembra che il tuo oggetto sia davvero una collezione:

class Wharrgarbl(object):

    ...

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

L'utilizzo delle altre opzioni di seguito potrebbe creare confusione per gli altri, a meno che non sia molto ovvio esattamente quali membri dell'oggetto verrebbero e non sarebbero iterati o specificati come coppie chiave-valore.

Opzione 1a: eredita la tua classe da 'typing.NamedTuple'(o la maggior parte equivalente 'collections.namedtuple') e utilizza il _asdictmetodo fornito per te.

from typing import NamedTuple

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

L'uso di una tupla con nome è un modo molto conveniente per aggiungere molte funzionalità alla classe con il minimo sforzo, incluso un _asdictmetodo . Tuttavia, una limitazione è che, come mostrato sopra, il NT includerà tutti i membri nel suo file _asdict.

Se ci sono membri che non desideri includere nel dizionario, dovrai modificare il _asdictrisultato:

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

Un'altra limitazione è che NT è di sola lettura. Questo può o non può essere desiderabile.

Opzione 2: implementare __iter__.

In questo modo, ad esempio:

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

Ora puoi semplicemente fare:

dict(my_object)

Questo funziona perché il dict()costruttore accetta un iterabile di (key, value)coppie per costruire un dizionario. Prima di fare ciò, chiediti se l'iterazione dell'oggetto come una serie di coppie di chiavi e valori in questo modo, mentre è conveniente per creare un dict, potrebbe effettivamente essere un comportamento sorprendente in altri contesti. Ad esempio, poniti la domanda "quale dovrebbe essere il comportamento di list(my_object)...?"

Inoltre, tieni presente che l'accesso diretto ai valori utilizzando la obj["a"]sintassi get item non funzionerà e lo spacchettamento degli argomenti delle parole chiave non funzionerà. Per quelli, dovresti implementare il protocollo di mappatura.

Opzione 3: implementare il protocollo di mappatura . Ciò consente il comportamento di accesso tramite chiave, il cast a un dictsenza l'utilizzo __iter__e fornisce anche il comportamento di decompressione ( {**my_obj}) e il comportamento di decompressione delle parole chiave se tutte le chiavi sono stringhe ( dict(**my_obj)).

Il protocollo di mappatura richiede che tu fornisca (almeno) due metodi insieme: keys()e __getitem__.

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

Ora puoi fare cose come:

>>> 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'}

Come accennato in precedenza, se stai usando una versione abbastanza nuova di python puoi anche decomprimere il tuo oggetto protocollo di mappatura in una comprensione del dizionario in questo modo (e in questo caso non è necessario che le tue chiavi siano stringhe):

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

Si noti che il protocollo mappatura prevale sulla il __iter__metodo quando esprimono un oggetto a un dictdirettamente (senza utilizzare kwarg disimballaggio, cioè dict(m)). Quindi è possibile, e talvolta conveniente, fare in modo che l'oggetto abbia un comportamento diverso quando usato come iterabile (ad esempio list(m)) rispetto a quando viene lanciato a dict( dict(m)).

SOTTOLINEATO : Solo perché PUOI usare il protocollo di mappatura, NON significa che DOVRESTI farlo . Ha davvero senso che il tuo oggetto venga trasmesso come un insieme di coppie chiave-valore o come argomenti e valori di parole chiave? Accedervi per chiave, proprio come un dizionario, ha davvero senso?

Se la risposta a queste domande è , è probabilmente una buona idea considerare l'opzione successiva.

Opzione 4: cerca di utilizzare il 'collections.abcmodulo " .

Ereditare la tua classe 'collections.abc.Mappingo 'collections.abc.MutableMappingsegnala ad altri utenti che, a tutti gli effetti, la tua classe è una mappatura * e ci si può aspettare che si comporti in quel modo.

Puoi ancora lanciare il tuo oggetto su un dictcome richiesto, ma probabilmente ci sarebbero pochi motivi per farlo. A causa della dattilografia , preoccuparsi di lanciare il proprio oggetto di mappatura su a dictsarebbe solo un passaggio aggiuntivo non necessario per la maggior parte del tempo.

Questa risposta potrebbe anche essere utile.

Come notato nei commenti seguenti: vale la pena ricordare che facendo questo in modo abc si trasforma essenzialmente la classe di oggetti in una dictclasse simile (supponendo che si usi MutableMappinge non la Mappingclasse base di sola lettura ). Tutto ciò che potresti fare con dict, potresti farlo con il tuo oggetto di classe. Questo può essere o non essere desiderabile.

Considera anche di guardare gli abcs numerici nel numbersmodulo:

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

Dal momento che stai anche lanciando il tuo oggetto su un int, potrebbe avere più senso trasformare essenzialmente la tua classe in una vera e propria classe in intmodo che il casting non sia necessario.

Opzione 5: cerca di utilizzare il dataclassesmodulo (solo Python 3.7), che include un comodo asdict()metodo di utilità.

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)

Ora puoi farlo:

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

Opzione 6: Usa typing.TypedDict, che è stata aggiunta in Python 3.8 .

NOTA: l'opzione 6 probabilmente NON è ciò che l'OP, o altri lettori in base al titolo di questa domanda, stanno cercando. Vedere ulteriori commenti di seguito.

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

Utilizzando questa opzione, l'oggetto risultante è undict (enfasi: esso è non una Wharrgarbl). Non c'è alcun motivo per "lanciarlo" su un dict (a meno che tu non ne stia facendo una copia).

E poiché l'oggetto è adict , la firma di inizializzazione è identica a quella di dicte come tale accetta solo argomenti di parole chiave o un altro dizionario.

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

Sottolineato : la "classe" di cui sopra Wharrgarblnon è affatto una nuova classe. È semplicemente zucchero sintattico per creare dictoggetti digitati con campi di diversi tipi per il controllo del tipo.

In quanto tale, questa opzione può essere piuttosto comoda per segnalare ai lettori del codice (e anche a un controllore di tipo come mypy) che dictci si aspetta che un tale oggetto abbia chiavi specifiche con tipi di valore specifici.

Ma questo significa che non puoi, ad esempio, aggiungere altri metodi, anche se puoi provare:

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

... ma non funzionerà:

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

* "Mappatura" è diventato il "nome" standard del dicttipo di anatra


1
Questo è un modo per ottenere l'effetto desiderato dall'OP, ma forse dovresti chiarire che ha altri effetti, ad esempio for k, v in my_objectsarà anche abilitato.
zwol

@ RickTeachey Vedo cosa stai guidando con il punto asdict. Certamente non ho bisogno che gli oggetti siano iterabili.
inclinazione

Perché possiamo semplicemente usare la funzione vars come menzionato in altri linkhttps: //stackoverflow.com/questions/61517/python-dictionary-from-an-objects-fields. Mi manca qualcosa in questa domanda?
utente

@user La domanda del PO voleva solo i membri a, be cin dict. La varsfunzione restituirà un dizionario con TUTTI i membri dell'oggetto inclusi quelli che non erano desiderati: sume version. In alcuni casi varsfunzionerà, ma nella mia esperienza non è davvero idiomatico python. Di solito le persone sono più esplicite, dicendo "Ora sto trasmettendo il mio oggetto a un dizionario" facendo ad es obj.asdict(). Con vars, di solito è qualcosa di più simile a obj_kwargs = vars(obj)seguito da MyObject(**obj_kwargs). In altre parole: varsviene utilizzato principalmente per copiare oggetti.
Rick sostiene Monica

che ne dici di aggiungere un metodo che restituisce self.__dict__?
user32882

19

Non esiste un metodo magico che faccia quello che vuoi. La risposta è semplicemente nominarlo in modo appropriato. asdictè una scelta ragionevole per una semplice conversione a dict, ispirata principalmente da namedtuple. Tuttavia, il tuo metodo conterrà ovviamente una logica speciale che potrebbe non essere immediatamente ovvia da quel nome; stai restituendo solo un sottoinsieme dello stato della classe. Se riesci a trovare un nome leggermente più prolisso che comunichi chiaramente i concetti, tanto meglio.

Altre risposte suggeriscono l'utilizzo __iter__, ma a meno che il tuo oggetto non sia veramente iterabile (rappresenta una serie di elementi), questo ha davvero poco senso e costituisce un imbarazzante abuso del metodo. Il fatto che tu voglia filtrare parte dello stato di classe rende questo approccio ancora più dubbio.


sì, sto cercando di ottenere una restituzione di un sottoinsieme dell'intero stato della classe, i valori di rilevanza. Ho alcuni valori "nascosti" memorizzati che rendono le cose più veloci per calcoli / confronti, ecc., Ma non sono nulla che un essere umano vorrebbe vedere o con cui giocare e inoltre non rappresentano realmente l'oggetto stesso.
inclinazione

@ tr3buchet Nel codice di esempio, questi valori vengono passati all'inizializzatore . È così che funziona nel tuo codice reale? Sembra strano che i calcoli memorizzati nella cache vengano passati come argomenti.
jpmc26

per niente, lo stavo facendo solo per fornire un esempio
inclinazione

Voglio solo notare che i documenti sembrano condonare l'uso __iter__come altre risposte hanno suggerito: "Per i mapping, dovrebbe iterare sulle chiavi del contenitore e dovrebbe anche essere reso disponibile come metodo iterkeys ()."
dbenton

@dbenton Questo non è ciò che sta facendo l'OP o ciò che le altre risposte consigliano. L'oggetto dell'OP non è un "contenitore", che in questo contesto viene fondamentalmente utilizzato come sinonimo di raccolta. Anche l'oggetto dell'OP non è ciò che di solito si intende per mappatura. Una mappatura sarebbe qualcosa che ha un numero arbitrario di chiavi e valori e dovrebbe estendere la Mappingclasse di base. Un oggetto con attributi denominati specifici non rientra in questa categoria.
jpmc26

11

qualcosa del genere probabilmente funzionerebbe

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)

oh interessante! un iterabile delle tuple, non l'ho considerato. Farò un rapido test
inclinazione

Inoltre, restituendo un iter di un elenco di istantanee di tuple, tutti i valori di x, yez nell'elenco su cui si basa l'iter. Nel caso in cui le 3 istruzioni yield, è possibile (una piccola finestra, ma ciononostante) che eseguendolo in un ambiente con thread potrebbe risultare in un altro thread che modifica l'attributo y o l'attributo z in modo tale che x, y e z non sono coerenti.
PaulMcG,

@PaulMcGuire: CPython garantisce che saranno tutti coerenti anche con il codice di Joran? Avrei pensato che fosse possibile programmare un altro thread, ad esempio, dopo la xcreazione della prima tupla con , ma prima della seconda tupla con y. Mi sbaglio, e in realtà la comprensione della lista è atomica rispetto alla schedulazione dei thread?
Steve Jessop,

Ora che me lo chiedi, mi rendo conto che anche la comprensione della lista non è garantita atomica, poiché lo switcher di thread di Python lo fa a livello di bytecode, non a livello di sorgente. Ma l'esposizione è solo per la durata della creazione dell'elenco: una volta che l'elenco è stato creato, qualsiasi aggiornamento alle proprietà non avrà importanza (a meno che i valori delle proprietà non siano oggetti modificabili). Meglio usare il dismodulo per visualizzare i bytecode effettivi.
PaulMcG,

11

Penso che questo funzionerà per te.

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)

Produzione

{'a': 1, 'c': 3, 'b': 2, 'sum': 6, 'version': 5}


che fine ha fatto somma e versione? in che modo è diverso dal semplice accesso __dict__come suggerito dall'altra risposta?
Joran Beasley,

non è vero. la chiamata a iteritems restituisce un iteratore
Garrett R

1
Mi piace questa risposta ... ma ha bisogno di un po 'più di lavoro prima di soddisfare i requisiti dell'OP ...
Joran Beasley,

questa risposta non è davvero diversa da @JoranBeasley. La differenza è che il suo è selettivo mentre il tuo prende il tutto self.__dict__che contiene elementi che non voglio avere nella versione del dizionario dell'oggetto.
inclinazione

6

Come molti altri, suggerirei di implementare una funzione to_dict () piuttosto che (o in aggiunta a) consentire il casting su un dizionario. Penso che renda più ovvio che la classe supporta quel tipo di funzionalità. Potresti facilmente implementare un metodo come questo:

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

1

È difficile dirlo senza conoscere l'intero contesto del problema, ma non ignorerei __iter__.

Implementerei __what_goes_here__sulla classe.

as_dict(self:
    d = {...whatever you need...}
    return d

No, questo è ciò in cui sono memorizzate le variabili.
Zizouz212

3
Ho specificamente sottolineato che w.__dict__non fa ciò di cui ho bisogno.
inclinazione

Ma __dict__potrebbe essere sovrascritto, no?
noisewaterphd

@noisewaterphd no, è un metodo magico (forse potresti ignorarlo, ma le implicazioni sono a dir poco spaventose)
Joran Beasley,

1
Perché non ereditare da dict, o forse meglio ancora, creare semplicemente un metodo sulla classe per restituirlo come un dict.
noisewaterphd

0

Sto cercando di scrivere una classe che sia "sia" listao a dict. Voglio che il programmatore sia in grado di "lanciare" questo oggetto su a list(lasciando cadere le chiavi) o dict(con le chiavi).

Guardando il modo in cui Python attualmente esegue il dict()cast: chiama Mapping.update()con l'oggetto che viene passato. Questo è il codice dal repository 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

L'ultimo sottocaso dell'istruzione if, su cui sta iterando, otherè quello che la maggior parte delle persone ha in mente. Tuttavia, come puoi vedere, è anche possibile avere una keys()proprietà. Ciò, combinato con a, __getitem__()dovrebbe rendere più semplice il cast di una sottoclasse in un dizionario:

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)

Quindi questo funzionerà:

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

Hai implementato il protocollo di mappatura. Vedi l' opzione 3 nella mia risposta sopra. Se vuoi anche listrilasciare le chiavi, puoi aggiungere un __iter__metodo che itera tra i valori e il casting dictcontinuerà a funzionare. Nota che di solito con i dizionari, se esegui il cast su un elenco, restituirà le CHIAVI e non i valori richiesti. Questo potrebbe essere sorprendente per altre persone che usano il tuo codice a meno che non sia ovvio il motivo per cui ciò dovrebbe accadere.
Rick sostiene Monica il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.