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 aotro número.
Para empezar a trabajar con dataclasses, debemos importar el módulo dataclasses:
from dataclasses import dataclassCon esto podemos utilizar el decorador @dataclass para crear una dataclass:
@dataclassclass A:Ahora, veamos el uso de lo que cambia al usar dataclasses.
Inicialización
Forma usual de hacerlo:
class Number: def __init__(self, value): self.value = valueSalida:
>>> n = Number(5)>>> n.value5Forma con dataclasses:
@dataclassclass Number: value: intSalida:
>>> n = Number(5)>>> n.value5Estos son los cambios al usar el decorador @dataclass:
- No hace falta definir el método
__init__para asignar los valores a `self“, ya que esto se hace automáticamente. - 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
valuees unint. 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:
@dataclassclass Number: value: int = 0Representació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:
class Number: def __init__(self, value): self.value = value
def __repr__(self): return f'Number({self.value})'Salida:
>>> n = Number(5)>>> nNumber(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.
@dataclassclass Number: value: intSalida:
>>> n = Number(5)>>> nNumber(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 siaes igual ab.a != b: Compara siaes diferente ab.a > b: Compara siaes mayor queb.a < b: Compara siaes menor queb.a >= b: Compara siaes mayor o igual queb.a <= b: Compara siaes menor o igual queb.
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.valueCon dataclass:
@dataclass(order=True)class Number: value: intNo 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: floatEl 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 == p2False>>> p1 == p3False>>> p1 > p2False>>> p1 > p3TrueProcesamiento 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.value5Por 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.
@dataclassclass Number: value: int
def __post_init__(self): self.value = abs(self.value)Salida:
>>> n = Number(-5)>>> n.value5Herencia
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: intAhora podemos crear instancias de Student:
>>> s1 = Student('John', 28, 1.80, 10)>>> s1.age28>>> s1.grade10Se 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)]
@dataclassclass 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
@dataclassclass 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.0Ahora podemos crear instancias de User:
>>> u1 = User(1, 'John', 28, 1.80)>>> u2 = User(2, 'John', 28, 1.80)>>> u1 == u2True>>> u1 > u2FalseEl 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:
@dataclassclass User: id: int name: str password: str = field(repr=False) age: int = 0 height: float = 0.0Ahora 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:
@dataclassclass User: id: int name: str is_verified: bool = field(init=False, default=False) age: int = 0 height: float = 0.0Ahora podemos crear instancias de User:
>>> user1 = User(1, 'John', 28, 1.80)>>> user1User(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?