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.
En la programación, aplicamos la abstracción para:
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:
abc
(Abstract Base Classes) para definir interfaces más formales.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)
Cuando necesitamos un enfoque más estricto, podemos usar el módulo abc
para crear clases abstractas e interfaces formales:
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
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:
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:
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)}")
Aunque en Python la distinción no es tan rígida como en otros lenguajes, conceptualmente:
En Python, gracias a la herencia múltiple, una clase puede implementar múltiples interfaces y heredar de múltiples clases abstractas.
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:
Python proporciona el módulo collections
con implementaciones adicionales:
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}")
Podemos crear nuestras propias estructuras de datos abstractas implementando las interfaces adecuadas para que se comporten como colecciones nativas de Python.
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}")
Diseña un sistema de clases para modelar figuras geométricas:
FiguraGeometrica
con métodos abstractos para calcular área y perímetro.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