Saltar al contenido

# conceptos-avanzados-en-python

Conceptos Avanzados en Python

Volver a la portada del curso
Notebook

Ejemplo Práctico: Clases y Objetos

Sección 2 · Programación Orientada a Objetos

Ejemplo práctico: clases y objetos con los pedidos

En esta práctica conviertes los 1000 pedidos de septiembre de PyShop en objetos que se validan solos, se ordenan, se agrupan en lotes y se exportan a varios formatos. Lo básico de clases ya lo conoces del curso de POO; aquí se trabaja con las herramientas que vienen después: dataclasses, métodos especiales, protocolos y mixins. Descarga el notebook adjunto clases-y-objetos-pedidos y ejecútalo celda por celda; solo necesita Python 3.13 y la biblioteca estándar.

1. De la clase manual a la dataclass

El notebook empieza con PedidoManual, escrito con __init__, __repr__ y __eq__ a mano. Tiene una sorpresa: al definir __eq__, Python pone __hash__ = None, y meter el pedido en un set lanza TypeError: unhashable type: 'PedidoManual'. La versión final usa el decorador para generar esos métodos:

@total_ordering
@dataclass(frozen=True, slots=True)
class Pedido:
    id: int
    fecha: date
    cliente: str
    ciudad: str
    producto: str
    cantidad: int

    @property
    def importe(self):
        return round(PRODUCTOS[self.producto] * self.cantidad, 2)

    def __lt__(self, otro):
        return self.importe < otro.importe

frozen=True impide modificar campos (FrozenInstanceError: cannot assign to field 'cantidad') y, como la igualdad sigue activa, genera __hash__: dos pedidos iguales dentro de un set cuentan como uno. slots=True elimina el __dict__ de cada objeto. El importe es una @property: se calcula a partir del producto y la cantidad, así que nunca queda desincronizado.

2. Un constructor que valida

Pedido.desde_fila(fila, num_linea) es un @classmethod que recibe un diccionario de csv.DictReader, convierte tipos y lanza FilaInvalidaError si la cantidad no es un entero positivo o el producto no existe. Con el CSV de la semilla 7 quedan 981 pedidos válidos y 19 errores; el primero es línea 111: cantidad no positiva -1. La venta total da 718890.0.

3. Ordenar y la trampa de los empates

Con __lt__ y @total_ordering funcionan sorted, min y max sin key. Pero el __eq__ de la dataclass compara todos los campos y __lt__ solo el importe. Hay 10 pedidos de 4495.0 (cinco teclados), y entre dos de ellos a > b y b > a dan True a la vez, mientras que a <= b da False. Por eso max(pedidos) devuelve el último de los empates (id 980) y max(pedidos, key=lambda p: p.importe) devuelve el primero (id 52). Cuando importa cuál sale, usa key.

4. Valores por defecto mutables

__post_init__ corre después del __init__ generado y sirve para validar o normalizar. Un campo de tipo lista se declara con field(default_factory=list); escribir = [] hace que la dataclass rechace la clase con ValueError. En una función normal nadie avisa:

def registrar_error(error, errores=None):
    if errores is None:
        errores = []          # una lista nueva en cada llamada
    errores.append(error)
    return errores

Con errores=[], la lista se crea una sola vez al definir la función y la segunda llamada arrastra el error de la primera. El notebook lo muestra con registrar_error.__defaults__.

5. Un lote que se comporta como colección

Lote contiene una lista de pedidos e implementa __len__, __iter__, __getitem__ y __contains__. Con eso responde a len(lote) (981), a for, a lote[:5] (que devuelve otro Lote) y a 980 in lote. Para que in sea rápido, guarda un set con los ids.

6. Exportadores intercambiables y mixins

ExportadorCSV y ExportadorJSON heredan de una clase abstracta con el método exportar(pedidos); ExportadorTexto no hereda de nada, pero cumple el Protocol Exportable porque tiene el mismo método. generar_reporte acepta cualquiera de los tres sin un solo if. Al final, dos mixins (ConEncabezadoMixin y ContadorMixin) se encadenan con super() siguiendo el MRO para añadir un encabezado y contar los pedidos exportados.

Trampas comunes

  • Definir __eq__ y olvidar el hash: el objeto deja de caber en sets y diccionarios. Una dataclass sin frozen tiene el mismo efecto.
  • Asignar en __post_init__ de una dataclass congelada: self.codigo = ... falla; se usa object.__setattr__.
  • Ordenar por un criterio y comparar por otro: con total_ordering, los empates dan resultados incoherentes.
  • Esperar que una subclase de una ABC funcione sin implementar el método abstracto: Python lanza TypeError al crear el objeto.
  • Llamar a la clase padre por su nombre en un mixin: rompe la cadena; super() respeta el MRO.

Qué hacer con el notebook

Ejecuta las secciones 0 a 8 y cambia valores para ver qué ocurre. Después resuelve los cuatro ejercicios: un método filtrar(**criterios) en Lote (los teclados de CDMX son 15, con ids que empiezan en [29, 131, 241, 260, 329]), un ExportadorMarkdown que funcione con generar_reporte, la corrección de una función con un diccionario mutable por defecto y un pedido_mas_caro que devuelva el primero de los 10 empates. Cada solución trae sus assert: escribe tu versión antes de leerla y verifica que pase las mismas pruebas.

Recursos