Abstracción y Diseño de Clases

¿Qué es la Abstracción?

La abstracción es uno de los principios fundamentales de la POO que consiste en identificar las características y comportamientos esenciales de un objeto, ignorando los detalles irrelevantes. Permite crear modelos simplificados que representan entidades complejas.

Aplicación de la Abstracción

En la programación, aplicamos la abstracción para:

  • Reducir la complejidad al centrarnos en lo importante
  • Crear clases y métodos generalizados que pueden ser reutilizados
  • Ocultar implementaciones complejas detrás de interfaces simples
  • Modelar conceptos del mundo real de manera efectiva

Interfaces en Python

Interfaces Formales y Virtuales

A diferencia de lenguajes como Java o C#, Python no tiene una palabra clave específica para declarar interfaces. Sin embargo, podemos implementar interfaces de dos formas:

  • Interfaces Informales (Duck Typing): Siguiendo la filosofía de "si camina como un pato y hace cuac como un pato, entonces es un pato", Python permite crear interfaces implícitas.
  • Interfaces Formales: Desde Python 3.8, podemos usar el módulo abc (Abstract Base Classes) para definir interfaces más formales.
interfaces_informales.py
class DispositivoEntrada:
    """
    Esta es una interfaz informal (implícita) para dispositivos de entrada.
    Cualquier clase que implemente estos métodos se considera un dispositivo de entrada.
    """
    def conectar(self):
        pass
    
    def desconectar(self):
        pass
    
    def enviar_entrada(self, datos):
        pass
    
    def recibir_feedback(self):
        pass


class Teclado:
    """Implementa la interfaz DispositivoEntrada"""
    
    def __init__(self, modelo):
        self.modelo = modelo
        self.conectado = False
    
    def conectar(self):
        self.conectado = True
        return f"Teclado {self.modelo} conectado correctamente."
    
    def desconectar(self):
        self.conectado = False
        return f"Teclado {self.modelo} desconectado."
    
    def enviar_entrada(self, texto):
        if self.conectado:
            return f"Enviando texto: '{texto}'"
        return "Error: el teclado no está conectado."
    
    def recibir_feedback(self):
        return "Luces del teclado activadas."


class Raton:
    """Implementa la interfaz DispositivoEntrada"""
    
    def __init__(self, modelo, dpi=1600):
        self.modelo = modelo
        self.dpi = dpi
        self.conectado = False
    
    def conectar(self):
        self.conectado = True
        return f"Ratón {self.modelo} conectado correctamente."
    
    def desconectar(self):
        self.conectado = False
        return f"Ratón {self.modelo} desconectado."
    
    def enviar_entrada(self, coordenadas):
        if self.conectado:
            x, y = coordenadas
            return f"Movimiento del ratón a las coordenadas: ({x}, {y})"
        return "Error: el ratón no está conectado."
    
    def recibir_feedback(self):
        return "LED del ratón encendido."


# Función que trabaja con cualquier dispositivo que implementa la interfaz
def probar_dispositivo(dispositivo):
    """
    Esta función acepta cualquier objeto que implemente la interfaz de DispositivoEntrada.
    No necesita verificar el tipo específico gracias al duck typing.
    """
    print(dispositivo.conectar())
    print(dispositivo.enviar_entrada("datos de prueba"))
    print(dispositivo.recibir_feedback())
    print(dispositivo.desconectar())


# Crear instancias
teclado = Teclado("Logitech K380")
raton = Raton("Logitech MX Master", 4000)

# Usar la función con diferentes dispositivos
print("Probando teclado:")
probar_dispositivo(teclado)

print("\nProbando ratón:")
probar_dispositivo(raton)
>>> Ejecuta el código para ver la salida

Interfaces Formales con ABC

Cuando necesitamos un enfoque más estricto, podemos usar el módulo abc para crear clases abstractas e interfaces formales:

interfaces_formales.py
from abc import ABC, abstractmethod

class DispositivoEntrada(ABC):
    """
    Interfaz formal para dispositivos de entrada.
    ABC = Abstract Base Class
    """
    
    @abstractmethod
    def conectar(self):
        """Conecta el dispositivo"""
        pass
    
    @abstractmethod
    def desconectar(self):
        """Desconecta el dispositivo"""
        pass
    
    @abstractmethod
    def enviar_entrada(self, datos):
        """Envía datos desde el dispositivo"""
        pass
    
    # Método concreto (no abstracto) que implementa comportamiento común
    def comprobar_conectividad(self):
        return "Comprobando estado de conexión..."


class Teclado(DispositivoEntrada):
    def __init__(self, modelo):
        self.modelo = modelo
        self.conectado = False
    
    # Implementación del método abstracto
    def conectar(self):
        self.conectado = True
        return f"Teclado {self.modelo} conectado correctamente."
    
    # Implementación del método abstracto
    def desconectar(self):
        self.conectado = False
        return f"Teclado {self.modelo} desconectado."
    
    # Implementación del método abstracto
    def enviar_entrada(self, texto):
        if self.conectado:
            return f"Enviando texto: '{texto}'"
        return "Error: el teclado no está conectado."


class Raton(DispositivoEntrada):
    def __init__(self, modelo, dpi=1600):
        self.modelo = modelo
        self.dpi = dpi
        self.conectado = False
    
    def conectar(self):
        self.conectado = True
        return f"Ratón {self.modelo} conectado correctamente."
    
    def desconectar(self):
        self.conectado = False
        return f"Ratón {self.modelo} desconectado."
    
    def enviar_entrada(self, coordenadas):
        if self.conectado:
            x, y = coordenadas
            return f"Movimiento del ratón a las coordenadas: ({x}, {y})"
        return "Error: el ratón no está conectado."


# Si intentamos instanciar DispositivoEntrada directamente, obtendremos un error
try:
    dispositivo_generico = DispositivoEntrada()
    print("¡Instancia creada!")  # Esto no debería ejecutarse
except TypeError as e:
    print(f"Error al instanciar clase abstracta: {e}")

# Crear instancias concretas
teclado = Teclado("Razer BlackWidow")
raton = Raton("Logitech G502", 16000)

# Usar las instancias
print("\nTeclado:")
print(teclado.comprobar_conectividad())  # Método heredado
print(teclado.conectar())  # Método implementado
print(teclado.enviar_entrada("Hola mundo!"))

print("\nRatón:")
print(raton.comprobar_conectividad())  # Método heredado
print(raton.conectar())  # Método implementado
print(raton.enviar_entrada((150, 200)))  # Método implementado
>>> Ejecuta el código para ver la salida
Decorador @abstractmethod

El decorador @abstractmethod indica que un método debe ser implementado por cualquier subclase concreta. Si una clase hereda de una clase abstracta pero no implementa todos los métodos abstractos, no podrá ser instanciada.

Beneficios de usar clases abstractas:

  • Garantiza que todas las subclases implementen métodos específicos
  • Proporciona un contrato claro para las subclases
  • Combina interfaces con implementaciones compartidas
  • Mejora la documentación del código

Clases Abstractas

Una clase abstracta es una clase que no puede ser instanciada directamente y que sirve como base para otras clases. Puede contener tanto métodos abstractos (que deben ser implementados por sus subclases) como métodos concretos (que proporcionan funcionalidad común).

Las clases abstractas en Python se crean utilizando el módulo abc y presentan una combinación de:

  • Métodos abstractos: definen lo que las subclases deben implementar
  • Métodos concretos: proporcionan funcionalidad común para las subclases
  • Atributos compartidos: pueden tener atributos que serán heredados
clases_abstractas.py
from abc import ABC, abstractmethod
import math

class Forma(ABC):
    """
    Clase abstracta para representar formas geométricas.
    """
    
    def __init__(self, color="Negro"):
        self.color = color
    
    @abstractmethod
    def area(self):
        """Calcula el área de la forma"""
        pass
    
    @abstractmethod
    def perimetro(self):
        """Calcula el perímetro de la forma"""
        pass
    
    # Método concreto compartido por todas las formas
    def descripcion(self):
        return f"Soy una forma de color {self.color}"
    
    # Método concreto con implementación por defecto que puede ser sobrescrito
    def escalar(self, factor):
        """
        Método que las subclases pueden implementar para escalar la forma.
        Esta es una implementación por defecto.
        """
        return f"Escalando la forma por un factor de {factor}"


class Circulo(Forma):
    def __init__(self, radio, color="Rojo"):
        super().__init__(color)
        self.radio = radio
    
    def area(self):
        return math.pi * (self.radio ** 2)
    
    def perimetro(self):
        return 2 * math.pi * self.radio
    
    # Sobrescribimos el método concreto para una implementación específica
    def escalar(self, factor):
        self.radio *= factor
        return f"El círculo ha sido escalado. Nuevo radio: {self.radio}"


class Rectangulo(Forma):
    def __init__(self, ancho, alto, color="Azul"):
        super().__init__(color)
        self.ancho = ancho
        self.alto = alto
    
    def area(self):
        return self.ancho * self.alto
    
    def perimetro(self):
        return 2 * (self.ancho + self.alto)


# Crear instancias
circulo = Circulo(5)
rectangulo = Rectangulo(4, 6)

# Usar métodos abstractos implementados
print(f"Área del círculo: {circulo.area():.2f}")
print(f"Perímetro del círculo: {circulo.perimetro():.2f}")
print(f"Descripción: {circulo.descripcion()}")
print(f"Escalar: {circulo.escalar(2)}")

print("\nÁrea del rectángulo: {:.2f}".format(rectangulo.area()))
print(f"Perímetro del rectángulo: {rectangulo.perimetro()}")
print(f"Descripción: {rectangulo.descripcion()}")
print(f"Escalar: {rectangulo.escalar(1.5)}")
>>> Ejecuta el código para ver la salida
Diferencia entre Interface y Clase Abstracta

Aunque en Python la distinción no es tan rígida como en otros lenguajes, conceptualmente:

  • Interfaces: Solo definen qué métodos debe implementar una clase, sin proporcionar implementación (todos los métodos son abstractos).
  • Clases abstractas: Pueden tener una mezcla de métodos abstractos y métodos con implementación, proporcionando funcionalidad compartida.

En Python, gracias a la herencia múltiple, una clase puede implementar múltiples interfaces y heredar de múltiples clases abstractas.

Colecciones como Estructuras Abstractas de Datos

Las estructuras de datos abstractas (ADT, por sus siglas en inglés) son modelos matemáticos para tipos de datos definidos por su comportamiento desde el punto de vista del usuario. Python implementa varias colecciones que actúan como ADTs:

  • Listas: Secuencias ordenadas con acceso por índice
  • Diccionarios: Mapas clave-valor
  • Conjuntos: Colecciones no ordenadas de elementos únicos
  • Colas: Estructuras FIFO (First In, First Out)
  • Pilas: Estructuras LIFO (Last In, First Out)

Python proporciona el módulo collections con implementaciones adicionales:

colecciones.py
from collections import defaultdict, Counter, deque, namedtuple
import queue

# defaultdict: Un diccionario que proporciona un valor predeterminado para claves inexistentes
print("=== defaultdict ===")
palabras_por_longitud = defaultdict(list)

palabras = ["python", "programación", "código", "desarrollo", "abstracción", "interfaz"]

for palabra in palabras:
    # No necesitamos verificar si la clave existe
    palabras_por_longitud[len(palabra)].append(palabra)

print(f"Palabras de 6 letras: {palabras_por_longitud[6]}")
print(f"Palabras de 11 letras: {palabras_por_longitud[11]}")
print(f"Palabras de 4 letras: {palabras_por_longitud[4]}")  # Clave que no existía previamente

# Counter: Un diccionario para contar ocurrencias
print("\n=== Counter ===")
texto = "Programación orientada a objetos con Python"
contador = Counter(texto.lower())

print(f"Letras más comunes: {contador.most_common(3)}")  # 3 letras más comunes
print(f"Ocurrencias de 'o': {contador['o']}")
print(f"Ocurrencias de 'z': {contador['z']}")  # Automáticamente devuelve 0

# deque: Una cola doblemente terminada
print("\n=== deque (cola doble) ===")
cola = deque(["tarea1", "tarea2", "tarea3"])
print(f"Cola inicial: {cola}")

# Añadir elementos
cola.append("tarea4")          # Añadir al final
cola.appendleft("tarea0")      # Añadir al inicio
print(f"Cola después de añadir: {cola}")

# Quitar elementos
tarea_primera = cola.popleft() # Quitar del inicio
tarea_ultima = cola.pop()      # Quitar del final
print(f"Eliminadas: primera={tarea_primera}, última={tarea_ultima}")
print(f"Cola final: {cola}")

# namedtuple: Tuplas con campos nombrados
print("\n=== namedtuple ===")
Punto = namedtuple('Punto', ['x', 'y', 'z'])
p = Punto(1, 2, 3)
print(f"Punto: {p}")
print(f"Coordenadas: x={p.x}, y={p.y}, z={p.z}")
print(f"Acceso por índice: {p[0]}, {p[1]}, {p[2]}")

# Cola de prioridad
print("\n=== Queue de prioridad ===")
pq = queue.PriorityQueue()
pq.put((2, "Tarea de prioridad media"))
pq.put((1, "Tarea de prioridad alta"))
pq.put((3, "Tarea de prioridad baja"))

print("Procesando tareas por prioridad:")
while not pq.empty():
    prioridad, tarea = pq.get()
    print(f"  Prioridad {prioridad}: {tarea}")
>>> Ejecuta el código para ver la salida

Creando tu Propia Estructura de Datos Abstracta

Podemos crear nuestras propias estructuras de datos abstractas implementando las interfaces adecuadas para que se comporten como colecciones nativas de Python.

queue_implementation.py
class Cola:
    """
    Implementación simple de una estructura de datos tipo Cola (FIFO).
    Esta es nuestra propia implementación de una estructura de datos abstracta.
    """
    
    def __init__(self):
        self._elementos = []
    
    def encolar(self, item):
        """Añade un elemento al final de la cola."""
        self._elementos.append(item)
    
    def desencolar(self):
        """Elimina y devuelve el primer elemento de la cola."""
        if self.esta_vacia():
            raise IndexError("No se puede desencolar de una cola vacía")
        return self._elementos.pop(0)
    
    def frente(self):
        """Devuelve el primer elemento sin eliminarlo."""
        if self.esta_vacia():
            raise IndexError("La cola está vacía")
        return self._elementos[0]
    
    def esta_vacia(self):
        """Comprueba si la cola está vacía."""
        return len(self._elementos) == 0
    
    def tamaño(self):
        """Devuelve el número de elementos en la cola."""
        return len(self._elementos)
    
    def __str__(self):
        """Representación en cadena de la cola."""
        return str(self._elementos)
    
    def __iter__(self):
        """Permite iterar sobre los elementos de la cola sin modificarla."""
        for elemento in self._elementos:
            yield elemento

# Ejemplo de uso de nuestra implementación de Cola
cola = Cola()

print("=== Operaciones con nuestra Cola ===")
print(f"¿La cola está vacía? {cola.esta_vacia()}")

# Encolar elementos
print("\nEncolando elementos...")
for i in range(1, 6):
    cola.encolar(f"Elemento {i}")
    print(f"  Encolado: 'Elemento {i}'")

print(f"\nContenido de la cola: {cola}")
print(f"Tamaño de la cola: {cola.tamaño()}")
print(f"Elemento en el frente: {cola.frente()}")

# Iterar sobre la cola sin modificarla
print("\nIterando sobre la cola:")
for elemento in cola:
    print(f"  {elemento}")

# Desencolar elementos
print("\nDesencolando elementos:")
while not cola.esta_vacia():
    print(f"  Desencolado: '{cola.desencolar()}'")

print(f"\n¿La cola está vacía ahora? {cola.esta_vacia()}")

# Demostrar el error al desencolar de una cola vacía
try:
    cola.desencolar()
except IndexError as e:
    print(f"\nError capturado: {e}")
>>> Ejecuta el código para ver la salida

Ejercicio Práctico

Figura Geométrica

Diseña un sistema de clases para modelar figuras geométricas:

  • Define una clase abstracta FiguraGeometrica con métodos abstractos para calcular área y perímetro.
  • Implementa al menos tres figuras concretas (por ejemplo, Círculo, Rectángulo, Triángulo).
  • Crea una colección heterogénea de figuras y calcula la suma de sus áreas.
ejercicio_figuras.py
from abc import ABC, abstractmethod

class FiguraGeometrica(ABC):
    # Implementa la clase abstracta
    pass

class Circulo(FiguraGeometrica):
    # Implementa esta figura
    pass

class Rectangulo(FiguraGeometrica):
    # Implementa esta figura
    pass

class Triangulo(FiguraGeometrica):
    # Implementa esta figura
    pass

# Crea figuras y calcula áreas