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

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

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

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

Ответ 1

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

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

Ответ 2

обзор

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

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

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

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

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

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

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


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

Вот обзор возможностей класса данных (см. Примеры в сводной таблице).

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

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

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

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.

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

Что ты не получаешь

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

Unpackable

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

Конверсии

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

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

Ограничения


Рекомендации

Ответ 4

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

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

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

Ответ 5

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

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

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

enter image description here

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

enter image description here