Regístrate para acceder a más de 15 cursos gratuitos de programación con un simulador

Principio de sustitución de Liskov Python: Profundizando en las clases

Hasta ahora, hemos aprendido a crear clases, heredar de otras y sobrescribir métodos. Sin embargo, usar estas herramientas sin cuidado puede generar problemas. En esta lección, veremos el LSP (Principio de Sustitución de Liskov), que nos ayuda a escribir código orientado a objetos más robusto y fácil de mantener.

¿Qué es el Principio de Sustitución de Liskov?

Este principio fue formulado en 1987 por la ingeniera de software Barbara Liskov. La idea, en palabras sencillas, es esta:

Si una función espera recibir un objeto de cierta clase, debería poder usar un objeto de una subclase sin que el programa falle.

Esto parece obvio, pero cuando uno cambia la forma en que se comportan los métodos en una subclase, puede romper esa "compatibilidad" entre padre e hijo.

Caso práctico: un logger

Supongamos que queremos escribir un sistema que registre (loggee) mensajes con distintos niveles de importancia, por ejemplo: debug, info, error, etc.

class Logger:
    def log(self, level, message):
        # Aquí iría la lógica para registrar el mensaje
        print(f"[{level.upper()}] {message}")

# Ejemplo de uso
logger = Logger()
logger.log('debug', 'Iniciando proceso')
logger.log('info', 'Proceso en progreso')

Con esto, podemos registrar mensajes con diferentes niveles: 'debug', 'info', 'warning', 'error', etc.

La firma (o "forma") del método log es siempre igual: primero va el nivel, luego el mensaje.

Problema al sobrescribir el método

Ahora supongamos que queremos hacer un pequeño cambio para que el nivel de log sea opcional y, por defecto, tenga el valor 'debug'. Para eso creamos una subclase:

class MyLogger(Logger):
    def log(self, message, level='debug'):
        print(f"[{level.upper()}] {message}")

Ahora podemos usar MyLogger así:

logger = MyLogger()
logger.log('Cargando datos')  # Usa nivel debug por defecto
logger.log('Conexión completada', 'info')  # Cambia el nivel

Parece útil... pero ¿cuál es el problema?

Donde todo se rompe

Imaginemos que este logger lo vamos a usar en otra parte del sistema, que espera trabajar con un objeto Logger. Esa parte del código espera que log() reciba primero el nivel y luego el mensaje.

# Supongamos una función que recibe un Logger
def procesar(logger: Logger):
    logger.log('info', 'Inicio de proceso')

Ahora pasamos un MyLogger a esa función:

procesar(MyLogger())

El problema es que MyLogger espera que el primer argumento sea el mensaje, pero la función le está pasando el nivel primero. Resultado: un comportamiento erróneo o una excepción.

Y aquí es donde entra el Principio de Liskov: si MyLogger es una subclase de Logger, debería comportarse como un Logger sin causar problemas. Pero al cambiar la firma del método, rompimos esa compatibilidad.

¿Por qué importa esto?

✅ El principio de sustitución nos ayuda a evitar errores cuando diseñamos jerarquías de clases. Si creamos objetos que se parecen, pero se comportan distinto, arruinamos el polimorfismo (la capacidad de usar objetos de distintas clases indistintamente, mientras cumplan con la misma interfaz).

Reglas al diseñar jerarquías de tipos

El principio va más allá de que las subclases "parezcan" sus padres. También hace recomendaciones sobre:

1. No reforzar los precondiciones

Los métodos en una subclase no deben exigir más que los de la clase padre.

Por ejemplo, si Logger.log() acepta ocho niveles de mensaje, no podemos hacer que una subclase sólo acepte tres. Eso sería "reforzar una restricción".

# Incorrecto: este logger acepta sólo niveles 'debug', 'info' y 'error'
class RestrictiveLogger(Logger):
    def log(self, level, message):
        if level not in ['debug', 'info', 'error']:
            raise ValueError('Nivel no soportado')
        print(f"[{level.upper()}] {message}")

Esto rompe el contrato del padre, porque alguien puede pasarle un nivel válido según Logger, pero inválido según RestrictiveLogger.

2. No debilitar los postcondiciones

Esto significa que no podemos devolver "menos" de lo que promete la clase padre. Por ejemplo, si el método del padre siempre confirma que algo se grabó correctamente, no podemos simplemente hacer nada o devolver un mensaje incompleto en la subclase.

3. No cambiar la forma de modificar datos heredados

Si una subclase sobreescribe propiedades o cambia la forma en que se modifican los datos del padre, también podemos romper este principio.

Esto se llama restricción histórica: la subclase no debe cambiar cómo funciona el tipo de datos base.

¿Y cómo nos protegemos?

Lo ideal es mantener la misma interfaz (firma y comportamiento) cuando sobrescribimos métodos. Si queremos cambiar cómo se usan algunas partes, es mejor hacerlo creando nuevas funciones o métodos, no reescribiendo los existentes con firmas incompatibles.

También podemos usar herramientas como @abstractmethod o clases abstractas para asegurarnos de que nuestras clases sigan ciertas reglas, pero eso lo vamos a ver más adelante.

¿Qué se puede y qué no se puede hacer?

Situación ¿Rompe LSP?
Cambiar el orden de los parámetros
Agregar un nuevo método No
Aceptar menos opciones en los parámetros
Devolver más valores que los esperados
Mantener comportamiento compatible No

Resumen

  • El Principio de Sustitución de Liskov (LSP) dice que una subclase debe poder sustituir a su clase padre sin que el programa falle.
  • Si cambiamos la firma de un método heredado (como el orden o cantidad de parámetros), podemos romper el código que espera un objeto del tipo original.
  • Los precondiciones no deben hacerse más estrictas en subclases.
  • Las postcondiciones no deben debilitarse; por ejemplo, no devolver menos información de la esperada.
  • Los cambios en la manera en que se modifican o acceden los datos heredados también pueden romper este principio.
  • No hay forma automática de asegurarnos de que seguimos LSP: depende de nosotros como programadores.

Para acceder completo a curso necesitas un plan básico

El plan básico te dará acceso completo a todos los cursos, ejercicios y lecciones de Códica, proyectos y acceso de por vida a la teoría de las lecciones completadas. La suscripción se puede cancelar en cualquier momento.

Obtener acceso
130
cursos
1000
ejercicios
2000+
horas de teoría
3200
test

Obtén acceso

Cursos de programación para principiantes y desarrolladores experimentados. Comienza tu aprendizaje de forma gratuita

  • 130 cursos, 2000+ horas de teoría
  • 1000 ejercicios prácticos en el navegador
  • 360 000 estudiantes
Al enviar el formulario, aceptas el «Política de privacidad» y los términos de la «Oferta», y también aceptas los «Términos y condiciones de uso»

Nuestros graduados trabajan en empresas como:

Bookmate
Health Samurai
Dualboot
ABBYY