- Gerardo Catalas | Backend Developer (.NET & SQL Server)/
- Blog/
- Resumen esencial de Programación Orientada a Objetos en Python/
Resumen esencial de Programación Orientada a Objetos en Python
Tabla de contenido
1. Fundamentos: Del concepto al código #
Antes de entrar en los pilares de la POO, es vital entender las piezas que componen este paradigma y cómo se diferencian de la programación estructurada tradicional.
Función vs. Método #
Una función es un bloque de código independiente y reutilizable que realiza una tarea específica. Un método es esencialmente una función, pero que “vive” dentro de una clase y opera sobre los datos (estado) de los objetos creados a partir de esa clase.
# Esto es una función (independiente)
def saludar(nombre):
return f"¡Hola, {nombre}!"
class Persona:
def __init__(self, nombre):
self.nombre = nombre
# Esto es un método (pertenece a la clase)
def saludar_metodo(self):
return f"Hola, soy {self.nombre}"
Clase, Objeto y Constructor #
- Clase: Es el molde o plano arquitectónico. Define qué atributos (datos) y métodos (comportamientos) tendrán los objetos.
- Objeto (Instancia): Es la materialización de esa clase. Si la clase es el plano de una casa, el objeto es la casa ya construida.
- Constructor (
__init__): Es un método especial que Python ejecuta automáticamente cada vez que creas un nuevo objeto. Se usa para inicializar el estado del objeto.
class Auto:
# El constructor inicializa el estado
def __init__(self, marca, modelo):
self.marca = marca # Atributo
self.modelo = modelo # Atributo
# Instanciación: creando objetos a partir del molde
mi_auto = Auto("Toyota", "Corolla")
print(mi_auto.marca) # Salida: Toyota
Conceptos de diseño:
- Entidad: El concepto abstracto en tu lógica de negocio (ej. un Auto).
- Propiedad: La característica conceptual de esa entidad (ej. el color).
- Atributo: La variable real en el código que almacena esa propiedad (
self.color).
2. Los 4 Pilares de la POO #
La Programación Orientada a Objetos se sostiene sobre cuatro conceptos fundamentales que garantizan un código modular, seguro y reutilizable.
A. Encapsulamiento #
El encapsulamiento consiste en proteger el estado interno de un objeto, evitando que sea modificado directamente desde el exterior de forma incontrolada.
En Python, no existen modificadores de acceso estrictos como private o public en Java, pero se usa la convención de guiones bajos (_ para protegido, __ para privado). Para interactuar con estos datos, usamos Getters y Setters.
La forma moderna y pythonic de implementarlos es usar el decorador @property:
class Producto:
def __init__(self, precio_inicial):
self.__precio = precio_inicial # Atributo "privado"
@property
def precio(self):
"""Getter: Devuelve el valor."""
return self.__precio
@precio.setter
def precio(self, nuevo_precio):
"""Setter: Valida antes de modificar."""
if nuevo_precio < 0:
raise ValueError("El precio no puede ser negativo")
self.__precio = nuevo_precio
p = Producto(100)
p.precio = 150 # Llama al setter automáticamente
print(p.precio) # Llama al getter automáticamente
B. Herencia y Sobreescritura #
La herencia permite crear nuevas clases (hijas) basadas en clases existentes (padres), heredando sus atributos y métodos.
La sobreescritura (Overriding) ocurre cuando la clase hija redefine un método de la clase padre para darle un comportamiento específico, a menudo usando super() para aprovechar la lógica original.
class Empleado:
def __init__(self, nombre, salario):
self.nombre = nombre
self.salario = salario
def calcular_bono(self):
return self.salario * 0.10
class Gerente(Empleado):
# Sobreescritura del método
def calcular_bono(self):
# Usamos super() para obtener el bono base y le sumamos un extra
bono_base = super().calcular_bono()
return bono_base + 500
g = Gerente("Ana", 1000)
print(g.calcular_bono()) # Salida: 600.0 (100 base + 500 extra)
Herencia Múltiple #
Python soporta herencia múltiple, lo que significa que una clase puede heredar de varias clases a la vez. Python maneja posibles conflictos de métodos utilizando el MRO (Method Resolution Order).
class Volador:
def volar(self): return "Volando alto"
class Nadador:
def nadar(self): return "Nadando profundo"
class Pato(Volador, Nadador): # Hereda de ambas
pass
donald = Pato()
print(donald.volar(), "y", donald.nadar())
C. Abstracción e Interfaces #
La abstracción consiste en exponer solo los detalles esenciales y ocultar la complejidad interna. Se implementa mediante Clases Abstractas. Una clase abstracta no puede ser instanciada y sirve como un molde base que las subclases deben implementar.
Una Interfaz es una forma estricta de abstracción. Funciona como un “contrato”: define qué debe poder hacer un objeto (sus métodos), pero no incluye ninguna lógica sobre cómo debe hacerlo. Garantiza que cualquier clase que implemente esa interfaz tendrá esos métodos disponibles.
En Python, esto se logra con el módulo abc (Abstract Base Classes). Si una clase abstracta solo contiene métodos abstractos (sin ninguna implementación), actúa funcionalmente como una interfaz.
from abc import ABC, abstractmethod
# Esto actúa como una Interfaz
class ProcesadorPago(ABC):
@abstractmethod
def procesar(self, monto):
pass
class PagoTarjeta(ProcesadorPago):
# Obligados a implementar el método procesar
def procesar(self, monto):
print(f"Procesando ${monto} con Tarjeta de Crédito")
class PagoPayPal(ProcesadorPago):
def procesar(self, monto):
print(f"Procesando ${monto} a través de PayPal")
D. Polimorfismo #
El polimorfismo (“muchas formas”) permite que diferentes objetos respondan al mismo método a su propia manera. Se combina perfectamente con la abstracción.
Usando el ejemplo anterior, podemos crear una función que procese pagos sin importarle el tipo exacto de objeto, confiando en el contrato de la interfaz:
def realizar_cobro(procesador: ProcesadorPago, monto: float):
# Polimorfismo en acción: 'procesador' puede ser Tarjeta o PayPal
procesador.procesar(monto)
realizar_cobro(PagoTarjeta(), 1500)
realizar_cobro(PagoPayPal(), 500)
3. Conceptos Avanzados y Herramientas #
Para dominar la POO en Python, debes conocer ciertas herramientas que el lenguaje ofrece para estructurar mejor las clases y asegurar el tipado.
Decoradores #
Un decorador es una función que envuelve a otra función (o método) para extender o modificar su comportamiento sin alterar su código fuente. En POO, los usamos constantemente (como vimos con @property).
# Creando un decorador personalizado
def registrar_auditoria(metodo):
def envoltorio(*args, **kwargs):
print(f"-> LOG: Ejecutando el método '{metodo.__name__}'")
return metodo(*args, **kwargs)
return envoltorio
class Sistema:
@registrar_auditoria
def iniciar(self):
print("El sistema ha iniciado correctamente.")
s = Sistema()
s.iniciar()
# Salida esperada:
# -> LOG: Ejecutando el método 'iniciar'
# El sistema ha iniciado correctamente.
Métodos de Instancia, de Clase y Estáticos #
Python ofrece diferentes formas de vincular métodos dentro de una clase:
- Método de Instancia (default): Recibe
selfcomo primer parámetro. Modifica o lee el estado del objeto. - Método de Clase (
@classmethod): Recibecls(la clase) como primer parámetro. Se usa para modificar el estado general de la clase o como constructores alternativos (Factory methods). - Método Estático (
@staticmethod): No recibe niselfnicls. Es básicamente una función normal que se coloca dentro de la clase por una cuestión de organización lógica, ya que no necesita acceder a propiedades de la clase ni del objeto.
class Fecha:
def __init__(self, dia, mes, año):
self.dia = dia
self.mes = mes
self.año = año
@classmethod
def desde_string(cls, fecha_str):
"""Constructor alternativo. Recibe 'cls' en lugar de 'self'."""
dia, mes, año = map(int, fecha_str.split('-'))
return cls(dia, mes, año)
@staticmethod
def es_bisiesto(año):
"""Lógica pura. No necesita acceder a 'self' ni 'cls'."""
return año % 4 == 0 and (año % 100 != 0 or año % 400 == 0)
# Uso del classmethod (Factory)
hoy = Fecha.desde_string("22-07-2025")
# Uso del staticmethod
print(Fecha.es_bisiesto(2024)) # True
Generics (Tipado Genérico) #
Con la evolución de Python hacia el tipado estático opcional, podemos usar Generics para crear clases que puedan trabajar con cualquier tipo de dato, manteniendo la seguridad de tipos en el análisis estático (ideal para IDEs o herramientas como mypy).
from typing import TypeVar, Generic
# Declaramos una variable de tipo T
T = TypeVar('T')
class Caja(Generic[T]):
def __init__(self, contenido: T):
self.contenido = contenido
def obtener_contenido(self) -> T:
return self.contenido
# Al instanciar, especificamos el tipo esperado
caja_enteros = Caja[int](123)
caja_strings = Caja[str]("Un mensaje")