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ć?


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.


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

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


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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.