Skip to content
Bootcamp Django Docs
GitHub

Lección 6

Dataclass

Prepárate para...

  • Entender qué son las dataclasses y cómo se diferencian de las clases tradicionales.
  • Aprender cómo crear y trabajar con dataclasses en Python.
  • Explorar las características avanzadas de las dataclasses, como la generación automática de métodos y la personalización del comportamiento de los atributos.

Las dataclasses son una forma de crear clases que se utilizan para almacenar datos. Son similares a las clases normales, pero requieren menos código para crearlas. Son útiles para crear objetos simples que contienen principalmente datos.

Las dataclass se introdujeron en Python 3.7, pero están disponibles en versiones anteriores de Python usando el paquete dataclasses.

La siguiente es una lista de las características de las dataclass:

  • Almacenar datos y representar cierto tipo de datos. Por ejemplo, un número.
  • Se pueden comparar a otros objetos del mismo tipo. Por ejemplo, un número puede ser mayor que, menor que, igual a otro número.

Para empezar a trabajar con dataclasses, debemos importar el módulo dataclasses:

from dataclasses import dataclass

Con esto podemos utilizar el decorador @dataclass para crear una dataclass:

@dataclass
class A:

Ahora, veamos el uso de lo que cambia al usar dataclasses.

Inicialización

Forma usual de hacerlo:

Clase normal
class Number:
def __init__(self, value):
self.value = value

Salida:

>>> n = Number(5)
>>> n.value
5

Forma con dataclasses:

@dataclass
class Number:
value: int

Salida:

>>> n = Number(5)
>>> n.value
5

Estos son los cambios al usar el decorador @dataclass:

  1. No hace falta definir el método __init__ para asignar los valores a `self“, ya que esto se hace automáticamente.
  2. Definimos los atributos de la clase por adelantado de una forma mucho más legible, junto con su tipo (type hints). Ahora sabemos fácilmente que value es un int. Esto es útil para el IDE, y para otras herramientas que analizan el código.

También podemos definir valores por defecto para los atributos:

@dataclass
class Number:
value: int = 0

Representación

La representación de un objeto es la forma en que se muestra cuando se imprime. Por ejemplo, cuando imprimimos un número, queremos que se muestre como 5, y no como <__main__.Number object at 0x0000020D1E0F4E80>. Esto es útil para la depuracion, y para mostrar información al usuario.

Para hacerlo en el caso de las clases normales, debemos definir el método __repr__ como sigue:

Uso de repr
class Number:
def __init__(self, value):
self.value = value
def __repr__(self):
return f'Number({self.value})'

Salida:

>>> n = Number(5)
>>> n
Number(5)

Por el lado de las dataclasses, esto se hace automáticamente al añadirse la función __repr__ al decorar la clase con @dataclass.

@dataclass
class Number:
value: int

Salida:

>>> n = Number(5)
>>> n
Number(value=5)

Comparación de datos

Generalmente, queremos comparar objetos de una clase con otros objetos de la misma clase. Por ejemplo, queremos saber si un número es mayor que otro, o si son iguales.

La comparación entre dos objetos a y b generalmente consiste de las siguientes operaciones:

  • a == b: Compara si a es igual a b.
  • a != b: Compara si a es diferente a b.
  • a > b: Compara si a es mayor que b.
  • a < b: Compara si a es menor que b.
  • a >= b: Compara si a es mayor o igual que b.
  • a <= b: Compara si a es menor o igual que b.

En Python, es posible definir métodos en clases que pueden realizar las operaciones mencionadas. Por ejemplo, para comparar si dos números son iguales, podemos definir el método __eq__ (de equal) y el método __gt__ (de greater than):

class Number:
def __init__(self, value):
self.value = value
def __eq__(self, other):
return self.value == other.value
def __gt__(self, other):
return self.value > other.value

Con dataclass:

@dataclass(order=True)
class Number:
value: int

No necesitamos definir los métodos __eq__ y __gt__, ya que se definen automáticamente al decorar la clase con @dataclass(order=True). El parámetro order=True indica que queremos que se definan los métodos de comparación.

Un método __eq__ generado por dataclass comparará una tupla de sus atributos con una tupla de los atributos de la otra instancia. Por ejemplo, si tenemos dos instancias a y b de la clase Number, y queremos comparar si son iguales, se hará lo siguiente:

(a.value,) == (b.value,)

Veamos otro ejemplo con más datos:

Crearemos una dataclass Person que tenga los atributos name, age y height. Queremos que se puedan comparar dos instancias de Person por su edad, y por su altura.

@dataclass(order=True)
class Person:
name: str
age: int = 0
height: float

El método __eq__ generado automáticamente se verácomo sigue:

def __eq__(self, other):
return (self.name, self.age, self.height) == (other.name, other.age, other.height)

Pasa lo mismo con el método __gt__:

def __gt__(self, other):
return (self.name, self.age, self.height) > (other.name, other.age, other.height)

Ahora podemos comparar dos instancias de Person por su edad y altura:

>>> p1 = Person('John', 20, 1.80)
>>> p2 = Person('John', 30, 1.80)
>>> p3 = Person('John', 20, 1.70)
>>> p1 == p2
False
>>> p1 == p3
False
>>> p1 > p2
False
>>> p1 > p3
True

Procesamiento de datos post-inicialización

En algunos casos, queremos que se realicen ciertas operaciones con los datos de una clase después de que se inicialice. Por ejemplo, queremos que se convierta un número a su valor absoluto después de que se inicialice.

Para hacerlo en el caso de las clases normales, debemos definir el método process como sigue:

class Number:
def __init__(self, value):
self.value = value
self.process()
def process(self):
self.value = abs(self.value)

Salida:

>>> n = Number(-5)
>>> n.value
5

Por el lado de las dataclasses, esto se hace automáticamente al añadirse la función __post_init__ al decorar la clase con @dataclass. Este método se llama después de que se inicialice la clase.

@dataclass
class Number:
value: int
def __post_init__(self):
self.value = abs(self.value)

Salida:

>>> n = Number(-5)
>>> n.value
5

Herencia

Las dataclasses también pueden heredar de otras dataclasses. Por ejemplo, podemos crear una dataclass Student que herede de Person:

@dataclass(order=True)
class Person:
name: str
age: int = 0
height: float
@dataclass(order=True)
class Student(Person):
grade: int

Ahora podemos crear instancias de Student:

>>> s1 = Student('John', 28, 1.80, 10)
>>> s1.age
28
>>> s1.grade
10

Se deben tener en cuenta las siguientes consideraciones al heredar de una dataclass:

  • Los atributos de la clase padre se heredan automáticamente.
  • El orden de los atributos de la clase hija es el orden de los atributos de la clase padre, seguido de los atributos de la clase hija.
  • Los métodos de comparación de la clase padre se heredan automáticamente.
  • Los métodos de comparación de la clase hija tienen prioridad sobre los métodos de comparación de la clase padre.

Campos

Con la implementación actual tenemos un nombre de variable y el tipo de dato con el que se va a trabajar. Pero, ¿qué pasa si queremos agregar más información a la variable? Por ejemplo, si queremos que el nombre de la variable sea diferente al nombre del atributo, o si queremos que el atributo no sea mutable. Esto se puede hacer con el uso de fields.

Inicialización compleja

Consideremos el caso en donde queremos que un atributo sea una lista al inicializar. La forma más simple de hacerlo sería con el método __post_init__:

import random
from typing import List
def get_random_marks() -> List[int]:
return [random.randint(1, 10) for _ in range(5)]
@dataclass
class Student:
marks: List[int]
def __post_init__(self):
self.marks = get_random_marks()

Salida:

>>> s = Student()
>>> s.marks
[7, 2, 9, 10, 3]

La dataclass Student espera una lista de notas. Elegimos que la lista de notas sea aleatoria, y la generamos en el método __post_init__. Esto funciona, pero no es la forma más legible de hacerlo.

Podemos hacerlo de una forma más legible con el uso de fields:

from dataclasses import field
@dataclass
class Student:
marks: List[int] = field(default_factory=get_random_marks)

Salida:

>>> s = Student()
>>> s.marks
[7, 2, 9, 10, 3]

El parámetro default_factory de field nos permite definir una función que se ejecutará para inicializar el atributo. En este caso, la función get_random_marks se ejecutará para inicializar el atributo marks. Esta se utiliza cuando no pasamos un valor al inicializar la clase.

Campos utilizados para la comparación

Anteriormente vimos que los métodos de comparación de una dataclass se generan automáticamente. Esto se hace comparando los atributos de la clase. Pero, ¿qué pasa si queremos que un atributo no se utilice para la comparación? Por ejemplo, si queremos que el atributo id no se utilice para comparar dos instancias de la clase User.

Esto se puede hacer con el uso de fields:

@dataclass(order=True)
class User:
id: int = field(compare=False)
name: str
age: int = 0
height: float = 0.0

Ahora podemos crear instancias de User:

>>> u1 = User(1, 'John', 28, 1.80)
>>> u2 = User(2, 'John', 28, 1.80)
>>> u1 == u2
True
>>> u1 > u2
False

El parámetro compare de field nos permite definir si el atributo se utilizará para la comparación. En este caso, el atributo id no se utilizará para la comparación. De esta forma ganamos granularidad en la comparación de objetos.

Campos utilizados para la representación

Anteriormente vimos que la representación de una dataclass se genera automáticamente. Esto se hace mostrando los atributos de la clase. Pero, ¿qué pasa si queremos que un atributo no se muestre en la representación? Por ejemplo, si queremos que el atributo password no se muestre en la representación de la clase User. O si tenemos demasiados atributos, y queremos mostrar solo algunos para que la representación sea más legible.

Esto se puede hacer con el uso de fields:

@dataclass
class User:
id: int
name: str
password: str = field(repr=False)
age: int = 0
height: float = 0.0

Ahora podemos crear instancias de User:

user1 = User(1, 'John', 'J0hnDo3', 28, 1.80)
print(user1)

Salida:

User(id=1, name='John', age=28, height=1.8)

Campos utilizados para la inicialización

Anteriormente vimos que la inicialización de una dataclass se genera automáticamente. Esto se hace asignando los atributos de la clase. Pero, ¿qué pasa si queremos que un atributo no se pueda asignar al inicializar? Por ejemplo, si queremos que el atributo is_verified no se pueda asignar al inicializar la clase User. O si queremos que el atributo name sea obligatorio.

Esto se puede hacer con el uso de fields:

@dataclass
class User:
id: int
name: str
is_verified: bool = field(init=False, default=False)
age: int = 0
height: float = 0.0

Ahora podemos crear instancias de User:

>>> user1 = User(1, 'John', 28, 1.80)
>>> user1
User(id=1, name='John', is_verified=False, age=28, height=1.8)

El parámetro init de field nos permite definir si el atributo se puede asignar al inicializar la clase. En este caso, el atributo is_verified no se puede asignar al inicializar la clase. Al utilizar init=False, el atributo is_verified se inicializa con el valor por defecto False.

¿Cuál de las siguientes afirmaciones sobre las dataclasses en Python es verdadera?