Una de las nuevas características introducidas en Python 3.7 son las clases de datos. Están diseñados para automatizar la generación de código para las clases que se utilizan para almacenar datos. A pesar de que utilizan otros mecanismos de trabajo, se pueden comparar con "tuplas con nombre mutable con valores predeterminados".
Introduccion
Todos los ejemplos anteriores requieren Python 3.7 o superior para su funcionamiento.
La mayoría de los desarrolladores de Python tienen que escribir estas clases regularmente:
class RegularBook: def __init__(self, title, author): self.title = title self.author = author
Ya en este ejemplo, la redundancia es visible. El título y los identificadores del autor se usan varias veces. La clase real también contendrá los métodos anulados __eq__
y __repr__
.
El módulo de dataclasses
contiene el decorador @dataclass
. Al usarlo, un código similar se vería así:
from dataclasses import dataclass @dataclass class Book: title: str author: str
Es importante tener en cuenta que se requieren anotaciones de tipo . Todos los campos que no tengan marcas de tipo serán ignorados. Por supuesto, si no desea utilizar un tipo específico, puede especificar Any
desde el módulo de typing
.
¿Qué obtienes como resultado? Obtiene automáticamente una clase, con los métodos implementados __init__
, __repr__
, __str__
y __eq__
. Además, será una clase regular y puede heredarla o agregar métodos arbitrarios.
>>> 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 o diccionario
Por supuesto, si la estructura es bastante simple, puede guardar los datos en un diccionario o tupla:
book = ("Fahrenheit 451", "Bradbury") other = {'title': 'Fahrenheit 451', 'author': 'Bradbury'}
Sin embargo, este enfoque tiene desventajas:
- Debe recordarse que la variable contiene datos relacionados con esta estructura.
- En el caso de un diccionario, debe realizar un seguimiento de los nombres de las teclas. Tal inicialización del diccionario
{'name': 'Fahrenheit 451', 'author': 'Bradbury'}
también será formalmente correcta. - En el caso de una tupla, debe realizar un seguimiento del orden de los valores, ya que no tienen nombres.
Hay una mejor opción:
Namedtuple
from collections import namedtuple NamedTupleBook = namedtuple("NamedTupleBook", ["title", "author"])
Si usamos la clase creada de esta manera, obtenemos prácticamente lo mismo que usar la clase de datos.
>>> book = NamedTupleBook("Fahrenheit 451", "Bradbury") >>> book.author 'Bradbury' >>> book NamedTupleBook(title='Fahrenheit 451', author='Bradbury') >>> book == NamedTupleBook("Fahrenheit 451", "Bradbury")) True
Pero a pesar de la similitud general, las tuplas con nombre tienen sus limitaciones. Vienen del hecho de que las tuplas con nombre siguen siendo tuplas.
Primero, aún puede comparar instancias de diferentes clases.
>>> Car = namedtuple("Car", ["model", "owner"]) >>> book = NamedTupleBook("Fahrenheit 451", "Bradbury")) >>> book == Car("Fahrenheit 451", "Bradbury") True
En segundo lugar, las tuplas con nombre son inmutables. En algunas situaciones, esto es útil, pero me gustaría tener más flexibilidad.
Finalmente, puede operar tanto en una tupla con nombre como en una normal. Por ejemplo, repita.
Otros proyectos
Si no se limita a la biblioteca estándar, puede encontrar otras soluciones a este problema. En particular, el proyecto atrae . Puede hacer incluso más que la clase de datos y funciona en versiones anteriores de python como 2.7 y 3.4. Sin embargo, el hecho de que no sea parte de la biblioteca estándar puede ser inconveniente
Creación
Puede usar el decorador @dataclass
para crear una clase de datos. En este caso, todos los campos de la clase definidos con anotaciones de tipo se utilizarán en los métodos correspondientes de la clase resultante.
Como alternativa, existe la función make_dataclass
, que funciona de manera similar a la creación de tuplas con nombre.
from dataclasses import make_dataclass Book = make_dataclass("Book", ["title", "author"]) book = Book("Fahrenheit 451", "Bradbury")
Valores por defecto
Una característica útil es la facilidad de agregar valores predeterminados a los campos. Todavía no hay necesidad de redefinir el método __init__
, solo especifique los valores directamente en la clase.
@dataclass class Book: title: str = "Unknown" author: str = "Unknown author"
Se tendrán en cuenta en el método generado __init__
>>> Book() Book(title='Unknown', author='Unknown author') >>> Book("Farenheit 451") Book(title='Farenheit 451', author='Unknown author')
Pero al igual que con las clases y métodos regulares, debe tener cuidado al usar valores predeterminados mutables. Si, por ejemplo, necesita usar la lista como valor predeterminado, hay otra forma, pero más sobre eso a continuación.
Además, es importante controlar el orden en que se determinan los campos con valores predeterminados, ya que coincide exactamente con su orden en el método __init__
Clases de datos inmutables
Las instancias de tuplas con nombre son inmutables. En muchas situaciones, esta es una buena idea. Para las clases de datos, también puede hacer esto. Simplemente especifique el parámetro frozen=True
al crear la clase, y si intenta cambiar sus campos, se FrozenInstanceError
una excepción 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'
Configuración de clase de datos
Además del parámetro frozen
, el decorador @dataclass
tiene otros parámetros:
init
: si es True
(predeterminado), se genera el método __init__
. Si la clase ya tiene un método __init__
definido, el parámetro se ignora.repr
: habilita (por defecto) la creación del método __repr__
. La cadena generada contiene el nombre de la clase y el nombre y la representación de todos los campos definidos en la clase. En este caso, se pueden excluir campos individuales (ver más abajo)eq
: habilita (por defecto) la creación del método __eq__
. Los objetos se comparan de la misma manera que si fueran tuplas que contienen los valores de campo correspondientes. Además, la coincidencia de tipos está marcada.order
habilita (el valor predeterminado es desactivado) la creación de los __lt__
, __le__
, __gt__
y __ge__
. Los objetos se comparan de la misma manera que las tuplas correspondientes de valores de campo. Al mismo tiempo, también se verifica el tipo de objetos. Si order
especifica el order
, pero eq
no, se ValueError
una excepción ValueError
. Además, la clase no debe contener métodos de comparación ya definidos.unsafe_hash
afecta la generación del método __hash__
. El comportamiento también depende de los valores de los parámetros eq
y frozen
Personaliza campos individuales
En la mayoría de las situaciones estándar, esto no es obligatorio, pero es posible personalizar el comportamiento de la clase de datos hasta campos individuales utilizando la función de campo.
Valores predeterminados modificables
Una situación típica mencionada anteriormente es el uso de listas u otros valores predeterminados mutables. Es posible que desee una clase de "estantería" que contenga una lista de libros. Si ejecuta el siguiente código:
@dataclass class Bookshelf: books: List[Book] = []
el intérprete informará un error:
ValueError: mutable default <class 'list'> for field books is not allowed: use default_factory
Sin embargo, para otros valores mutables, esta advertencia no funcionará y conducirá a un comportamiento incorrecto del programa.
Para evitar problemas, se sugiere utilizar el parámetro default_factory
de la función de field
. Su valor puede ser cualquier objeto llamado o función sin parámetros.
La versión correcta de la clase se ve así:
@dataclass class Bookshelf: books: List[Book] = field(default_factory=list)
Otras opciones
Además de la default_factory
especificada, la función de campo tiene los siguientes parámetros:
default
: el valor default
. Este parámetro es obligatorio porque la llamada al field
reemplaza el valor predeterminado del campo.init
: habilita (por defecto) el uso de un campo en el método __init__
repr
: habilita (por defecto) el uso de un campo en el método __repr__
compare
incluye (por defecto) el uso del campo en los métodos de comparación ( __eq__
, __le__
y otros)hash
: puede ser un valor booleano o None
. Si es True
, el campo se usa para calcular el hash. Si None
especifica None
(por defecto), se utiliza el valor del parámetro de compare
.
Una de las razones para especificar hash=False
para una compare=True
determinada compare=True
puede ser la dificultad de calcular el hash de campo mientras sea necesario para la comparación.metadata
: diccionario personalizado o None
. El valor está envuelto en MappingProxyType
para que se vuelva inmutable. Este parámetro no es utilizado por las clases de datos en sí y está destinado a extensiones de terceros.
Procesamiento después de la inicialización
El método __init__
generado __init__
llama al método __post_init__
, si está definido en la clase. Como regla, se llama en la forma self.__post_init__()
, sin embargo, si las variables de tipo InitVar
definen en la clase, se pasarán como parámetros del método.
Si no se ha generado el método __post_init__
, no se llamará a __post_init__
.
Por ejemplo, agregue una descripción de libro generada
@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 solo para inicialización
Una de las posibilidades asociadas con el método __post_init__
son los parámetros utilizados solo para la inicialización. Si, al declarar un campo, especifica InitVar
como su tipo, su valor se pasará como un parámetro del método __post_init__
. De ninguna otra manera se usan tales campos en la clase de datos.
@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)
Herencia
Cuando usa el decorador @dataclass
, @dataclass
todas las clases principales comenzando con el objeto y para cada clase de datos encontrada guarda los campos en un diccionario ordenado, luego agrega las propiedades de la clase que se está procesando. Todos los métodos generados utilizan campos del diccionario ordenado resultante.
Como resultado, si la clase principal define valores predeterminados, deberá definir los campos con valores predeterminados.
Como el diccionario ordenado almacena los valores en orden de inserción, para las siguientes clases
@dataclass class BaseBook: title: Any = None author: str = None @dataclass class Book(BaseBook): desc: str = None title: str = "Unknown"
Se __init__
un método __init__
con esta firma:
def __init__(self, title: str="Unknown", author: str=None, desc: str=None)