Conceptos de Programación Orientada a Objetos en Python: Herencia, Encapsulamiento y Polimorfismo

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
# --------------------------------

Etiquetas: Python Programación Orientada a Objetos herencia Encapsulamiento polimorfismo

Publicado el 7-14 22:56