# conceptos-avanzados-en-python
Conceptos Avanzados en Python
Volver a la portada del cursoEjemplo 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 sinfrozentiene el mismo efecto. - Asignar en
__post_init__de una dataclass congelada:self.codigo = ...falla; se usaobject.__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
TypeErroral 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.