PythonAprende PythonDocumentación

Dataclasses y properties

Clases de datos sin boilerplate y atributos calculados.

Cuando una clase existe solo para guardar datos, escribir __init__, __repr__ y __eq__ a mano es boilerplate puro. Las dataclasses lo generan por ti.

@dataclass: datos sin boilerplate

from dataclasses import dataclass

@dataclass
class Punto:
    x: float
    y: float

p = Punto(1.5, 2.5)
print(p)                 # Punto(x=1.5, y=2.5)  ← __repr__ gratis
print(p == Punto(1.5, 2.5))   # True            ← __eq__ gratis

El decorador genera automáticamente:

  • __init__ con los campos anotados
  • __repr__ legible para debugging
  • __eq__ que compara campo por campo

Valores por defecto

@dataclass
class Usuario:
    nombre: str
    activo: bool = True
    etiquetas: list[str] = field(default_factory=list)

La trampa importante: para valores mutables por defecto (listas, dicts, sets) usa field(default_factory=list). Un = [] directo se evaluaría UNA vez y todas las instancias compartirían la misma lista.

Validez congelada: frozen

@dataclass(frozen=True)
class Moneda:
    codigo: str

dolar = Moneda("USD")
dolar.codigo = "EUR"   # FrozenInstanceError: inmutable

Con frozen=True la instancia es inmutable — perfecta como clave de diccionario o para constantes.

@property: atributos calculados

Una property expone un método como si fuera un atributo:

class Rectangulo:
    def __init__(self, ancho, alto):
        self.ancho = ancho
        self.alto = alto

    @property
    def area(self):
        return self.ancho * self.alto

r = Rectangulo(3, 5)
print(r.area)    # 15 ← SIN paréntesis: parece atributo, es un cálculo

¿Por qué y no un método area()? Semántica: si es una característica del objeto (y no una acción), property. Y puedes empezar con atributo simple y migrar a property sin romper a quien usa tu clase.

Properties con validación (setter)

class Temperatura:
    def __init__(self):
        self._celsius = 0     # el guion bajo dice: "uso interno"

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, valor):
        if valor < -273.15:
            raise ValueError("Debajo del cero absoluto: imposible")
        self._celsius = valor

t = Temperatura()
t.celsius = 25      # pasa por el setter
t.celsius = -500    # ValueError

La convención _celsius (un guion bajo) señala "no toques esto directamente": quien usa tu clase pasa por la property, que valida.

Resumen

  • @dataclass genera __init__, __repr__ y __eq__ desde las anotaciones
  • Defaults mutables → field(default_factory=list), nunca = []
  • frozen=True hace la instancia inmutable
  • @property expone cálculos como atributos; el setter agrega validación
  • _atributo con guion bajo: convención de "interno, no lo uses"

Quiz

  1. 1. ¿Qué genera automáticamente el decorador @dataclass?

  2. 2. ¿Qué logra el decorador @property?

  3. 3. ¿Por qué NO se hace `etiquetas: list = []` en una dataclass?

Ejercicios

Ejercicio 1: Dataclass Libro

Crea una dataclass `Libro` con campos titulo (str), autor (str) y anio (int). Dos libros con los mismos datos deben ser iguales (==).

Cargando editor…

Ejercicio 2: Property calculada

Crea la clase `Circulo` con atributo radio y una @property `area` que devuelva 3.14159 * radio ** 2 (sin paréntesis al leerla).

Cargando editor…