Um dos novos recursos introduzidos no Python 3.7 são as classes Data. Eles são projetados para automatizar a geração de código para classes usadas para armazenar dados. Apesar de usarem outros mecanismos de trabalho, eles podem ser comparados com "tuplas nomeadas mutáveis com valores padrão".
1. Introdução
Todos os exemplos acima requerem Python 3.7 ou superior para sua operação.
A maioria dos desenvolvedores de python precisa escrever essas classes regularmente:
class RegularBook: def __init__(self, title, author): self.title = title self.author = author
Já neste exemplo, a redundância é visível. Os identificadores de título e autor são usados várias vezes. A classe real também conterá os métodos substituídos __eq__
e __repr__
.
O módulo de dataclasses
contém o decorador @dataclass
. Usando-o, um código semelhante seria assim:
from dataclasses import dataclass @dataclass class Book: title: str author: str
É importante observar que as anotações de tipo são necessárias . Todos os campos que não possuem marcas de tipo serão ignorados. Obviamente, se você não quiser usar um tipo específico, poderá especificar Any
no módulo de typing
.
O que você obtém como resultado? Você obtém automaticamente uma classe, com os métodos implementados __init__
, __repr__
, __str__
e __eq__
. Além disso, será uma classe regular e você poderá herdá-la ou adicionar métodos arbitrários.
>>> book = Book(title="Fahrenheit 451", author="Bradbury") >>> book Book(title='Fahrenheit 451', author='Bradbury') >>> book.author 'Bradbury' >>> other = Book("Fahrenheit 451", "Bradbury") >>> book == other True
Alternativas
Tupla ou dicionário
Obviamente, se a estrutura for bastante simples, você poderá salvar os dados em um dicionário ou em uma tupla:
book = ("Fahrenheit 451", "Bradbury") other = {'title': 'Fahrenheit 451', 'author': 'Bradbury'}
No entanto, essa abordagem tem desvantagens:
- Deve-se lembrar que a variável contém dados relacionados a essa estrutura.
- No caso de um dicionário, você deve acompanhar os nomes das chaves. Essa inicialização do dicionário
{'name': 'Fahrenheit 451', 'author': 'Bradbury'}
também estará formalmente correta. - No caso de uma tupla, você deve acompanhar a ordem dos valores, pois eles não têm nomes.
Existe uma opção melhor:
Namedtuple
from collections import namedtuple NamedTupleBook = namedtuple("NamedTupleBook", ["title", "author"])
Se usarmos a classe criada dessa maneira, obteremos praticamente o mesmo que usar a classe de dados.
>>> book = NamedTupleBook("Fahrenheit 451", "Bradbury") >>> book.author 'Bradbury' >>> book NamedTupleBook(title='Fahrenheit 451', author='Bradbury') >>> book == NamedTupleBook("Fahrenheit 451", "Bradbury")) True
Mas, apesar da semelhança geral, as tuplas nomeadas têm suas limitações. Eles vêm do fato de que tuplas nomeadas ainda são tuplas.
Primeiro, você ainda pode comparar instâncias de diferentes classes.
>>> Car = namedtuple("Car", ["model", "owner"]) >>> book = NamedTupleBook("Fahrenheit 451", "Bradbury")) >>> book == Car("Fahrenheit 451", "Bradbury") True
Segundo, as tuplas nomeadas são imutáveis. Em algumas situações, isso é útil, mas eu gostaria de mais flexibilidade.
Finalmente, você pode operar em uma tupla nomeada e também em uma tupla comum. Por exemplo, itere sobre.
Outros projetos
Se não estiver limitado à biblioteca padrão, você poderá encontrar outras soluções para esse problema. Em particular, o projeto atrai . Ele pode fazer ainda mais que a classe de dados e funciona em versões mais antigas do python, como 2.7 e 3.4. No entanto, o fato de não fazer parte da biblioteca padrão pode ser inconveniente
Criação
Você pode usar o decorador @dataclass
para criar uma classe de dados. Nesse caso, todos os campos da classe definidos com anotação de tipo serão usados nos métodos correspondentes da classe resultante.
Como alternativa, existe a função make_dataclass
, que funciona de maneira semelhante à criação de tuplas nomeadas.
from dataclasses import make_dataclass Book = make_dataclass("Book", ["title", "author"]) book = Book("Fahrenheit 451", "Bradbury")
Valores padrão
Um recurso útil é a facilidade de adicionar valores padrão aos campos. Ainda não há necessidade de redefinir o método __init__
, basta especificar os valores diretamente na classe.
@dataclass class Book: title: str = "Unknown" author: str = "Unknown author"
Eles serão levados em consideração no método __init__
gerado
>>> Book() Book(title='Unknown', author='Unknown author') >>> Book("Farenheit 451") Book(title='Farenheit 451', author='Unknown author')
Mas, como nas classes e métodos regulares, você precisa ter cuidado ao usar padrões mutáveis. Se, por exemplo, você precisar usar a lista como valor padrão, existe outra maneira, mas mais sobre isso abaixo.
Além disso, é importante monitorar a ordem na qual os campos com valores padrão são determinados, pois ele corresponde exatamente à ordem deles no método __init__
Classes de dados imutáveis
Instâncias de tuplas nomeadas são imutáveis. Em muitas situações, é uma boa ideia. Para classes de dados, você também pode fazer isso. Basta especificar o parâmetro frozen=True
ao criar a classe e, se você tentar alterar seus campos, uma exceção FrozenInstanceError
será FrozenInstanceError
@dataclass(frozen=True) class Book: title: str author: str
>>> book = Book("Fahrenheit 451", "Bradbury") >>> book.title = "1984" dataclasses.FrozenInstanceError: cannot assign to field 'title'
Configuração da classe de dados
Além do parâmetro frozen
, o decorador @dataclass
possui outros parâmetros:
init
: se for True
(padrão), o método __init__
será gerado. Se a classe já tiver um método __init__
definido, o parâmetro será ignorado.repr
: permite (por padrão) a criação do método __repr__
. A sequência gerada contém o nome da classe e o nome e a representação de todos os campos definidos na classe. Nesse caso, campos individuais podem ser excluídos (veja abaixo)eq
: ativa (por padrão) a criação do método __eq__
. Os objetos são comparados da mesma maneira como se fossem tuplas contendo os valores de campo correspondentes. Além disso, a correspondência de tipo está marcada.order
permite (o padrão está desativado) a criação dos __lt__
, __le__
, __gt__
e __ge__
. Os objetos são comparados da mesma maneira que as tuplas correspondentes dos valores do campo. Ao mesmo tempo, o tipo de objetos também é verificado. Se a order
especificada, mas o eq
não for, uma exceção ValueError
será lançada. Além disso, a classe não deve conter métodos de comparação já definidos.unsafe_hash
afeta a geração do método __hash__
. O comportamento também depende dos valores dos parâmetros eq
e frozen
Personalizar campos individuais
Na maioria das situações padrão, isso não é necessário, mas é possível personalizar o comportamento da classe de dados em campos individuais usando a função de campo.
Padrões modificáveis
Uma situação típica mencionada acima é o uso de listas ou outros valores padrão mutáveis. Você pode querer uma classe de “estante de livros” contendo uma lista de livros. Se você executar o seguinte código:
@dataclass class Bookshelf: books: List[Book] = []
o intérprete relatará um erro:
ValueError: mutable default <class 'list'> for field books is not allowed: use default_factory
No entanto, para outros valores mutáveis, esse aviso não funcionará e levará ao comportamento incorreto do programa.
Para evitar problemas, é sugerido o uso do parâmetro default_factory
da função de field
. Seu valor pode ser qualquer objeto ou função chamado sem parâmetros.
A versão correta da classe fica assim:
@dataclass class Bookshelf: books: List[Book] = field(default_factory=list)
Outras opções
Além do default_factory
especificado, a função de campo possui os seguintes parâmetros:
default
: o valor default
. Este parâmetro é necessário porque a chamada para o field
substitui o valor do campo padrão.init
: ativa (padrão) o uso de um campo no método __init__
repr
: habilita (padrão) o uso de um campo no método __repr__
compare
inclui (padrão) o uso do campo em métodos de comparação ( __eq__
, __le__
e outros)hash
: pode ser um valor booleano ou None
. Se for True
, o campo é usado para calcular o hash. Se None
especificado (por padrão), o valor do parâmetro de compare
será usado.
Um dos motivos para especificar hash=False
para uma determinada compare=True
pode ser a dificuldade de calcular o hash do campo enquanto for necessário para comparação.metadata
: dicionário personalizado ou None
. O valor é MappingProxyType
em MappingProxyType
para que se torne imutável. Este parâmetro não é usado pelas próprias classes de dados e destina-se a extensões de terceiros.
Processando após a inicialização
O método __init__
gerado automaticamente chama o método __post_init__
, se definido na classe. Como regra, ele é chamado no formato self.__post_init__()
, no entanto, se variáveis do tipo InitVar
definidas na classe, elas serão passadas como parâmetros do método.
Se o método __init__
não tiver sido gerado, __post_init__
não será chamado.
Por exemplo, adicione uma descrição do livro gerada
@dataclass class Book: title: str author: str desc: str = None def __post_init__(self): self.desc = self.desc or "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury')
Parâmetros apenas para inicialização
Uma das possibilidades associadas ao método __post_init__
são os parâmetros usados apenas para a inicialização. Se, ao declarar um campo, especificar InitVar
como seu tipo, seu valor será passado como um parâmetro do método __post_init__
. De nenhuma outra maneira, esses campos são usados na classe de dados.
@dataclass class Book: title: str author: str gen_desc: InitVar[bool] = True desc: str = None def __post_init__(self, gen_desc: str): if gen_desc and self.desc is None: self.desc = "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury') >>> Book("Fareneheit 481", "Bradbury", gen_desc=False) Book(title='Fareneheit 481', author='Bradbury', desc=None)
Herança
Quando você usa o decorador @dataclass
, ele percorre todas as classes pai, começando com o objeto e, para cada classe de dados encontrada, salva os campos em um dicionário ordenado e adiciona as propriedades da classe processada. Todos os métodos gerados usam campos do dicionário ordenado resultante.
Como resultado, se a classe pai definir valores padrão, você precisará definir os campos com valores padrão.
Como o dicionário ordenado armazena os valores em ordem de inserção, para as seguintes classes
@dataclass class BaseBook: title: Any = None author: str = None @dataclass class Book(BaseBook): desc: str = None title: str = "Unknown"
um método __init__
com esta assinatura será gerado:
def __init__(self, title: str="Unknown", author: str=None, desc: str=None)