Co to są klasy danych i czym różnią się od typowych klas?

141

W PEP 557 klasy danych są wprowadzane do standardowej biblioteki Pythona.

Używają @dataclassdekoratora i mają być "zmiennymi nazwanymi krotkami z wartością domyślną", ale nie jestem pewien, czy rozumiem, co to właściwie oznacza i czym różnią się od zwykłych klas.

Czym dokładnie są klasy danych Pythona i kiedy najlepiej ich używać?

król Julian
źródło
8
Biorąc pod uwagę obszerną treść PEP, czego jeszcze chciałbyś się dowiedzieć? namedtuples są niezmienne i nie mogą mieć wartości domyślnych dla atrybutów, podczas gdy klasy danych są zmienne i mogą je mieć.
jonrsharpe
29
@jonrsharpe Wydaje mi się rozsądne, że na ten temat powinien istnieć wątek przepełnienia stosu. Stackoverflow ma być encyklopedią w formacie pytań i odpowiedzi, prawda? Odpowiedź nigdy nie brzmi „po prostu zajrzyj na tę inną witrynę”. Nie powinno być tutaj głosów przeciw.
Luke Davis,
10
Istnieje pięć wątków na temat dodawania pozycji do listy. Jedno pytanie @dataclassnie spowoduje dezintegracji witryny.
eric
2
@jonrsharpe namedtuplesMOŻE mieć wartości domyślne. Zajrzyj tutaj: stackoverflow.com/questions/11351032/…
MJB

Odpowiedzi:

152

Klasy danych to zwykłe klasy, które są nastawione na przechowywanie stanu, a ponadto zawierają dużo logiki. Za każdym razem, gdy tworzysz klasę składającą się głównie z atrybutów, tworzysz klasę danych.

To, co dataclassesrobi moduł, ułatwia tworzenie klas danych. Dba o dużo płyty kotłowej za Ciebie.

Jest to szczególnie ważne, gdy klasa danych musi mieć możliwość mieszania; wymaga to zarówno __hash__metody, jak i __eq__metody. Jeśli dodasz niestandardową __repr__metodę ułatwiającą debugowanie, może to stać się dość rozwlekłe:

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))

Dzięki temu dataclassesmożesz zredukować to do:

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

Dekorator tej samej klasy mogą generować metod porównawczych ( __lt__, __gt__etc.) i uchwyt niezmienność.

namedtupleklasy są również klasami danych, ale są domyślnie niezmienne (a także są sekwencjami). dataclassessą znacznie bardziej elastyczne pod tym względem i mogą być łatwo skonstruowane w taki sposób, że mogą pełnić taką samą rolę jak namedtupleklasa .

PEP został zainspirowany attrsprojektem , który może zrobić jeszcze więcej (w tym sloty, walidatory, konwertery, metadane itp.).

Jeśli chcesz zobaczyć kilka przykładów, ostatnio użyłem dataclasseskilku moich rozwiązań Advent of Code , zobacz rozwiązania na dzień 7 , 8 , 11 i 20 .

Jeśli chcesz używać dataclassesmodułu w wersjach Pythona <3.7, możesz zainstalować backportowany moduł (wymaga 3.6) lub użyć attrsprojektu wspomnianego powyżej.

Martijn Pieters
źródło
2
Czy w pierwszym przykładzie celowo ukrywasz członków klasy z elementami instancji o takich samych nazwach? Proszę o pomoc w zrozumieniu tego idiomu.
VladimirLenin
4
@VladimirLenin: nie ma atrybutów klas, są tylko adnotacje typu. Zobacz PEP 526 , a konkretnie sekcję Adnotacje zmiennych klas i instancji .
Martijn Pieters
1
@Bananach: @dataclassgeneruje mniej więcej tę samą __init__metodę, z quantity_on_handargumentem słowa kluczowego o wartości domyślnej. Podczas tworzenia instancji quantity_on_handzawsze ustawi ona atrybut instancji. Tak więc mój pierwszy przykład bez klasy danych używa tego samego wzorca, aby powtórzyć działanie kodu wygenerowanego przez klasę danych.
Martijn Pieters
1
@Bananach: więc w pierwszym przykładzie moglibyśmy po prostu pominąć ustawianie atrybutu instancji i nie przesłonić atrybutu klasy, jest to jednak zbędne ustawianie go w tym sensie, ale klasy danych to ustawiają.
Martijn Pieters
1
@ user2853437 Twój przypadek użycia nie jest tak naprawdę obsługiwany przez klasy danych; być może lepiej byłoby, gdybyś użył większego kuzyna dataclass, attrs . Ten projekt obsługuje konwertery na pole, które umożliwiają normalizowanie wartości pól. Jeśli chcesz trzymać się klas danych, to tak, zrób normalizację w __post_init__metodzie.
Martijn Pieters
62

Przegląd

Pytanie zostało rozwiązane. Jednak ta odpowiedź zawiera kilka praktycznych przykładów, które mogą pomóc w podstawowym zrozumieniu klas danych.

Czym dokładnie są klasy danych Pythona i kiedy najlepiej ich używać?

  1. generatory kodu : generuj kod standardowy; możesz zdecydować się na zaimplementowanie specjalnych metod w zwykłej klasie lub zaimplementować je automatycznie w klasie danych.
  2. kontenery danych : struktury przechowujące dane (np. krotki i dykty), często z kropkami, dostęp do atrybutów, takich jak klasy namedtuplei inne .

„zmienne nazwane tuleje z domyślnymi [s]”

Oto, co oznacza to drugie zdanie:

  • zmienne : domyślnie dataclass atrybuty mogą być przypisane. Opcjonalnie możesz uczynić je niezmiennymi (patrz Przykłady poniżej).
  • namedtuple : masz kropkowany dostęp do atrybutów, taki jak a namedtuplelub zwykła klasa.
  • domyślne : możesz przypisać wartości domyślne do atrybutów.

W porównaniu z typowymi klasami oszczędzasz przede wszystkim na wpisywaniu standardowego kodu.


cechy

To jest przegląd funkcji klasy danych (TL; DR? Zobacz tabelę podsumowującą w następnej sekcji).

Co dostałeś

Oto funkcje, które domyślnie uzyskujesz z klas danych.

Atrybuty + reprezentacja + porównanie

import dataclasses


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

Te wartości domyślne są zapewniane przez automatyczne ustawienie następujących słów kluczowych na True:

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

Co możesz włączyć

Dodatkowe funkcje są dostępne, jeśli ustawiono odpowiednie słowa kluczowe True.

Zamówienie

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

Zaimplementowano teraz metody porządkowania (przeciążanie operatorów:) < > <= >=, podobnie jak w functools.total_orderingprzypadku silniejszych testów równości.

Hashable, Mutable

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

Chociaż obiekt jest potencjalnie zmienny (prawdopodobnie niepożądany), zaimplementowano skrót.

Hashable, Immutable

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

Hash jest teraz zaimplementowany, a zmiana obiektu lub przypisywanie atrybutów jest niedozwolone.

Ogólnie rzecz biorąc, obiekt jest haszowalny, jeśli jest unsafe_hash=Truealbo frozen=True.

Zobacz także oryginalną tablicę haszującą z dodatkowymi szczegółami.

Czego nie dostajesz

Aby uzyskać następujące funkcje, należy ręcznie zaimplementować specjalne metody:

Rozpakowanie

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

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

Optymalizacja

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

Rozmiar obiektu jest teraz zmniejszony:

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

W niektórych przypadkach __slots__poprawia również szybkość tworzenia instancji i uzyskiwania dostępu do atrybutów. Ponadto gniazda nie pozwalają na przypisanie domyślne; w przeciwnym razie ValueErrorpodniesione jest a .

Więcej informacji o automatach znajdziesz w tym poście na blogu .


Tabelka podsumowująca

+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
|       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__                              |
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+

+ Te metody nie są generowane automatycznie i wymagają ręcznej implementacji w klasie danych.

* __ne__ nie jest potrzebne i dlatego nie jest realizowane .


Dodatkowe funkcje

Po inicjalizacji

@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)

Dziedzictwo

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

Konwersje

Konwersja dataclass na krotki lub dict, rekurencyjnie :

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

Ograniczenia


Bibliografia

  • Rozmowa R. Hettingera na temat klas danych: generator kodu kończący wszystkie generatory kodu
  • T. Hunnera Dyskusja na Prostsze klas: Python klas bez wszystkich Cruft
  • Dokumentacja Pythona dotycząca haszowania szczegółów
  • Real Python's guide on The Ultimate Guide to Data Classes in Python 3.7
  • A. Shaw post na blogu Krótka prezentacja klas danych w Pythonie 3.7
  • Repozytorium github E. Smitha dotyczące klas danych
pylang
źródło
2

Ze specyfikacji PEP :

Dostarczany jest dekorator klasy, który sprawdza definicję klasy pod kątem zmiennych z adnotacjami typu, jak zdefiniowano w PEP 526 „Składnia adnotacji zmiennych”. W tym dokumencie takie zmienne nazywane są polami. Korzystając z tych pól, dekorator dodaje wygenerowane definicje metod do klasy w celu obsługi inicjalizacji wystąpienia, repr, metod porównawczych i opcjonalnie innych metod, zgodnie z opisem w sekcji Specyfikacja. Taka klasa nazywa się klasą danych, ale tak naprawdę nie ma w niej nic specjalnego: dekorator dodaje wygenerowane metody do klasy i zwraca tę samą klasę, którą otrzymał.

@dataclassGenerator dodaje metody do klasy, że chcesz inaczej zdefiniować siebie podobne __repr__, __init__, __lt__, i __gt__.

Mahmoud Hanafy
źródło
2

Rozważ tę prostą klasę Foo

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

Oto dir()wbudowane porównanie. Po lewej stronie znajduje się Foodekorator bez @dataclass, a po prawej - dekorator @dataclass.

wprowadź opis obrazu tutaj

Oto kolejna różnica, po użyciu inspectmodułu do porównania.

wprowadź opis obrazu tutaj

prosti
źródło