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

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

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

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


person kingJulian    schedule 23.12.2017    source источник
comment
Учитывая обширное содержание PEP, что еще вы хотели бы узнать? namedtuples неизменяемы и не могут иметь значений по умолчанию для атрибутов, тогда как классы данных изменяемы и могут иметь их.   -  person jonrsharpe    schedule 23.12.2017
comment
@jonrsharpe Мне кажется разумным, что по этой теме должен быть поток stackoverflow. Stackoverflow - это энциклопедия в формате вопросов и ответов, не так ли? Ответ: никогда не заглядывать на этот другой сайт. Здесь не должно было быть голосов против.   -  person Luke Davis    schedule 29.01.2018
comment
Есть пять тем, как добавить элемент в список. Один вопрос по @dataclass не приведет к распаду сайта.   -  person eric    schedule 19.02.2019
comment
@jonrsharpe namedtuples МОЖЕТ иметь значения по умолчанию. Посмотрите здесь: stackoverflow.com/questions/11351032/   -  person MJB    schedule 08.08.2019


Ответы (4)


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

Модуль 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 project, который может делать даже больше (включая слоты, валидаторы, конвертеры, метаданные и т. д.).

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

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

person Martijn Pieters    schedule 23.12.2017
comment
В первом примере вы намеренно скрываете члены класса с членами экземпляра с одинаковыми именами? Пожалуйста, помогите понять эту идиому. - person VladimirLenin; 19.01.2018
comment
@VladimirLenin: нет атрибутов класса, есть только аннотации типов. См. PEP 526, в частности раздел Аннотации переменных класса и экземпляра. - person Martijn Pieters; 19.01.2018
comment
@VladimirLenin: значение = 0 по умолчанию для quantity_on_hand является избыточным в первом примере и только там, чтобы повторить пример dataclasses. - person Martijn Pieters; 19.01.2018
comment
@MartijnPieters Чтобы прояснить, вы имели в виду, что существует только один атрибут класса, а именно quantity_on_hand, и я скрываю его с помощью атрибута экземпляра, но я выбрал эту конструкцию только для повторения примера dataclasses. Я мог бы пропустить значение по умолчанию =0, и в этом случае quantity_on_hand: int будет просто аннотацией типа, как для двух других переменных? - person Bananach; 16.03.2019
comment
@Bananach: @dataclass генерирует примерно тот же метод __init__ с аргументом ключевого слова quantity_on_hand со значением по умолчанию. Когда вы создаете экземпляр, он всегда устанавливает атрибут экземпляра quantity_on_hand. Итак, мой первый пример, не относящийся к классу данных, использует тот же шаблон, чтобы повторить то, что будет делать сгенерированный код класса данных. - person Martijn Pieters; 16.03.2019
comment
@Bananach: поэтому в первом примере мы могли просто опустить установку атрибута экземпляра, а не затенять атрибут класса, это в любом случае избыточная установка его в этом смысле, но классы данных делают установите это. - person Martijn Pieters; 16.03.2019
comment
@MartijnPieters Не могли бы вы добавить информацию о том, как использовать методы класса данных, прежде чем устанавливать значение атрибута класса данных, например. фильтрация того, что я хочу сохранить в атрибуте? Обычно я делал бы это в методе __init__(), чтобы вызвать метод класса для входного параметра и сохранить его в атрибуте. Должен ли я сейчас использовать метод post_init_()? - person user2853437; 09.06.2020
comment
@ user2853437 ваш вариант использования на самом деле не поддерживается классами данных; возможно, вам будет лучше использовать более крупного родственника классов данных, attrs. Этот проект поддерживает преобразователи для каждого поля, которые позволяют нормализовать значения полей. Если вы хотите придерживаться классов данных, то да, выполните нормализацию в методе __post_init__. - person Martijn Pieters; 10.06.2020

Обзор

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

Что такое классы данных 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 < / a> с более сильными тестами на равенство.

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__ также увеличивает скорость создания экземпляров и доступа к атрибутам. Кроме того, слоты не допускают назначения по умолчанию; в противном случае возникает 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

Конверсии

Преобразуйте класс данных в кортеж или словарь, рекурсивно:

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

Ограничения


использованная литература

  • разговор Р. Хеттингера о классах данных: генератор кода для завершения всего кода генераторы
  • выступление Т. Ханнера о более простых классах: классы Python без всякой примеси
  • документация по деталям хеширования в Python
  • руководство Real Python в Полном руководстве по классам данных в Python 3.7
  • в блоге на < em> Краткий обзор классов данных Python 3.7
  • репозиторий github Э. Смита в классах данных
person pylang    schedule 11.09.2018
comment
Я бы поставил два лайка, если бы это было возможно. Очень хороший ответ @pylang. Я снимаю шляпу перед вами, сэр / мадам;) - person 10SecTom; 23.09.2020
comment
Это гораздо лучший ответ, чем принятый. Браво! - person Corel; 20.10.2020
comment
Мне очень нравятся эти расширенные ответы в микроблогах. Хорошо отформатирован, разделен на удобные для восприятия заголовки, фрагменты кода и справочные разделы. - person Josh Peak; 23.12.2020
comment
любая идея, почему утиный ввод / вывод типа, например. @dataclasses.dataclass class RGB(r=255,g=0,b=0) не поддерживаются? Для базового типа структуры мне важно это сокращение - person WestCoastProjects; 26.12.2020

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

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

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

person Mahmoud Hanafy    schedule 23.12.2017

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

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

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

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

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

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

person prosti    schedule 23.05.2019