O que são classes de dados e como elas são diferentes das classes comuns?

141

Com o PEP 557, as classes de dados são introduzidas na biblioteca padrão do python.

Eles fazem uso do @dataclassdecorador e devem ser "nomes nomeados mutáveis ​​com padrão", mas não tenho muita certeza de entender o que isso realmente significa e como eles são diferentes das classes comuns.

O que exatamente são as classes de dados python e quando é melhor usá-las?

kingJulian
fonte
8
Dado o extenso conteúdo do PEP, o que mais você gostaria de saber? namedtuples são imutáveis ​​e não podem ter valores padrão para os atributos, enquanto as classes de dados são mutáveis ​​e podem tê-los.
jonrsharpe
31
@jonrsharpe Parece-me razoável que deve haver um encadeamento de stackoverflow sobre o assunto. Stackoverflow pretende ser uma enciclopédia no formato de perguntas e respostas, não? A resposta nunca é "basta olhar neste outro site". Não deveria ter havido votos negativos aqui.
Luke Davis
12
Existem cinco tópicos sobre como anexar um item a uma lista. Uma pergunta sobre @dataclassnão fará com que o site se desintegre.
eric
2
@jonrsharpe namedtuplesPODE ter valores padrão. Dê uma olhada aqui: stackoverflow.com/questions/11351032/…
MJB 08/08/19

Respostas:

152

As classes de dados são apenas classes regulares que são voltadas para o estado de armazenamento, mais do que contêm muita lógica. Toda vez que você cria uma classe que consiste principalmente de atributos, você cria uma classe de dados.

O que o dataclassesmódulo faz é facilitar a criação de classes de dados. Ele cuida de muitas placas de caldeira para você.

Isso é especialmente importante quando sua classe de dados deve ser lavável; isso requer um __hash__método e um __eq__método. Se você adicionar um __repr__método personalizado para facilitar a depuração, isso poderá se tornar bastante detalhado:

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

Com dataclassesvocê você pode reduzi-lo a:

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

A mesma classe decorador também pode gerar métodos de comparação ( __lt__, __gt__, etc.) e imutabilidade punho.

namedtupleclasses também são classes de dados, mas são imutáveis ​​por padrão (além de serem sequências). dataclassessão muito mais flexíveis a esse respeito e podem ser facilmente estruturados para que possam desempenhar o mesmo papel que uma namedtupleclasse .

O PEP foi inspirado no attrsprojeto , que pode fazer ainda mais (incluindo slots, validadores, conversores, metadados, etc.).

Se você quiser ver alguns exemplos, recentemente usei dataclassesvárias das minhas soluções Advent of Code , consulte as soluções para os dias 7 , 8 , 11 e 20 .

Se você quiser usar o dataclassesmódulo nas versões Python <3.7, poderá instalar o módulo backport (requer 3.6) ou usar o attrsprojeto mencionado acima.

Martijn Pieters
fonte
2
No primeiro exemplo, você oculta intencionalmente os membros da classe com membros da instância com os mesmos nomes? Por favor, ajude a entender esse idioma.
VladimirLenin
4
@ VladimirLenin: não há atributos de classe, existem apenas anotações de tipo. Consulte PEP 526 , especificamente a seção Anotações de classe e variável de instância .
Martijn Pieters
1
@ Bananach: @dataclassgera aproximadamente o mesmo __init__método, com um quantity_on_handargumento de palavra - chave com valor padrão. Quando você cria uma instância, ele sempre define o quantity_on_handatributo da instância. Portanto, meu primeiro exemplo que não seja da classe de dados usa o mesmo padrão para ecoar o que o código gerado pela classe de dados fará.
Martijn Pieters
1
@Bananach: assim, no primeiro exemplo, nós poderia simplesmente omitir definir um atributo de instância e não sombra do atributo de classe, é configuração redundante de qualquer maneira, nesse sentido, mas dataclasses não defini-lo.
Martijn Pieters
1
@ user2853437 seu caso de uso não é realmente suportado por classes de dados; talvez você esteja melhor usando o primo maior dos dataclasses, attrs . Esse projeto suporta conversores por campo que permitem normalizar valores de campo. Se você deseja manter as classes de dados, sim, faça a normalização no __post_init__método
Martijn Pieters
62

Visão geral

A questão foi abordada. No entanto, esta resposta adiciona alguns exemplos práticos para ajudar no entendimento básico de classes de dados.

O que exatamente são as classes de dados python e quando é melhor usá-las?

  1. geradores de código : gerar código padrão; você pode optar por implementar métodos especiais em uma classe regular ou ter uma classe de dados implementá-los automaticamente.
  2. contêineres de dados : estruturas que mantêm dados (por exemplo, tuplas e dictos), geralmente com pontos, atribuem acesso como classes namedtuplee outras .

"mutuamente nomeados com padrão [s]"

Aqui está o que a última frase significa:

  • mutável : por padrão, os atributos da classe de dados podem ser reatribuídos. Você pode opcionalmente torná-los imutáveis ​​(veja os exemplos abaixo).
  • namedtuple : você pontilhou, atribui o acesso como uma namedtupleclasse ou uma classe regular.
  • padrão : você pode atribuir valores padrão a atributos.

Comparado às classes comuns, você economiza principalmente na digitação do código padrão.


Recursos

Esta é uma visão geral dos recursos da classe de dados (TL; DR? Consulte a Tabela Resumo na próxima seção).

O que você ganha

Aqui estão os recursos que você obtém por padrão das classes de dados.

Atributos + Representação + Comparação

import dataclasses


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

Esses padrões são fornecidos configurando automaticamente as seguintes palavras-chave para True:

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

O que você pode ativar

Recursos adicionais estão disponíveis se as palavras-chave apropriadas estiverem definidas como True.

Ordem

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

Os métodos de ordenação estão agora implementados (sobrecarregando os operadores :) < > <= >=, da mesma forma que functools.total_orderingnos testes de igualdade mais fortes.

Hashable, Mutable

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

Embora o objeto seja potencialmente mutável (possivelmente indesejado), um hash é implementado.

Hashable, imutável

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

Um hash agora está implementado e a alteração do objeto ou a atribuição de atributos não é permitida.

No geral, o objeto é hashable se unsafe_hash=Trueou frozen=True.

Veja também a tabela lógica de hash original com mais detalhes.

O que você não recebe

Para obter os seguintes recursos, métodos especiais devem ser implementados manualmente:

Desembalar

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

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

Otimização

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

O tamanho do objeto agora é reduzido:

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

Em algumas circunstâncias, __slots__também melhora a velocidade de criação de instâncias e acesso a atributos. Além disso, os slots não permitem atribuições padrão; caso contrário, a ValueErroré gerado.

Veja mais sobre slots nesta postagem do blog .


Tabela de resumo

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

+ Esses métodos não são gerados automaticamente e requerem implementação manual em uma classe de dados.

* __ne__ não é necessário e, portanto, não é implementado .


Características adicionais

Pós-inicialização

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

Herança

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

Conversões

Converta uma classe de dados em uma tupla ou um ditado, recursivamente :

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

Limitações


Referências

  • R. Hettinger fala sobre Dataclasses: O gerador de código para terminar todos os geradores de código
  • A palestra de T. Hunner sobre Classes Mais Fáceis: Classes Python Sem Todo o Cruft
  • Documentação do Python sobre detalhes de hash
  • Real Python's guide on The Ultimate Guide to Data Classes in Python 3.7
  • Postagem do blog de A. Shaw em Um breve tour pelas classes de dados do Python 3.7
  • Repositório github de E. Smith em classes de dados
pylang
fonte
2

A partir da especificação PEP :

É fornecido um decorador de classe que inspeciona uma definição de classe para variáveis ​​com anotações de tipo, conforme definido no PEP 526, "Sintaxe para anotações de variáveis". Neste documento, essas variáveis ​​são chamadas de campos. Usando esses campos, o decorador adiciona definições de método geradas à classe para dar suporte à inicialização de instância, repr, métodos de comparação e, opcionalmente, outros métodos, conforme descrito na seção Especificação. Essa classe é chamada de classe de dados, mas não há realmente nada de especial nela: o decorador adiciona métodos gerados à classe e retorna a mesma classe que recebeu.

O @dataclassgerador adiciona métodos para a classe que você de outra forma se define como __repr__, __init__, __lt__, e __gt__.

Mahmoud Hanafy
fonte
2

Considere esta classe simples Foo

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

Aqui está a dir()comparação interna. No lado esquerdo está o Foosem o decorador @dataclass e à direita está o decorador @dataclass.

insira a descrição da imagem aqui

Aqui está outra diferença, depois de usar o inspectmódulo para comparação.

insira a descrição da imagem aqui

prosti
fonte