Introdução às classes de dados

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) 

Source: https://habr.com/ru/post/pt415829/


All Articles