Cosa sono le classi di dati e in che cosa differiscono dalle classi comuni?


141

Con PEP 557 le classi di dati vengono introdotte nella libreria standard di Python.

Usano il @dataclassdecoratore e dovrebbero essere "nomi mutabili con default" ma non sono sicuro di capire cosa significhi effettivamente e come siano diversi dalle classi comuni.

Cosa sono esattamente le classi di dati Python e quando è meglio usarle?


8
Dato l'ampio contenuto del PEP, cos'altro potresti sapere? namedtuplesono immutabili e non possono avere valori predefiniti per gli attributi, mentre le classi di dati sono mutabili e possono averli.
jonrsharpe,

31
@jonrsharpe Mi sembra ragionevole che ci dovrebbe essere un thread stackoverflow sull'argomento. Stackoverflow è pensato per essere un'enciclopedia in formato domande e risposte, no? La risposta non è mai "basta guardare su questo altro sito Web". Non avrebbero dovuto esserci downvotes qui.
Luke Davis,

12
Esistono cinque thread su come aggiungere un elemento a un elenco. Una domanda su @dataclassnon farà disintegrare il sito.
eric,

2
@jonrsharpe namedtuplesCAN ha valori predefiniti. Dai un'occhiata qui: stackoverflow.com/questions/11351032/…
MJB

Risposte:


152

Le classi di dati sono solo classi regolari orientate alla memorizzazione dello stato, più che contengono molta logica. Ogni volta che crei una classe che consiste principalmente di attributi, hai creato una classe di dati.

Ciò che il dataclassesmodulo fa è semplificare la creazione di classi di dati. Si prende cura di molte piastre della caldaia per te.

Ciò è particolarmente importante quando la classe di dati deve essere hash; ciò richiede un __hash__metodo e un __eq__metodo. Se aggiungi un __repr__metodo personalizzato per facilitare il debug, questo può diventare abbastanza dettagliato:

class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def __init__(
            self, 
            name: str, 
            unit_price: float,
            quantity_on_hand: int = 0
        ) -> None:
        self.name = name
        self.unit_price = unit_price
        self.quantity_on_hand = quantity_on_hand

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

    def __repr__(self) -> str:
        return (
            'InventoryItem('
            f'name={self.name!r}, unit_price={self.unit_price!r}, '
            f'quantity_on_hand={self.quantity_on_hand!r})'

    def __hash__(self) -> int:
        return hash((self.name, self.unit_price, self.quantity_on_hand))

    def __eq__(self, other) -> bool:
        if not isinstance(other, InventoryItem):
            return NotImplemented
        return (
            (self.name, self.unit_price, self.quantity_on_hand) == 
            (other.name, other.unit_price, other.quantity_on_hand))

Con dataclasseste puoi ridurlo a:

from dataclasses import dataclass

@dataclass(unsafe_hash=True)
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

La stessa classe decoratore può anche generare metodi di confronto ( __lt__, __gt__ecc) e maniglia immutabilità.

namedtuplele classi sono anche classi di dati, ma sono immutabili per impostazione predefinita (oltre ad essere sequenze). dataclassessono molto più flessibili in questo senso e possono essere facilmente strutturati in modo tale da poter ricoprire lo stesso ruolo di una namedtupleclasse .

Il PEP è stato ispirato dal attrsprogetto , che può fare ancora di più (compresi slot, validatori, convertitori, metadati, ecc.).

Se vuoi vedere alcuni esempi, ho usato di recente dataclassesalcune delle mie soluzioni Advent of Code , vedi le soluzioni per il giorno 7 , il giorno 8 , il giorno 11 e il giorno 20 .

Se si desidera utilizzare il dataclassesmodulo nelle versioni Python <3.7, è possibile installare il modulo backport (richiede 3.6) o utilizzare il attrsprogetto sopra menzionato.


2
Nel primo esempio nascondi intenzionalmente i membri della classe con membri di istanza con gli stessi nomi? Per favore aiutaci a capire questo idioma.
VladimirLenin,

4
@VladimirLenin: non ci sono attributi di classe, ci sono solo annotazioni di tipo. Vedi PEP 526 , in particolare la sezione Annotazioni variabili di classe e istanza .
Martijn Pieters

1
@Bananach: il @dataclassgenera approssimativamente lo stesso __init__metodo, con un quantity_on_handargomento di parole chiave con valore predefinito. Quando crei un'istanza, imposterà sempre l' quantity_on_handattributo dell'istanza. Quindi il mio primo esempio di non dataclass utilizza lo stesso modello per fare eco a ciò che farà il codice generato dalla dataclass.
Martijn Pieters

1
@Bananach: così nel primo esempio, si potrebbe semplicemente omettere modificando un attributo grado, esclusa ombra l'attributo di classe, è superfluo impostazione comunque in tal senso, ma dataclasses non impostarlo.
Martijn Pieters

1
@ user2853437 il tuo caso d'uso non è realmente supportato da dataclass; forse staresti meglio usando il cugino più grande dei dataclass, attrs . Tale progetto supporta i convertitori per campo che consentono di normalizzare i valori dei campi. Se si desidera attenersi agli occhiali da vista, quindi sì, eseguire la normalizzazione nel __post_init__metodo.
Martijn Pieters

62

Panoramica

La domanda è stata affrontata. Tuttavia, questa risposta aggiunge alcuni esempi pratici per aiutare nella comprensione di base degli occhiali.

Cosa sono esattamente le classi di dati Python e quando è meglio usarle?

  1. generatori di codice : genera codice boilerplate; puoi scegliere di implementare metodi speciali in una classe normale o fare in modo che una dataclass li implementi automaticamente.
  2. contenitori di dati : strutture che contengono dati (ad es. tuple e dadi), spesso con accesso ad attributi punteggiato, come classi namedtuplee altri .

"mutable namedtuples con default [s]"

Ecco cosa significa quest'ultima frase:

  • mutabile : per impostazione predefinita, gli attributi della dataclass possono essere riassegnati. Facoltativamente, puoi renderli immutabili (vedi esempi di seguito).
  • namedtuple : hai punteggiato, accedi agli attributi come una namedtupleo una classe normale.
  • impostazione predefinita : è possibile assegnare valori predefiniti agli attributi.

Rispetto alle classi comuni, si risparmia principalmente sulla digitazione del codice del boilerplate.


Caratteristiche

Questa è una panoramica delle funzionalità della dataclass (TL; DR? Vedere la tabella riassuntiva nella sezione successiva).

Cosa ottieni

Ecco le funzionalità che si ottengono di default dai dataclass.

Attributi + Rappresentazione + Confronto

import dataclasses


@dataclasses.dataclass
#@dataclasses.dataclass()                                       # alternative
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

Queste impostazioni predefinite vengono fornite impostando automaticamente le seguenti parole chiave su True:

@dataclasses.dataclass(init=True, repr=True, eq=True)

Cosa puoi accendere

Funzionalità aggiuntive sono disponibili se le parole chiave appropriate sono impostate su True.

Ordine

@dataclasses.dataclass(order=True)
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

I metodi di ordinazione sono ora implementati (operatori di sovraccarico:) < > <= >=, in modo simile a functools.total_orderingtest di uguaglianza più forti.

Hashable, Mutable

@dataclasses.dataclass(unsafe_hash=True)                        # override base `__hash__`
class Color:
    ...

Sebbene l'oggetto sia potenzialmente mutabile (probabilmente indesiderato), viene implementato un hash.

Hashable, Immutable

@dataclasses.dataclass(frozen=True)                             # `eq=True` (default) to be immutable 
class Color:
    ...

Ora è implementato un hash e non è possibile modificare l'oggetto o assegnare agli attributi.

Nel complesso, l'oggetto è hasash se uno unsafe_hash=Trueo frozen=True.

Vedi anche la tabella logica di hashing originale con maggiori dettagli.

Quello che non ottieni

Per ottenere le seguenti funzionalità, è necessario implementare manualmente metodi speciali:

Apertura della confezione

@dataclasses.dataclass
class Color:
    r : int = 0
    g : int = 0
    b : int = 0

    def __iter__(self):
        yield from dataclasses.astuple(self)

Ottimizzazione

@dataclasses.dataclass
class SlottedColor:
    __slots__ = ["r", "b", "g"]
    r : int
    g : int
    b : int

La dimensione dell'oggetto è ora ridotta:

>>> imp sys
>>> sys.getsizeof(Color)
1056
>>> sys.getsizeof(SlottedColor)
888

In alcune circostanze, __slots__migliora anche la velocità di creazione di istanze e di accesso agli attributi. Inoltre, gli slot non consentono assegnazioni predefinite; in caso contrario, ValueErrorviene sollevato a.

Vedi di più sulle slot in questo post del blog .


Tabella riassuntiva

+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
|       Feature        |       Keyword        |                      Example                       |           Implement in a Class          |
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
| Attributes           |  init                |  Color().r -> 0                                    |  __init__                               |
| Representation       |  repr                |  Color() -> Color(r=0, g=0, b=0)                   |  __repr__                               |
| Comparision*         |  eq                  |  Color() == Color(0, 0, 0) -> True                 |  __eq__                                 |
|                      |                      |                                                    |                                         |
| Order                |  order               |  sorted([Color(0, 50, 0), Color()]) -> ...         |  __lt__, __le__, __gt__, __ge__         |
| Hashable             |  unsafe_hash/frozen  |  {Color(), {Color()}} -> {Color(r=0, g=0, b=0)}    |  __hash__                               |
| Immutable            |  frozen + eq         |  Color().r = 10 -> TypeError                       |  __setattr__, __delattr__               |
|                      |                      |                                                    |                                         |
| Unpacking+           |  -                   |  r, g, b = Color()                                 |   __iter__                              |
| Optimization+        |  -                   |  sys.getsizeof(SlottedColor) -> 888                |  __slots__                              |
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+

+ Questi metodi non vengono generati automaticamente e richiedono un'implementazione manuale in una classe di dati.

* __ne__ non è necessario e quindi non implementato .


Caratteristiche aggiuntive

Post-inizializzazione

@dataclasses.dataclass
class RGBA:
    r : int = 0
    g : int = 0
    b : int = 0
    a : float = 1.0

    def __post_init__(self):
        self.a : int =  int(self.a * 255)


RGBA(127, 0, 255, 0.5)
# RGBA(r=127, g=0, b=255, a=127)

Eredità

@dataclasses.dataclass
class RGBA(Color):
    a : int = 0

conversioni

Convertire una classe di dati ad una tupla o un dizionario, in modo ricorsivo :

>>> dataclasses.astuple(Color(128, 0, 255))
(128, 0, 255)
>>> dataclasses.asdict(Color(128, 0, 255))
{r: 128, g: 0, b: 255}

limitazioni


Riferimenti

  • Discorso di R. Hettinger su Dataclasses: il generatore di codice per terminare tutti i generatori di codice
  • Il discorso di T. Hunner su Classi più semplici: Classi Python senza tutte le cruft
  • Documentazione di Python sui dettagli di hashing
  • Guida di Real Python su The Ultimate Guide to Data Classes in Python 3.7
  • Il post del blog di A. Shaw in Un breve tour delle classi di dati di Python 3.7
  • Il repository github di E. Smith su dataclass

2

Dalla specifica PEP :

Viene fornito un decoratore di classe che ispeziona una definizione di classe per variabili con annotazioni di tipo come definito in PEP 526, "Sintassi per annotazioni di variabili". In questo documento, tali variabili sono chiamate campi. Usando questi campi, il decoratore aggiunge definizioni di metodi generati alla classe per supportare l'inizializzazione dell'istanza, una repr, metodi di confronto e, facoltativamente, altri metodi come descritto nella sezione Specifica. Tale classe è chiamata Data Class, ma non c'è davvero nulla di speciale nella classe: il decoratore aggiunge metodi generati alla classe e restituisce la stessa classe che è stata data.

Il @dataclassgeneratore aggiunge metodi alla classe che si sarebbe altrimenti definisci come __repr__, __init__, __lt__, e __gt__.


2

Considera questa semplice classe Foo

from dataclasses import dataclass
@dataclass
class Foo:    
    def bar():
        pass  

Ecco il dir()confronto integrato. Sul lato sinistro è il Foosenza il decoratore @dataclass, e sul lato destro è con il decoratore @dataclass.

inserisci qui la descrizione dell'immagine

Ecco un altro diff, dopo aver usato il inspectmodulo per il confronto.

inserisci qui la descrizione dell'immagine

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.