Composición y dunder methods
¿Qué es?
La composición consiste en construir objetos a partir de otros objetos: una clase contiene instancias de otras clases como atributos. Es la relación "tiene un": un Curso tiene estudiantes; un Estudiante tiene calificaciones.
Los dunder methods (de double underscore, doble guion bajo: __init__, __str__, __len__...) son métodos especiales que Python invoca automáticamente cuando usas sintaxis nativa sobre tus objetos. Definirlos hace que tus clases se comporten como tipos integrados.
¿Cómo funciona?
En composición, simplemente guardas otros objetos dentro de self (por ejemplo, una lista de Estudiante). Para los dunder, defines métodos con nombres reservados: __str__ controla qué muestra print(obj); __repr__, la representación técnica; __len__, qué devuelve len(obj); __eq__, cómo se comparan dos objetos con ==; __lt__, cómo se ordenan con < (lo que usa sorted).
¿Para qué sirve?
La composición sirve para modelar relaciones del mundo real y para mantener cada clase enfocada en una responsabilidad. Los dunder sirven para que tu objeto sea natural de usar: len(curso), print(estudiante), sorted(estudiantes) funcionan sin métodos raros.
Qué pieza del sistema construimos
Construimos la clase Curso, que contiene una lista de estudiantes (composición), y le damos dunder methods para que len(curso) cuente estudiantes, print(curso) muestre un resumen legible y sorted(curso.estudiantes) los ordene por promedio. Es el ensamblaje que une todas las piezas anteriores.
Composición: un Curso que contiene estudiantes
Asumimos la clase Estudiante de los módulos previos (con nombre y la propiedad promedio).
class Curso:
def __init__(self, nombre):
self.nombre = nombre
self.estudiantes = [] # el Curso "tiene" estudiantes
def inscribir(self, estudiante):
self.estudiantes.append(estudiante)
@property
def promedio_general(self):
if not self.estudiantes:
return 0.0
return sum(e.promedio for e in self.estudiantes) / len(self.estudiantes)
El Curso no hereda de Estudiante: lo contiene. Esta es la diferencia clave entre composición ("tiene un") y herencia ("es un"). Un curso no es un estudiante; tiene estudiantes.
Regla práctica para decidir: si dudas entre herencia y composición, prefiere composición. "El curso tiene estudiantes" es composición; "el becado es un estudiante" es herencia. Si la frase natural es "tiene", compón.
Dunder methods: que el objeto hable Python
__str__ y __repr__
class Estudiante:
def __init__(self, nombre):
self.nombre = nombre
self.notas = []
@property
def promedio(self):
return sum(self.notas) / len(self.notas) if self.notas else 0.0
def __str__(self):
return f"{self.nombre} (promedio {self.promedio:.2f})"
def __repr__(self):
return f"Estudiante({self.nombre!r})"
__str__ es para humanos (lo que ve print); __repr__ es para programadores (lo que ves en la consola o dentro de una lista, ideal para depurar). Define al menos __repr__.
__len__, __eq__ y __lt__
class Curso:
def __init__(self, nombre):
self.nombre = nombre
self.estudiantes = []
def inscribir(self, estudiante):
self.estudiantes.append(estudiante)
def __len__(self):
return len(self.estudiantes)
def __str__(self):
return f"Curso {self.nombre}: {len(self)} estudiantes"
Agregamos a Estudiante la comparación y el orden:
def __eq__(self, otro):
return isinstance(otro, Estudiante) and self.nombre == otro.nombre
def __lt__(self, otro):
return self.promedio < otro.promedio
Con __lt__ definido, sorted() ya sabe ordenar estudiantes por promedio.
Todo junto
curso = Curso("POO")
ana = Estudiante("Ana"); ana.notas += [4.0, 5.0]
luis = Estudiante("Luis"); luis.notas += [3.0, 3.5]
curso.inscribir(ana)
curso.inscribir(luis)
print(curso) # Curso POO: 2 estudiantes
print(len(curso)) # 2
print(sorted(curso.estudiantes)) # [Estudiante('Luis'), Estudiante('Ana')]
for e in curso.estudiantes:
print(e) # usa __str__ de Estudiante
sorted muestra a Luis primero (promedio menor) gracias a __lt__, y cada uno se representa con __repr__ dentro de la lista.
Error común: definir __eq__ y olvidar __hash__. Si haces tu objeto comparable con __eq__, deja de ser usable como clave de diccionario o en un set salvo que definas también __hash__. Para objetos que no cambian, puedes añadir __hash__ = object.__hash__ o derivarlo de los mismos campos que usa __eq__.
Tech English: composition = composición, dunder = doble guion bajo, representation = representación (__repr__), has-a / is-a = tiene-un / es-un.
Ejercicios
-
Cursoindexable. Diseña enCursoel dunder__getitem__para quecurso[0]devuelva el primer estudiante inscrito. Indica qué otro dunder necesitarías para quefor e in curso:recorra los estudiantes directamente. -
Comparar calificaciones. Diseña
__eq__y__lt__en la claseCalificacion(del módulo de encapsulamiento) para poder ordenar una lista de notas consorted. Justifica sobre qué campo comparas.