Что такое классы данных и чем они отличаются от обычных классов?

143

С помощью PEP 557 классы данных вводятся в стандартную библиотеку Python.

Они используют @dataclassдекоратор, и они должны быть «изменяемыми именованными кортежами по умолчанию», но я не совсем уверен, что понимаю, что это на самом деле означает и чем они отличаются от обычных классов.

Что такое классы данных Python и когда их лучше всего использовать?

kingJulian
источник
8
Что еще вы хотели бы узнать, учитывая обширное содержание PEP? namedtuples являются неизменяемыми и не могут иметь значений по умолчанию для атрибутов, тогда как классы данных изменяемы и могут иметь их.
jonrsharpe
31
@jonrsharpe Мне кажется разумным, что по этой теме должен быть поток stackoverflow. Stackoverflow - это энциклопедия в формате вопросов и ответов, не так ли? Ответ: никогда не «просто посмотрите на этот другой веб-сайт». Здесь не должно было быть голосов против.
Люк Дэвис
12
Есть пять тем, как добавить элемент в список. Один вопрос @dataclassне приведет к распаду сайта.
Эрик
2
@jonrsharpe namedtuplesМОЖЕТ иметь значения по умолчанию. Посмотрите здесь: stackoverflow.com/questions/11351032/…
MJB

Ответы:

153

Классы данных - это просто обычные классы, которые предназначены для хранения состояния, а не содержат много логики. Каждый раз, когда вы создаете класс, который в основном состоит из атрибутов, вы создали класс данных.

Что dataclassesмодуль делает делает его легче создавать классы данных. Он позаботится о большом количестве котельной для вас.

Это особенно важно, когда ваш класс данных должен быть хешируемым; для этого требуется __hash__метод, а также __eq__метод. Если вы добавите собственный __repr__метод для упрощения отладки, он может стать довольно подробным:

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

С dataclassesего помощью вы можете уменьшить его до:

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

Тот же декоратор класса может также генерировать методы сравнения ( __lt__, __gt__и т. Д.) И обрабатывать неизменяемость.

namedtupleклассы также являются классами данных, но по умолчанию неизменяемы (а также являются последовательностями). dataclassesгораздо более гибки в этом отношении и могут быть легко структурированы так, чтобы выполнять ту же роль, что и namedtupleкласс .

PEP был вдохновлен attrsпроектом , который может делать даже больше (включая слоты, валидаторы, конвертеры, метаданные и т. Д.).

Если вы хотите увидеть несколько примеров, которые я недавно использовал dataclassesдля нескольких своих решений Advent of Code , см. Решения для дней 7 , 8 , 11 и 20 .

Если вы хотите использовать dataclassesмодуль в версиях Python <3.7, вы можете установить модуль с обратным переносом (требуется 3.6) или использовать attrsпроект, упомянутый выше.

Мартейн Питерс
источник
2
В первом примере вы намеренно скрываете члены класса с членами экземпляра с одинаковыми именами? Пожалуйста, помогите понять эту идиому.
VladimirLenin
4
@VladimirLenin: атрибутов класса нет, есть только аннотации типов. См. PEP 526 , в частности раздел аннотаций переменных класса и экземпляра .
Мартин Питерс
1
@Bananach: @dataclassгенерирует примерно тот же __init__метод с quantity_on_handаргументом ключевого слова со значением по умолчанию. Когда вы создаете экземпляр, он quantity_on_handвсегда устанавливает атрибут экземпляра. Итак, мой первый пример, не относящийся к классу данных, использует тот же шаблон, чтобы повторить то, что будет делать код, сгенерированный классом данных.
Мартейн Питерс
1
@Bananach: так , в первом примере, мы могли бы просто Опустить установки атрибута экземпляра и не затенять атрибут класса, он является излишней установкой его в любом случае , в этом смысле, но dataclasses же установить его.
Мартейн Питерс
1
@ user2853437 ваш вариант использования на самом деле не поддерживается классами данных; Возможно, вам будет лучше использовать старшего брата dataclass, attrs . Этот проект поддерживает преобразователи для отдельных полей, которые позволяют нормализовать значения полей. Если вы хотите придерживаться классов данных, то да, выполните нормализацию в __post_init__методе.
Мартейн Питерс
64

обзор

Вопрос решен. Однако этот ответ добавляет несколько практических примеров, чтобы помочь в базовом понимании классов данных.

Что такое классы данных Python и когда их лучше всего использовать?

  1. генераторы кода : генерировать шаблонный код; вы можете выбрать реализацию специальных методов в обычном классе или автоматическую их реализацию в классе данных.
  2. контейнеры данных : структуры, содержащие данные (например, кортежи и словари), часто с точками, доступ к атрибутам, например классы namedtupleи другие .

"изменяемые именованные кортежи по умолчанию [s]"

Вот что означает последняя фраза:

  • изменяемый : по умолчанию атрибуты класса данных можно переназначить. При желании вы можете сделать их неизменяемыми (см. Примеры ниже).
  • namedtuple : у вас есть доступ к атрибутам с точками, например, namedtupleк классу или обычному классу.
  • default : атрибутам можно присвоить значения по умолчанию.

По сравнению с обычными классами вы в первую очередь экономите на вводе стандартного кода.


Характеристики

Это обзор функций класса данных (TL; DR? См. Сводную таблицу в следующем разделе).

Что вы получаете

Вот функции, которые вы получаете по умолчанию от классов данных.

Атрибуты + Представление + Сравнение

import dataclasses


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

Эти значения по умолчанию предоставляются путем автоматической установки следующих ключевых слов True:

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

Что можно включить

Дополнительные функции доступны, если для соответствующих ключевых слов установлено значение True.

порядок

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

Теперь реализованы методы упорядочения (операторы перегрузки :) < > <= >=, аналогично functools.total_orderingболее строгим проверкам на равенство.

Hashable, изменяемый

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

Хотя объект потенциально является изменяемым (возможно, нежелательным), реализован хэш.

Hashable, неизменяемый

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

Теперь реализован хэш, и изменение объекта или присвоение атрибутов запрещено.

В целом объект является хешируемым, если либо unsafe_hash=Trueлибо frozen=True.

См. Также исходную таблицу логики хеширования с более подробной информацией.

Что вы не получите

Чтобы получить следующие возможности, необходимо вручную реализовать специальные методы:

Распаковка

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

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

оптимизация

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

Теперь размер объекта уменьшен:

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

В некоторых случаях __slots__также увеличивает скорость создания экземпляров и доступа к атрибутам. Также слоты не допускают назначения по умолчанию; в противном случае возникает a ValueError.

Узнайте больше о слотах в этом сообщении в блоге .


Таблица результатов

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

+ Эти методы не создаются автоматически и требуют ручной реализации в классе данных.

* __ne__ не требуется и, следовательно, не реализуется .


Дополнительные возможности

После инициализации

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

наследование

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

Конверсии

Преобразование DATACLASS в кортеж или Dict, рекурсивно :

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

Ограничения


Ссылки

  • Р. Hettinger в разговоре на Dataclasses: код генератор , чтобы закончить все генераторы коды
  • Т. Ханнера разговор по проще классов: классы Python без всех хлама
  • Документация Python по деталям хеширования
  • Руководство Real Python по The Ultimate Guide to Data Classes in Python 3.7
  • А. Шоу блоге на небольшой тур по Python классов 3.7 данных
  • Репозиторий Github Э. Смита на классах данных
pylang
источник
2

Из спецификации PEP :

Предоставляется декоратор классов, который проверяет определение класса на предмет переменных с аннотациями типов, как определено в PEP 526, «Синтаксис для аннотаций переменных». В этом документе такие переменные называются полями. Используя эти поля, декоратор добавляет к классу сгенерированные определения методов для поддержки инициализации экземпляра, представления, методов сравнения и, возможно, других методов, как описано в разделе «Спецификация». Такой класс называется классом данных, но на самом деле в этом классе нет ничего особенного: декоратор добавляет сгенерированные методы к классу и возвращает тот же класс, что и был дан.

@dataclassГенератор добавляет методы к классу , который вы бы иначе определять себя как __repr__, __init__, __lt__и __gt__.

Махмуд Ханафи
источник
2

Рассмотрим этот простой класс Foo

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

Вот dir()встроенное сравнение. Слева - Fooдекоратор @dataclass без декоратора, а справа - декоратор @dataclass.

введите описание изображения здесь

Вот еще одно различие после использования inspectмодуля для сравнения.

введите описание изображения здесь

прости
источник