`super()` y la linearización C3 en Python

Cuando escribes super().método(), la intuición dice “llama al padre”. Esa intuición es incorrecta en cuanto aparece herencia múltiple. Lo que super() realmente hace es llamar al siguiente en el MRO — el Method Resolution Order — una lista lineal y determinista que Python calcula una sola vez cuando se define la clase.

El MRO no es un árbol que Python recorre en tiempo de ejecución; es una secuencia plana, calculada en tiempo de definición, almacenada en Clase.__mro__. Cada vez que buscas un atributo o método, Python recorre esa secuencia de izquierda a derecha y retorna el primero que encuentra. super() simplemente te da acceso al resto de esa secuencia a partir de la clase actual, no un acceso directo al padre declarado.

El algoritmo que construye esa secuencia se llama linearización C3 (formalizado por Kim Barrett et al., 1996, aplicado a Python en la PEP 3141). Garantiza dos propiedades que son más difíciles de satisfacer simultáneamente de lo que parecen:

  1. Monotonicidad: los padres siempre aparecen después de sus hijos.
  2. Preservación del orden local: si declaras class C(A, B), A siempre aparece antes que B en el MRO de C.

Esto importa porque en herencia múltiple clásica (el “problema del diamante”) una clase puede aparecer en múltiples ramas del árbol. Sin un algoritmo explícito, el orden de búsqueda sería ambiguo o inconsistente dependiendo del camino que sigas. C3 resuelve eso garantizando que el resultado sea único y predecible.

Si no entiendes el MRO, super() produce resultados sorprendentes: métodos que se saltan, métodos que se llaman dos veces, o jerarquías que Python directamente rechaza con TypeError: Cannot create a consistent method resolution order.

En Python 3, super() sin argumentos aprovecha magia del compilador (__class__ y __self__ se inyectan como cell variables) para evitar la ceremonia de Python 2 donde había que escribir super(ClaseActual, self).

# Ejemplo del problema del diamante con cooperative inheritance

class Base:
    def process(self, values: list) -> list:
        # Fin de la cadena: devolvemos la lista sin modificar.
        # Notar que NO llamamos super().process() aquí porque
        # object no tiene ese método.
        return values


class Validator(Base):
    def process(self, values: list) -> list:
        filtered = [v for v in values if v is not None]
        print(f"  Validator: {len(values)} → {len(filtered)} items")
        return super().process(filtered)   # siguiente en el MRO, no Base directamente


class Transformer(Base):
    def process(self, values: list) -> list:
        transformed = [str(v).strip() for v in values]
        print(f"  Transformer: aplicó strip+str a {len(transformed)} items")
        return super().process(transformed)


class Pipeline(Validator, Transformer):
    """
    MRO calculado por C3:
    Pipeline → Validator → Transformer → Base → object
    
    Sin C3, habría ambigüedad: ¿Validator llama a Base o a Transformer?
    Con C3, Validator.super() apunta a Transformer, preservando el orden
    declarado en Pipeline(Validator, Transformer).
    """

    def process(self, values: list) -> list:
        print(f"Pipeline recibe: {values}")
        result = super().process(values)   # arranca la cadena del MRO
        print(f"Pipeline entrega: {result}")
        return result


def inspect_mro(cls) -> None:
    print(f"\nMRO de {cls.__name__}:")
    for i, c in enumerate(cls.__mro__):
        print(f"  [{i}] {c.__name__}")


if __name__ == "__main__":
    inspect_mro(Pipeline)

    raw = [" hola ", None, " mundo ", None, " python "]
    Pipeline().process(raw)

Qué pasa en cada paso

Cuando Python evalúa class Pipeline(Validator, Transformer), C3 construye el MRO así:

L(Pipeline) = Pipeline + merge(
    L(Validator),     # Validator → Base → object
    L(Transformer),   # Transformer → Base → object
    [Validator, Transformer]  # orden local declarado
)

El algoritmo merge toma el primer elemento de cada lista que no aparezca en la cola de ninguna otra lista y lo añade a la secuencia final, removiéndolo de todas las listas. Esto asegura que Base no aparezca antes que Transformer, porque Base todavía está en la cola de la lista de Transformer. El resultado es Pipeline → Validator → Transformer → Base → object.

Ahora fíjate en el papel de super() dentro de Validator.process. Cuando Python llega ahí durante una llamada sobre una instancia de Pipeline, el MRO en juego es el de Pipeline, no el de Validator. Entonces super() dentro de Validator no significa “el padre de Validator” sino “el siguiente después de Validator en el MRO de la instancia concreta”. Ese siguiente es Transformer. Por eso la cadena cooperativa funciona: cada clase delega hacia adelante sin saber quién viene después, y el MRO lo orquesta todo.

Esto tiene una implicación de diseño importante: para que la herencia cooperativa funcione, todas las clases de la jerarquía deben llamar a super(), incluida la clase base (Base en el ejemplo). Si Base no llama a super().process(), la cadena se corta limpiamente en ella — lo cual está bien siempre que object no tenga ese método. Pero si una clase en medio de la jerarquía omite super(), rompe la cadena silenciosamente y las clases que vienen después en el MRO nunca se ejecutan.

La salida del ejemplo deja ver el orden con claridad:

MRO de Pipeline:
  [0] Pipeline
  [1] Validator
  [2] Transformer
  [3] Base
  [4] object

Pipeline recibe: [' hola ', None, ' mundo ', None, ' python ']
  Validator: 5 → 3 items
  Transformer: aplicó strip+str a 3 items
Pipeline entrega: ['hola', 'mundo', 'python']

Errores que debes conocer

Error: Olvidar llamar a super() en una clase intermedia, rompiendo la cadena cooperativa sin ningún aviso del intérprete.

# ❌ Wrong
class Transformer(Base):
    def process(self, values: list) -> list:
        transformed = [str(v).strip() for v in values]
        return transformed  # Base y cualquier clase posterior nunca se llaman

# ✅ Right
class Transformer(Base):
    def process(self, values: list) -> list:
        transformed = [str(v).strip() for v in values]
        return super().process(transformed)  # delega al siguiente en el MRO

Si el método termina en una clase que no sea la base de la cadena, las clases que siguen en el MRO quedan silenciosamente excluidas — exactamente el tipo de bug que tarda horas en encontrarse.


Error: Crear una jerarquía donde C3 no puede construir un MRO consistente.

# ❌ Wrong
class A(Base): ...
class B(Base): ...
class X(A, B): ...
class Y(B, A): ...
class Z(X, Y): ...  # TypeError: Cannot create a consistent MRO
# X dice A antes que B; Y dice B antes que A. Imposible linearizar.

# ✅ Right
class A(Base): ...
class B(Base): ...
class X(A, B): ...
class Y(A, B): ...  # orden consistente con X
class Z(X, Y): ...  # MRO válido: Z → X → Y → A → B → Base → object

El error no ocurre en tiempo de ejecución al llamar el método — ocurre en el momento en que Python intenta definir la clase Z, lo que facilita detectarlo temprano.


Error: Asumir que super() en Python 3 es equivalente a nombrar la clase explícitamente cuando usas __init_subclass__ o metaclases.

# ❌ Wrong — en algunos contextos de metaclases, __class__ puede no resolver
#    lo que esperas, y el resultado es recursión infinita o la clase incorrecta
class Meta(type):
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)  # aquí sí está bien
        return cls

# ✅ Right — en metaclases, es preferible ser explícito cuando hay herencia de metaclases
class Meta(type):
    def __new__(mcs, name, bases, namespace):
        cls = type.__new__(mcs, name, bases, namespace)
        return cls

super() sin argumentos funciona correctamente en métodos de instancia y de clase normales; en metaclases con herencia compleja, ser explícito elimina toda ambigüedad sobre qué super() está resolviendo.

97

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio