La programación orientada a objetos (POO) es un paradigma de diseño de software que organiza el código en torno a "objetos" en lugar de funciones y lógica. Los conceptos clave de la POO, como la herencia, el encapsulamiento y el polimorfismo, son fundamentales para escribir código modular, reutilizable y mantenible.
Herencia
La herencia permite que una clase (clase hija o derivada) adopte las propiedades y métodos de otra clase (clase padre o base). Esto promueve la reutilización de código y establece una relación "es un" entre las clases.
# Clase base que define atributos y métodos comunes
class Vehiculo:
def __init__(self, marca):
self.marca = marca
def arrancar(self):
return f"El vehículo de marca {self.marca} está arrancando."
# Clase derivada que hereda de Vehiculo
class Coche(Vehiculo):
def __init__(self, marca, modelo):
# Llama al constructor de la clase padre para inicializar la marca
super().__init__(marca)
self.modelo = modelo
# Sobrescribe el método arrancar de la clase padre
def arrancar(self):
return f"El {self.modelo} de marca {self.marca} ha arrancado."
# Creación de instancias y demostración de herencia y polimorfismo
mi_coche = Coche("Toyota", "Corolla")
print(mi_coche.arrancar()) # Salida: El Corolla de marca Toyota ha arrancado.
# Verificación de tipos y jerarquía de clases
print(f"¿Es mi_coche una instancia de Coche? {isinstance(mi_coche, Coche)}") # Salida: True
print(f"¿Es mi_coche una instancia de Vehiculo? {isinstance(mi_coche, Vehiculo)}") # Salida: True
print(f"¿Es Vehiculo una subclase de Coche? {issubclass(Vehiculo, Coche)}") # Salida: False
print(f"¿Es Coche una subclase de Vehiculo? {issubclass(Coche, Vehiculo)}") # Salida: True
Cuando una clase hija define un método con el mismo nombre que uno en su clase padre, se produce una sobrescritura. Las llamadas a ese método en una instancia de la clase hija ejecutarán la versión de la clase hija. Si la clase hija no tiene un método específico pero su clace padre sí, se utilizará el método del padre.
Atributos y Métodos Privados
Python utiliza convenciones de nombres para indicar la accesibilidad de atributos y métodos, en lugar de modificadores de acceso explícitos como en otros lenguajes.
- Sin prefijo de guion bajo: Atributos y métodos públicos. Accesibles desde cualquier lugar y heredables por las clases hijas.
- Prefijo de un guion bajo (
\_): Convención para indicar atributos y métodos "protegidos" o "privados" lógicamente. Son accesiblees, pero se espera que no se accedan directamente desde fuera de la clase o sus subclases. - Prefijo de dos guiones bajos (
\_\_): Atributos y métodos "privados" verdaderos. Python realiza un proceso de "name mangling" (renombrado) para dificultar su acceso desde fuera de la clase y por las clases hijas.
Convención de un guion bajo (\_)
Aunque se denominen "privados" por convención, estos elementos son accesibles y heredables. La desaprobación de su acceso directo es más una guía de buenas prácticas.
class Empleado:
_departamento = "Ventas" # Atributo de clase protegido por convención
def __init__(self, nombre):
self._nombre_empleado = nombre # Atributo de instancia protegido por convención
def _obtener_saludo(self): # Método protegido por convención
print("¡Hola desde el departamento!")
class Gerente(Empleado):
def mostrar_info(self):
print(f"Gerente: {self._nombre_empleado}, Departamento: {self._departamento}")
self._obtener_saludo()
gerente_ejemplo = Gerente("Ana")
gerente_ejemplo.mostrar_info()
# Salida:
# Gerente: Ana, Departamento: Ventas
# ¡Hola desde el departamento!
Doble guion bajo (\_\_)
Los elementos con doble guion bajo experimentan un renombraod interno (name mangling). El intérprete de Python los transforma en nombres como \_NombreClase\_\_nombre\_privado para evitar colisiones de nombres y dificultar el acceso externo.
class Configuracion:
__clave_secreta = "XYZ123" # Atributo "privado" real
def __init__(self, id_config):
self.__id = id_config # Atributo de instancia "privado" real
def __generar_token(self): # Método "privado" real
return f"token-{self.__id}-{self.__clave_secreta}"
def obtener_token_seguro(self):
# Solo se puede acceder desde métodos internos de la clase
return self.__generar_token()
# Intentos de acceso directo fallarán
# config = Configuracion("CFG001")
# print(config.__clave_secreta) # AttributeError
# print(config.__id) # AttributeError
# config.__generar_token() # AttributeError
# Acceso a través de métodos públicos
config = Configuracion("CFG001")
print(config.obtener_token_seguro()) # Salida: token-CFG001-XYZ123
# Acceso mediante name mangling (no recomendado)
print(config._Configuracion__clave_secreta) # Salida: XYZ123
print(config._Configuracion__id) # Salida: CFG001
print(config._Configuracion__generar_token()) # Salida: token-CFG001-XYZ123
Para proporcionar acceso controlado a atributos "privados", es una práctica común definir métodos "getter" y "setter" públicos.
class CuentaBancaria:
def __init__(self, titular, saldo_inicial=0):
self.__titular = titular
if saldo_inicial < 0:
raise ValueError("El saldo inicial no puede ser negativo.")
self.__saldo = saldo_inicial
# Getter para el saldo
def get_saldo(self):
return self.__saldo
# Setter para el saldo (con validación)
def depositar(self, monto):
if monto > 0:
self.__saldo += monto
else:
print("El monto a depositar debe ser positivo.")
def retirar(self, monto):
if 0 < monto <= self.__saldo:
self.__saldo -= monto
else:
print("Monto inválido o fondos insuficientes.")
cuenta = CuentaBancaria("Carlos", 1000)
print(f"Saldo inicial: {cuenta.get_saldo()}") # Salida: Saldo inicial: 1000
cuenta.depositar(500)
print(f"Saldo después del depósito: {cuenta.get_saldo()}") # Salida: Saldo después del depósito: 1500
cuenta.retirar(200)
print(f"Saldo después del retiro: {cuenta.get_saldo()}") # Salida: Saldo después del retiro: 1300
Polimorfismo
El polimorfismo ("muchas formas") permite que objetos de diferentes clases respondan a la misma llamada de método de maneras específicas para su tipo. Esto se logra comúnmente a través de la sobrescritura de métodos.
class Pato:
def hacer_sonido(self):
return "¡Cuac!"
class Perro:
def hacer_sonido(self):
return "¡Guau!"
class Gato:
def hacer_sonido(self):
return "¡Miau!"
# Función que opera sobre cualquier objeto con un método hacer_sonido
def escuchar_animal(animal):
print(animal.hacer_sonido())
pato = Pato()
perro = Perro()
gato = Gato()
escuchar_animal(pato) # Salida: ¡Cuac!
escuchar_animal(perro) # Salida: ¡Guau!
escuchar_animal(gato) # Salida: ¡Miau!
Encapsulamiento
El encapsulamiento es el principio de agrupar datos (atributos) y los métodos que operan sobre esos datos dentro de una sola unidad (la clase). Oculta los detalles de implementación interna y expone una interfaz pública controlada para interactuar con el objeto.
class CalculadoraCientifica:
def __init__(self):
# Atributo privado que almacena el historial de operaciones
self.__historial = []
def sumar(self, a, b):
resultado = a + b
self.__registrar_operacion("suma", a, b, resultado)
return resultado
def restar(self, a, b):
resultado = a - b
self.__registrar_operacion("resta", a, b, resultado)
return resultado
# Método privado para registrar operaciones en el historial
def __registrar_operacion(self, operacion, val1, val2, res):
self.__historial.append(f"{val1} {operacion} {val2} = {res}")
# Método público para obtener el historial (solo lectura)
def ver_historial(self):
print("--- Historial de Operaciones ---")
for op in self.__historial:
print(op)
print("--------------------------------")
calc = CalculadoraCientifica()
calc.sumar(10, 5)
calc.restar(20, 7)
calc.ver_historial()
# Salida:
# --- Historial de Operaciones ---
# 10 suma 5 = 15
# 20 resta 7 = 13
# --------------------------------