La herencia múltiple en Python no es solo “heredar de dos clases a la vez”. Es un sistema con reglas precisas que determinan qué método gana cuando dos padres definen el mismo nombre. Entender esas reglas transforma la herencia múltiple de una fuente de bugs misteriosos en una herramienta de diseño deliberada.
Cuando escribes class D(B, C), Python necesita saber en qué orden buscar métodos. Ese orden se llama MRO (Method Resolution Order), y Python lo calcula usando el algoritmo C3 linearization. El resultado está disponible en cualquier momento con D.__mro__ o D.mro().
C3 garantiza tres propiedades: los hijos siempre van antes que sus padres, el orden en que listas las bases se respeta, y la monotonía (si en alguna subclase B va antes que C, eso se mantiene en toda la jerarquía). Esto resuelve el diamond problem de forma determinista: si D(B, C) y tanto B como C heredan de A, Python visita D → B → C → A → object, nunca A dos veces.
El problema aparece cuando ignoras el MRO y llamas directamente a super() sin entender qué clase va a recibir esa llamada. super() no significa “mi padre”; significa “la siguiente clase en el MRO del objeto concreto que se está construyendo”. Esa distinción es crítica en jerarquías con múltiples bases.
Los Mixins son el patrón que hace que la herencia múltiple escale bien en la práctica. Un Mixin es una clase diseñada explícitamente para ser mezclada: tiene una sola responsabilidad, idealmente no tiene __init__ propio ni estado interno, y no tiene sentido instanciarla sola. Lo que aporta es comportamiento puro que cualquier clase puede adquirir combinándolo.
La diferencia entre un Mixin bien diseñado y una clase base ordinaria es de intención y disciplina: si necesitas saber “de qué tipo es este objeto”, usas herencia normal. Si solo necesitas añadirle capacidades ortogonales (serialización, logging, validación), usas Mixins.
import json
import logging
from datetime import datetime
# ── Mixins ──────────────────────────────────────────────────────────────
class JSONSerializableMixin:
"""Añade serialización JSON a cualquier clase que exponga __dict__."""
def to_json(self) -> str:
# Filtra atributos que empiezan con _ para no exponer internals
public = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
return json.dumps(public, default=str)
@classmethod
def from_json(cls, data: str):
return cls(**json.loads(data))
class TimestampMixin:
"""Registra cuándo se creó y modificó por última vez la instancia."""
def __init__(self, **kwargs):
# Llama a super() para que el resto del MRO pueda inicializarse
super().__init__(**kwargs)
self.created_at = datetime.utcnow()
self.updated_at = datetime.utcnow()
def touch(self):
self.updated_at = datetime.utcnow()
class AuditLogMixin:
"""Emite un log cada vez que se llama a save()."""
logger = logging.getLogger("audit")
def save(self):
self.logger.info("Guardando %s con id=%s", type(self).__name__, getattr(self, "id", "?"))
# Delega al siguiente en el MRO; si nadie más define save(), llega a object
super().save()
# ── Clase base de dominio ────────────────────────────────────────────────
class Model:
"""Simula una capa de persistencia mínima."""
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def save(self):
# En producción esto haría un INSERT/UPDATE real
print(f"[DB] {type(self).__name__} guardado: {self.__dict__}")
# ── Clase concreta que combina todo ─────────────────────────────────────
class User(AuditLogMixin, TimestampMixin, JSONSerializableMixin, Model):
"""
MRO: User → AuditLogMixin → TimestampMixin → JSONSerializableMixin → Model → object
Cada Mixin cede el control a super() y la cadena llega hasta Model.save()
"""
def __init__(self, id: int, username: str, email: str, **kwargs):
# **kwargs asegura que atributos extra (como los de los Mixins) fluyan
super().__init__(id=id, username=username, email=email, **kwargs)
# ── Uso ─────────────────────────────────────────────────────────────────
logging.basicConfig(level=logging.INFO)
user = User(id=1, username="alice", email="alice@example.com")
print(User.__mro__)
# (<class 'User'>, <class 'AuditLogMixin'>, <class 'TimestampMixin'>,
# <class 'JSONSerializableMixin'>, <class 'Model'>, <class 'object'>)
user.save()
# INFO:audit:Guardando User con id=1
# [DB] User guardado: {id: 1, username: 'alice', ...}
print(user.to_json())
user.touch()
Lo que hace cada decisión de diseño
La convención de nombrar los Mixins con el sufijo Mixin no es solo estética: le indica a quien lee el código que esa clase no está pensada para instanciarse sola y que su valor está en la combinación.
Fíjate en cómo TimestampMixin.__init__ llama a super().__init__(**kwargs). Esto es lo que hace que la cadena de inicialización funcione. Si TimestampMixin no llamara a super(), Model.__init__ nunca se ejecutaría y el objeto quedaría sin sus atributos de dominio. El uso de **kwargs es el mecanismo que permite que cada eslabón de la cadena consuma lo que necesita y pase el resto hacia adelante.
El orden de bases en class User(AuditLogMixin, TimestampMixin, JSONSerializableMixin, Model) es deliberado: los Mixins van primero, la clase base concreta al final. Esto garantiza que el MRO procese las capas de comportamiento adicional antes de llegar a la implementación real. Si pusieras Model primero, AuditLogMixin.save() nunca se ejecutaría porque Model.save() lo taparía en el MRO.
AuditLogMixin.save() también llama a super().save(). Esto es lo que conecta el log con la persistencia real. Sin esa llamada, el Mixin cortaría la cadena y Model.save() quedaría huérfano.
La pregunta de cuándo usar herencia múltiple versus composición tiene una respuesta práctica: si puedes escribir “un User es un Model“, usa herencia. Si solo puedes decir “un User tiene capacidad de serialización”, la composición o un Mixin son más honestos semánticamente. Los Mixins son el punto de encuentro: te dan la comodidad sintáctica de la herencia con la claridad conceptual de la composición.
Errores que debes conocer
Error: Olvidar super().__init__() en un Mixin que define __init__, cortando la cadena de inicialización del MRO.
# ❌ Wrong
class TimestampMixin:
def __init__(self, **kwargs):
self.created_at = datetime.utcnow()
# No llama a super(); Model.__init__ nunca se ejecuta
# ✅ Right
class TimestampMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs) # cede el control al siguiente en el MRO
self.created_at = datetime.utcnow()
Sin super().__init__(**kwargs), Model.__init__ jamás recibe los argumentos y el objeto queda sin estado.
Error: Poner la clase base concreta antes que los Mixins, invirtiendo el MRO y haciendo que el comportamiento adicional quede inaccesible.
# ❌ Wrong
class User(Model, AuditLogMixin, TimestampMixin, JSONSerializableMixin):
# Model.save() aparece antes en el MRO; AuditLogMixin.save() nunca se llama
...
# ✅ Right
class User(AuditLogMixin, TimestampMixin, JSONSerializableMixin, Model):
# Los Mixins interceptan primero; Model queda al final como implementación base
...
El orden de bases no es una preferencia estética; determina directamente qué método gana en cada punto de la cadena.
Error: Añadir estado mutable propio en un Mixin, creando acoplamiento oculto entre clases que lo usan.
# ❌ Wrong
class CacheMixin:
cache = {} # atributo de clase compartido por TODAS las subclases simultáneamente
# ✅ Right
class CacheMixin:
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._cache = {} # estado por instancia, sin efecto en otras clases
Un atributo de clase en un Mixin es compartido por todas las clases que lo incluyan en el mismo proceso, lo que produce bugs que solo aparecen cuando dos clases distintas usan el mismo Mixin a la vez.
N° 98