HDP115

Programación Orientada a Objetos

Aprende los fundamentos de la programación orientada a objetos en Python, incluyendo clases, objetos, herencia, polimorfismo y encapsulamiento.

CE

Cristian Escalante

Última actualización: 21 de mayo de 2025

python
programación
POO

Programación Orientada a Objetos en Python

La Programación Orientada a Objetos (POO) es un paradigma de programación que organiza el diseño de software en torno a objetos, que son instancias de clases. Python es un lenguaje multiparadigma que soporta completamente la POO, permitiéndonos crear programas más organizados, modulares y reutilizables.

Conceptos fundamentales de la POO

Clases y Objetos

Una clase es una plantilla o un plano que define las características y comportamientos que tendrán los objetos creados a partir de ella. Un objeto es una instancia concreta de una clase.

# Definición de una clase simple
class Persona:
    def __init__(self, nombre, edad):
        self.nombre = nombre
        self.edad = edad
    
    def saludar(self):
        return f"¡Hola! Me llamo {self.nombre} y tengo {self.edad} años."

# Creación de objetos (instancias de la clase)
persona1 = Persona("Ana", 28)
persona2 = Persona("Carlos", 35)

# Uso de los objetos
print(persona1.saludar())  # Salida: ¡Hola! Me llamo Ana y tengo 28 años.
print(persona2.saludar())  # Salida: ¡Hola! Me llamo Carlos y tengo 35 años.

El método __init__

El método __init__ es un método especial (constructor) que se ejecuta automáticamente cuando se crea un nuevo objeto. Se utiliza para inicializar los atributos del objeto.

class Rectangulo:
    def __init__(self, ancho, alto):
        self.ancho = ancho
        self.alto = alto
        self.area = ancho * alto  # Calculado al crear el objeto

rectangulo = Rectangulo(5, 3)
print(f"Ancho: {rectangulo.ancho}")
print(f"Alto: {rectangulo.alto}")
print(f"Área: {rectangulo.area}")

El parámetro self

El parámetro self es una referencia al propio objeto y se utiliza para acceder a las variables y métodos del objeto. Es el primer parámetro de todos los métodos de instancia.

class Contador:
    def __init__(self, valor_inicial=0):
        self.valor = valor_inicial
    
    def incrementar(self):
        self.valor += 1
    
    def decrementar(self):
        self.valor -= 1
    
    def obtener_valor(self):
        return self.valor

contador = Contador(10)
contador.incrementar()
contador.incrementar()
print(contador.obtener_valor())  # Salida: 12

Los cuatro pilares de la POO

1. Encapsulamiento

El encapsulamiento consiste en ocultar los detalles internos de un objeto y exponer solo lo necesario. En Python, por convención, se utilizan guiones bajos para indicar el nivel de acceso:

class CuentaBancaria:
    def __init__(self, titular, saldo_inicial):
        self.titular = titular       # Atributo público
        self._saldo = saldo_inicial  # Atributo "protegido" (por convención)
        self.__pin = "1234"          # Atributo "privado" (name mangling)
    
    def depositar(self, cantidad):
        if cantidad > 0:
            self._saldo += cantidad
            return True
        return False
    
    def retirar(self, cantidad):
        if 0 < cantidad <= self._saldo:
            self._saldo -= cantidad
            return True
        return False
    
    def consultar_saldo(self):
        return self._saldo
    
    def _generar_estado_cuenta(self):
        # Método "protegido"
        return f"Estado de cuenta para {self.titular}: ${self._saldo}"
    
    def __verificar_pin(self, pin):
        # Método "privado"
        return pin == self.__pin

cuenta = CuentaBancaria("Ana García", 1000)
print(cuenta.consultar_saldo())  # Salida: 1000
cuenta.depositar(500)
print(cuenta.consultar_saldo())  # Salida: 1500

# Acceso a atributos "protegidos" (posible, pero no recomendado)
print(cuenta._saldo)  # Salida: 1500

# Acceso a atributos "privados" (name mangling)
# print(cuenta.__pin)  # Esto generaría un error
print(cuenta._CuentaBancaria__pin)  # Salida: 1234 (name mangling)

Nota: Python no tiene un verdadero encapsulamiento como otros lenguajes. Los atributos con doble guion bajo (__) utilizan "name mangling" (se renombran internamente), pero siguen siendo accesibles.

2. Herencia

La herencia permite crear nuevas clases basadas en clases existentes, heredando sus atributos y métodos.

# Clase base (superclase)
class Animal:
    def __init__(self, nombre, edad):
        self.nombre = nombre
        self.edad = edad
    
    def hacer_sonido(self):
        return "Algún sonido"
    
    def descripcion(self):
        return f"{self.nombre} tiene {self.edad} años"

# Clase derivada (subclase)
class Perro(Animal):
    def __init__(self, nombre, edad, raza):
        # Llamada al constructor de la clase base
        super().__init__(nombre, edad)
        self.raza = raza
    
    # Sobrescribir un método de la clase base
    def hacer_sonido(self):
        return "¡Guau!"
    
    # Método específico de la subclase
    def mover_cola(self):
        return f"{self.nombre} está moviendo la cola"

# Clase derivada (subclase)
class Gato(Animal):
    def hacer_sonido(self):
        return "¡Miau!"

# Crear instancias
mi_perro = Perro("Bobby", 3, "Labrador")
mi_gato = Gato("Luna", 2)

# Usar los objetos
print(mi_perro.descripcion())  # Heredado de Animal
print(mi_perro.hacer_sonido())  # Sobrescrito en Perro
print(mi_perro.mover_cola())   # Específico de Perro

print(mi_gato.descripcion())   # Heredado de Animal
print(mi_gato.hacer_sonido())  # Sobrescrito en Gato

Herencia múltiple

Python permite que una clase herede de múltiples clases base:

class DispositivoElectronico:
    def __init__(self, marca, modelo):
        self.marca = marca
        self.modelo = modelo
        self.encendido = False
    
    def encender(self):
        self.encendido = True
    
    def apagar(self):
        self.encendido = False

class DispositivoConectado:
    def __init__(self):
        self.conectado = False
    
    def conectar(self):
        self.conectado = True
    
    def desconectar(self):
        self.conectado = False

class Telefono(DispositivoElectronico, DispositivoConectado):
    def __init__(self, marca, modelo, numero):
        DispositivoElectronico.__init__(self, marca, modelo)
        DispositivoConectado.__init__(self)
        self.numero = numero
    
    def llamar(self, numero_destino):
        if self.encendido and self.conectado:
            return f"Llamando a {numero_destino} desde {self.numero}"
        return "No se puede realizar la llamada"

mi_telefono = Telefono("Samsung", "Galaxy S21", "123-456-7890")
mi_telefono.encender()
mi_telefono.conectar()
print(mi_telefono.llamar("098-765-4321"))

3. Polimorfismo

El polimorfismo permite que objetos de diferentes clases respondan al mismo método de manera diferente.

def hacer_sonar_animal(animal):
    return animal.hacer_sonido()

# Usando el polimorfismo con las clases anteriores
animales = [
    Animal("Genérico", 1),
    Perro("Bobby", 3, "Labrador"),
    Gato("Luna", 2)
]

for animal in animales:
    print(f"{animal.nombre}: {animal.hacer_sonido()}")

4. Abstracción

La abstracción consiste en ocultar la complejidad y mostrar solo lo esencial. En Python, podemos usar clases abstractas para definir interfaces.

from abc import ABC, abstractmethod

class FiguraGeometrica(ABC):
    @abstractmethod
    def area(self):
        pass
    
    @abstractmethod
    def perimetro(self):
        pass

class Rectangulo(FiguraGeometrica):
    def __init__(self, ancho, alto):
        self.ancho = ancho
        self.alto = alto
    
    def area(self):
        return self.ancho * self.alto
    
    def perimetro(self):
        return 2 * (self.ancho + self.alto)

class Circulo(FiguraGeometrica):
    def __init__(self, radio):
        self.radio = radio
    
    def area(self):
        import math
        return math.pi * self.radio ** 2
    
    def perimetro(self):
        import math
        return 2 * math.pi * self.radio

# No se puede instanciar una clase abstracta
# figura = FiguraGeometrica()  # Esto generaría un error

# Pero sí sus subclases concretas
rectangulo = Rectangulo(5, 3)
circulo = Circulo(4)

print(f"Área del rectángulo: {rectangulo.area()}")
print(f"Perímetro del rectángulo: {rectangulo.perimetro()}")
print(f"Área del círculo: {circulo.area():.2f}")
print(f"Perímetro del círculo: {circulo.perimetro():.2f}")

Métodos especiales (dunder methods)

Python utiliza métodos especiales (también llamados "dunder methods" o "magic methods") para definir cómo se comportan los objetos en diferentes situaciones:

class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # Representación para desarrolladores
    def __repr__(self):
        return f"Punto({self.x}, {self.y})"
    
    # Representación para usuarios
    def __str__(self):
        return f"({self.x}, {self.y})"
    
    # Suma de puntos
    def __add__(self, otro):
        return Punto(self.x + otro.x, self.y + otro.y)
    
    # Comparación de igualdad
    def __eq__(self, otro):
        return self.x == otro.x and self.y == otro.y
    
    # Longitud (distancia al origen)
    def __len__(self):
        import math
        return int(math.sqrt(self.x ** 2 + self.y ** 2))

p1 = Punto(3, 4)
p2 = Punto(1, 2)

print(p1)          # Llama a __str__
print(repr(p1))    # Llama a __repr__
print(p1 + p2)     # Llama a __add__
print(p1 == p2)    # Llama a __eq__
print(len(p1))     # Llama a __len__

Propiedades

Las propiedades permiten definir métodos que se comportan como atributos:

class Temperatura:
    def __init__(self, celsius=0):
        self._celsius = celsius
    
    @property
    def celsius(self):
        return self._celsius
    
    @celsius.setter
    def celsius(self, valor):
        if valor < -273.15:
            raise ValueError("La temperatura no puede ser menor que el cero absoluto")
        self._celsius = valor
    
    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, valor):
        self.celsius = (valor - 32) * 5/9

temp = Temperatura(25)
print(f"Celsius: {temp.celsius}°C")
print(f"Fahrenheit: {temp.fahrenheit}°F")

temp.celsius = 30
print(f"Celsius: {temp.celsius}°C")
print(f"Fahrenheit: {temp.fahrenheit}°F")

temp.fahrenheit = 68
print(f"Celsius: {temp.celsius}°C")
print(f"Fahrenheit: {temp.fahrenheit}°F")

Métodos estáticos y de clase

Python permite definir métodos que pertenecen a la clase en lugar de a las instancias:

class MathUtils:
    # Variable de clase
    PI = 3.14159
    
    def __init__(self):
        # Este es un método de instancia
        self.valor = 0
    
    @staticmethod
    def es_primo(n):
        # Método estático: no accede a self ni a cls
        if n <= 1:
            return False
        for i in range(2, int(n**0.5) + 1):
            if n % i == 0:
                return False
        return True
    
    @classmethod
    def area_circulo(cls, radio):
        # Método de clase: accede a cls (la clase)
        return cls.PI * radio ** 2

# Uso de métodos estáticos y de clase
print(MathUtils.es_primo(17))       # True
print(MathUtils.area_circulo(5))    # 78.53975

# También se pueden llamar desde una instancia
utils = MathUtils()
print(utils.es_primo(23))           # True
print(utils.area_circulo(3))        # 28.27431

Ejemplo práctico: Sistema de gestión de biblioteca

Vamos a crear un sistema simple de gestión de biblioteca utilizando POO:

class Libro:
    def __init__(self, titulo, autor, isbn, copias=1):
        self.titulo = titulo
        self.autor = autor
        self.isbn = isbn
        self.copias_disponibles = copias
    
    def __str__(self):
        return f"{self.titulo} por {self.autor} (ISBN: {self.isbn})"
    
    def prestar(self):
        if self.copias_disponibles > 0:
            self.copias_disponibles -= 1
            return True
        return False
    
    def devolver(self):
        self.copias_disponibles += 1
        return True

class Usuario:
    def __init__(self, id, nombre, email):
        self.id = id
        self.nombre = nombre
        self.email = email
        self.libros_prestados = []
    
    def __str__(self):
        return f"{self.nombre} (ID: {self.id})"
    
    def prestar_libro(self, libro):
        if libro.prestar():
            self.libros_prestados.append(libro)
            return True
        return False
    
    def devolver_libro(self, libro):
        if libro in self.libros_prestados:
            libro.devolver()
            self.libros_prestados.remove(libro)
            return True
        return False
    
    def listar_libros_prestados(self):
        return self.libros_prestados

class Biblioteca:
    def __init__(self, nombre):
        self.nombre = nombre
        self.catalogo = {}
        self.usuarios = {}
    
    def agregar_libro(self, libro):
        self.catalogo[libro.isbn] = libro
    
    def buscar_libro(self, isbn):
        return self.catalogo.get(isbn)
    
    def registrar_usuario(self, usuario):
        self.usuarios[usuario.id] = usuario
    
    def buscar_usuario(self, id_usuario):
        return self.usuarios.get(id_usuario)
    
    def prestar_libro(self, id_usuario, isbn):
        usuario = self.buscar_usuario(id_usuario)
        libro = self.buscar_libro(isbn)
        
        if usuario and libro:
            return usuario.prestar_libro(libro)
        return False
    
    def devolver_libro(self, id_usuario, isbn):
        usuario = self.buscar_usuario(id_usuario)
        libro = self.buscar_libro(isbn)
        
        if usuario and libro:
            return usuario.devolver_libro(libro)
        return False

# Uso del sistema
biblioteca = Biblioteca("Biblioteca Municipal")

# Agregar libros
libro1 = Libro("El Quijote", "Miguel de Cervantes", "978-84-376-0494-7", 3)
libro2 = Libro("Cien años de soledad", "Gabriel García Márquez", "978-84-376-0494-8", 2)
libro3 = Libro("1984", "George Orwell", "978-84-376-0494-9", 5)

biblioteca.agregar_libro(libro1)
biblioteca.agregar_libro(libro2)
biblioteca.agregar_libro(libro3)

# Registrar usuarios
usuario1 = Usuario(1, "Ana García", "ana@ejemplo.com")
usuario2 = Usuario(2, "Carlos López", "carlos@ejemplo.com")

biblioteca.registrar_usuario(usuario1)
biblioteca.registrar_usuario(usuario2)

# Prestar libros
biblioteca.prestar_libro(1, "978-84-376-0494-7")
biblioteca.prestar_libro(1, "978-84-376-0494-8")
biblioteca.prestar_libro(2, "978-84-376-0494-9")

# Mostrar libros prestados por usuario
print(f"Libros prestados a {usuario1.nombre}:")
for libro in usuario1.listar_libros_prestados():
    print(f"- {libro}")

# Devolver un libro
biblioteca.devolver_libro(1, "978-84-376-0494-7")

# Mostrar libros prestados actualizados
print(f"\nLibros prestados a {usuario1.nombre} después de devolución:")
for libro in usuario1.listar_libros_prestados():
    print(f"- {libro}")

Mejores prácticas en POO con Python

  1. Sigue el principio de responsabilidad única: Cada clase debe tener una sola responsabilidad.
  2. Usa encapsulamiento adecuadamente: Utiliza convenciones de nomenclatura para indicar qué atributos y métodos son parte de la interfaz pública.
  3. Favorece la composición sobre la herencia: La composición (tener objetos como atributos) a menudo es más flexible que la herencia.
  4. Utiliza herencia cuando exista una relación "es un": Un perro es un animal, por lo que la herencia es apropiada.
  5. No abuses de la herencia múltiple: Puede complicar el código y crear problemas de resolución de métodos.
  6. Documenta tus clases y métodos: Usa docstrings para explicar el propósito y uso de cada clase y método.
  7. Sigue las convenciones de nomenclatura de Python:
    • Nombres de clase en PascalCase
    • Métodos y atributos en snake_case
    • Constantes en MAYÚSCULAS

Ejercicios prácticos

  1. Crea una clase CuentaBancaria con métodos para depositar, retirar y consultar saldo. Luego, crea subclases CuentaAhorro y CuentaCorriente con comportamientos específicos.
  2. Implementa un sistema de gestión de empleados con una clase base Empleado y subclases como Gerente, Desarrollador y Diseñador, cada una con atributos y métodos específicos.
  3. Diseña una jerarquía de clases para representar diferentes tipos de vehículos, utilizando herencia y polimorfismo.
  4. Crea una clase Tienda que utilice composición para gestionar productos, inventario y ventas.
  5. Implementa una clase Matriz con sobrecarga de operadores para realizar operaciones matemáticas con matrices.

En la próxima lección, exploraremos el manejo de archivos en Python, aprendiendo a leer, escribir y manipular diferentes tipos de archivos.

Manejo de excepciones
Aprende a controlar errores y situaciones inesperadas en tus...
Manejo de Archivos
Aprende a leer, escribir y manipular archivos en Python, inc...

<AppApaSeptima =' { "title": "Object-Oriented Programming in Python", "author": "Python.org", "url": "https://docs.python.org/3/tutorial/classes.html" }, { "title": "Python's Instance, Class, and Static Methods Demystified", "author": "Real Python", "url": "https://realpython.com/instance-class-and-static-methods-demystified/" }, { "title": "Inheritance and Composition: A Python OOP Guide", "author": "Real Python", "url": "https://realpython.com/inheritance-composition-python/" } ' />

Conceptos Básicos de HTML

Aprende los conceptos básicos de HTML

Conceptos Básicos de CSS

Aprende los conceptos básicos de CSS

Conceptos Básicos de JavaScript

Aprende los conceptos básicos de JavaScript

Conceptos Básicos SQL

Aprende los conceptos básicos de SQL

Conceptos Básicos de GIT

Aprende los conceptos básicos de GIT

Conceptos Básicos de UML

Aprende los conceptos básicos de UML

Refuerzo Academico de Herramientas de Productividad 2025