Fundamentos de Programación Orientada a Objetos en Python

¿Qué es la Programación Orientada a Objetos?

La Programación Orientada a Objetos (POO) es un paradigma de programación basado en el concepto de "objetos", que representan entidades del mundo real. Cada objeto contiene datos (atributos) y código (métodos) para manipular esos datos.

La POO nos permite organizar nuestro código de manera modular, facilitando su reutilización y mantenimiento.

Ventajas de la POO
  • Modularidad: El código se organiza en unidades separadas y reutilizables.
  • Reutilización: Las clases pueden ser utilizadas para crear múltiples objetos.
  • Mantenibilidad: Los cambios afectan solo a partes específicas del código.
  • Escalabilidad: Facilita la ampliación del sistema con nuevas funcionalidades.
  • Abstracción: Permite representar entidades complejas de forma simplificada.

Clases y Objetos

¿Qué son las Clases?

Una clase es un plano o plantilla que define las características y comportamientos de un tipo de objeto. Es como un molde para crear objetos.

¿Qué son los Objetos?

Un objeto es una instancia particular de una clase. Representa una entidad específica basada en la plantilla definida por su clase.

Sintaxis para Definir una Clase
class NombreClase:
    # Atributos de clase
    atributo_clase = valor
    
    # Método constructor
    def __init__(self, parámetros):
        # Atributos de instancia
        self.atributo1 = valor1
        self.atributo2 = valor2
    
    # Métodos de instancia
    def nombre_metodo(self, parámetros):
        # Código del método
        pass
clase_y_objeto.py
class Persona:
    """
    Clase que representa a una persona.
    """
    # Atributo de clase (compartido por todas las instancias)
    especie = "Humano"
    
    # Método constructor
    def __init__(self, nombre, edad):
        # Atributos de instancia (únicos para cada objeto)
        self.nombre = nombre
        self.edad = edad
    
    # Método de instancia
    def presentarse(self):
        return f"¡Hola! Me llamo {self.nombre} y tengo {self.edad} años."
    
    # Otro método de instancia
    def cumplir_años(self):
        self.edad += 1
        return f"{self.nombre} ahora tiene {self.edad} años."


# Crear instancias (objetos) de la clase Persona
persona1 = Persona("Ana", 25)
persona2 = Persona("Carlos", 30)

# Acceder a atributos
print(f"Nombre de persona1: {persona1.nombre}")
print(f"Edad de persona1: {persona1.edad}")
print(f"Especie de persona1: {persona1.especie}")

print(f"Nombre de persona2: {persona2.nombre}")
print(f"Edad de persona2: {persona2.edad}")
print(f"Especie de persona2: {persona2.especie}")

# Llamar a métodos
print(persona1.presentarse())
print(persona2.presentarse())

# Modificar atributos
persona1.nombre = "Ana María"
print(f"Nuevo nombre de persona1: {persona1.nombre}")

# Llamar al método cumplir_años
print(persona1.cumplir_años())
>>> Ejecuta el código para ver la salida
Puntos Clave
  • self: Es una referencia al objeto actual. Es el primer parámetro de cada método de instancia.
  • Atributos de clase: Son compartidos por todas las instancias de la clase.
  • Atributos de instancia: Son específicos para cada objeto creado.
  • __init__: Es el método constructor que se llama automáticamente al crear un objeto.

Tipos de Métodos en Python

Python permite definir varios tipos de métodos dentro de una clase, cada uno con un propósito específico:

1. Métodos de Instancia

Son los métodos más comunes. Operan en una instancia específica de la clase y pueden acceder y modificar los atributos del objeto.

2. Métodos de Clase

Operan en la clase en sí misma, en lugar de en instancias específicas. Se definen usando el decorador @classmethod.

3. Métodos Estáticos

No operan en la clase ni en sus instancias. Son funciones normales que pertenecen al espacio de nombres de la clase. Se definen usando el decorador @staticmethod.

tipos_metodos.py
class Estudiante:
    # Atributo de clase
    escuela = "Instituto Tecnológico"
    
    def __init__(self, nombre, edad, calificaciones=None):
        # Atributos de instancia
        self.nombre = nombre
        self.edad = edad
        self.calificaciones = calificaciones if calificaciones else []
    
    # Método de instancia
    def agregar_calificacion(self, calificacion):
        self.calificaciones.append(calificacion)
        return f"Calificación {calificacion} agregada para {self.nombre}"
    
    def promedio(self):
        if not self.calificaciones:
            return 0
        return sum(self.calificaciones) / len(self.calificaciones)
    
    # Método de clase
    @classmethod
    def cambiar_escuela(cls, nueva_escuela):
        cls.escuela = nueva_escuela
        return f"La escuela ahora es: {cls.escuela}"
    
    @classmethod
    def crear_desde_cadena(cls, cadena):
        """Crea un estudiante desde una cadena con formato 'nombre,edad,cal1,cal2,...'"""
        partes = cadena.split(',')
        nombre = partes[0]
        edad = int(partes[1])
        calificaciones = [float(cal) for cal in partes[2:]]
        
        return cls(nombre, edad, calificaciones)
    
    # Método estático
    @staticmethod
    def es_aprobado(calificacion):
        """Verifica si una calificación es aprobatoria (>= 70)"""
        return calificacion >= 70


# Crear estudiantes
estudiante1 = Estudiante("María", 20)
estudiante2 = Estudiante("Juan", 19, [85, 90, 78])

# Usar método de instancia
print(estudiante1.agregar_calificacion(95))
print(estudiante1.agregar_calificacion(87))
print(f"Promedio de {estudiante1.nombre}: {estudiante1.promedio()}")
print(f"Promedio de {estudiante2.nombre}: {estudiante2.promedio()}")

# Usar método de clase
print(Estudiante.cambiar_escuela("Universidad Nacional"))
print(f"Escuela de estudiante1: {estudiante1.escuela}")
print(f"Escuela de estudiante2: {estudiante2.escuela}")

# Usar método de clase para crear un nuevo estudiante
nuevo_estudiante = Estudiante.crear_desde_cadena("Pedro,21,76,82,91")
print(f"Nuevo estudiante: {nuevo_estudiante.nombre}")
print(f"Calificaciones: {nuevo_estudiante.calificaciones}")
print(f"Promedio: {nuevo_estudiante.promedio()}")

# Usar método estático
for calificacion in [65, 70, 90]:
    estado = "aprobado" if Estudiante.es_aprobado(calificacion) else "reprobado"
    print(f"Calificación {calificacion}: {estado}")
>>> Ejecuta el código para ver la salida
Comparación de Tipos de Métodos
Tipo Decorador Primer parámetro Acceso a atributos de clase Acceso a atributos de instancia
Instancia Ninguno self
Clase @classmethod cls No
Estático @staticmethod Ninguno No No

El Constructor en Python

El constructor es un método especial que se llama automáticamente cuando se crea una instancia de una clase. En Python, el constructor se define con el método __init__.

Sintaxis del Constructor
class MiClase:
    def __init__(self, parametro1, parametro2, ...):
        self.atributo1 = parametro1
        self.atributo2 = parametro2
        # Inicialización de otros atributos
        ...

Usos del Constructor

  • Inicializar atributos de instancia
  • Realizar configuraciones necesarias al crear el objeto
  • Validar datos iniciales
  • Asignar valores por defecto
constructores.py
class Producto:
    # Constructor con parámetros obligatorios y opcionales
    def __init__(self, nombre, precio, stock=0, categoria=None):
        # Inicialización de atributos
        self.nombre = nombre
        self.precio = precio
        self.stock = stock
        self.categoria = categoria if categoria else "Sin categoría"
        
        # Validación básica
        if precio < 0:
            print(f"Advertencia: El precio de {nombre} no debería ser negativo")
        
        # Cálculo de atributos derivados
        self.impuesto = precio * 0.16  # 16% de impuesto
        
        # Contador de productos vendidos
        self.vendidos = 0
    
    def mostrar_info(self):
        return f"Producto: {self.nombre}, Precio: ${self.precio}, Stock: {self.stock}, Categoría: {self.categoria}"
    
    def vender(self, cantidad=1):
        if cantidad <= self.stock:
            self.stock -= cantidad
            self.vendidos += cantidad
            return f"Vendidos {cantidad} de {self.nombre}. Stock restante: {self.stock}"
        else:
            return f"Error: Stock insuficiente. Solo hay {self.stock} unidades disponibles."


# Crear productos con diferentes combinaciones de parámetros
producto1 = Producto("Laptop", 999.99, 10, "Electrónica")
producto2 = Producto("Teclado", 49.99, 20)  # Sin categoría (usa el valor por defecto)
producto3 = Producto("Mouse", 19.99)  # Sin stock ni categoría (usa los valores por defecto)
producto4 = Producto("Monitor", -150, 5, "Periféricos")  # Precio negativo (generará advertencia)

# Mostrar información de los productos
print(producto1.mostrar_info())
print(producto2.mostrar_info())
print(producto3.mostrar_info())
print(producto4.mostrar_info())

# Vender productos
print("\nRealizando ventas:")
print(producto1.vender(2))
print(producto2.vender(25))  # Intentar vender más de lo que hay en stock
print(producto3.vender())  # Usar el valor por defecto (1)
>>> Ejecuta el código para ver la salida

Constructor con Valores por Defecto

En el ejemplo anterior, vimos cómo definir valores por defecto para parámetros opcionales. Esto es muy útil para hacer que las clases sean flexibles y fáciles de usar.

Buenas Prácticas
  • Coloca los parámetros obligatorios primero en el constructor
  • Usa valores por defecto para parámetros opcionales
  • Realiza validaciones en el constructor para garantizar que los objetos se creen en un estado válido
  • No realices operaciones complejas o que consuman mucho tiempo en el constructor
  • Considera usar métodos de clase como constructores alternativos para diferentes formas de crear objetos

Métodos Mágicos (Dunder Methods)

Los métodos mágicos (o dunder methods, por "double underscore") son métodos especiales que comienzan y terminan con doble guion bajo. Permiten definir cómo se comportan los objetos con operadores y funciones integradas de Python.

Métodos Mágicos Comunes
  • __init__: El constructor
  • __str__: Representación legible para humanos (llamada por str() y print())
  • __repr__: Representación oficial del objeto (llamada por repr())
  • __len__: Implementa el comportamiento de len()
  • __add__, __sub__, __mul__, etc.: Sobrecarga de operadores (+, -, *, etc.)
  • __eq__, __lt__, __gt__, etc.: Comparaciones (==, <, >, etc.)
  • __getitem__, __setitem__: Acceso a índices y asignación
metodos_magicos.py
class Vector:
    """Clase que representa un vector matemático en 2D."""
    
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    
    # Representación para humanos
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    # Representación para desarrolladores
    def __repr__(self):
        return f"Vector(x={self.x}, y={self.y})"
    
    # Sobrecarga de operadores
    def __add__(self, otro):
        """Vector + Vector"""
        if isinstance(otro, Vector):
            return Vector(self.x + otro.x, self.y + otro.y)
        else:
            raise TypeError("Solo se puede sumar con otro Vector")
    
    def __sub__(self, otro):
        """Vector - Vector"""
        if isinstance(otro, Vector):
            return Vector(self.x - otro.x, self.y - otro.y)
        else:
            raise TypeError("Solo se puede restar con otro Vector")
    
    def __mul__(self, escalar):
        """Vector * escalar"""
        if isinstance(escalar, (int, float)):
            return Vector(self.x * escalar, self.y * escalar)
        else:
            raise TypeError("Solo se puede multiplicar por un número")
    
    # Para permitir: escalar * Vector
    def __rmul__(self, escalar):
        """escalar * Vector"""
        return self.__mul__(escalar)
    
    # Comparación
    def __eq__(self, otro):
        """Vector == Vector"""
        if isinstance(otro, Vector):
            return self.x == otro.x and self.y == otro.y
        return False
    
    # Para obtener la longitud (magnitud) del vector
    def __abs__(self):
        """Magnitud del vector: |Vector|"""
        return (self.x**2 + self.y**2)**0.5
    
    # Para el operador len()
    def __len__(self):
        """Dimensión del vector (siempre 2 para vectores 2D)"""
        return 2


# Crear vectores
v1 = Vector(3, 4)
v2 = Vector(1, 1)
v3 = Vector(3, 4)

print("Vectores creados:")
print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v3 = {v3}")

# Representación para desarrolladores
print("\nRepresentación para desarrolladores:")
print(f"repr(v1) = {repr(v1)}")

# Operaciones aritméticas
print("\nOperaciones:")
print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
print(f"v1 * 2 = {v1 * 2}")
print(f"3 * v2 = {3 * v2}")  # Esto usa __rmul__

# Comparaciones
print("\nComparaciones:")
print(f"v1 == v2: {v1 == v2}")
print(f"v1 == v3: {v1 == v3}")

# Funciones integradas
print("\nFunciones integradas:")
print(f"abs(v1) = {abs(v1)}")  # Magnitud del vector
print(f"len(v1) = {len(v1)}")  # Dimensión del vector

# Usar un método mágico directamente (no recomendado, pero posible)
print("\nLlamada directa a un método mágico:")
print(f"v1.__add__(v2) = {v1.__add__(v2)}")
>>> Ejecuta el código para ver la salida
Ventajas de los Métodos Mágicos
  • Permiten que los objetos de nuestras clases se comporten como tipos integrados de Python
  • Hacen que el código sea más intuitivo al usar operadores comunes
  • Mejoran la legibilidad del código al utilizar sintaxis natural
  • Facilitan la integración con bibliotecas y funciones existentes

Los métodos mágicos son una de las características más potentes de Python para la programación orientada a objetos, ya que permiten crear clases que se integran perfectamente con el resto del lenguaje.

Ejercicio Práctico

Creación de una Clase Cuenta Bancaria

Vamos a poner en práctica los conceptos aprendidos creando una clase CuentaBancaria con las siguientes características:

  • Atributos para número de cuenta, titular y saldo
  • Métodos para depositar y retirar dinero
  • Métodos mágicos para comparación y representación
  • Un método de clase para crear cuentas con bonificación
  • Un método estático para validar números de cuenta
ejercicio_cuenta.py
# Crea aquí tu clase CuentaBancaria
# ...

Resumen: Fundamentos de POO

En esta sección, hemos aprendido los conceptos fundamentales de la Programación Orientada a Objetos en Python:

  • Clases y objetos: Las clases son plantillas que definen atributos y métodos, mientras que los objetos son instancias específicas de una clase.
  • Atributos: Pueden ser de clase (compartidos entre todas las instancias) o de instancia (específicos de cada objeto).
  • Métodos: Incluyen métodos de instancia, métodos de clase y métodos estáticos, cada uno con un propósito específico.
  • Constructor: El método __init__ que inicializa un objeto cuando se crea.
  • Métodos mágicos: Permiten que nuestros objetos interactúen con operadores y funciones integradas de Python.
Puntos Clave
  • La POO es un paradigma que organiza el código en torno a objetos que contienen datos y comportamiento.
  • Python es un lenguaje flexible que permite implementar la POO de manera elegante y expresiva.
  • Los conceptos fundamentales (clases, objetos, métodos) son la base para temas más avanzados como herencia, polimorfismo y encapsulamiento.
  • El uso adecuado de métodos de clase, estáticos y métodos mágicos puede hacer que nuestro código sea más limpio y expresivo.