Programación Orientada a Objetos
Aprende los fundamentos de la programación orientada a objetos en Python, incluyendo clases, objetos, herencia, polimorfismo y encapsulamiento.
Cristian Escalante
Última actualización: 21 de mayo de 2025
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
- Sigue el principio de responsabilidad única: Cada clase debe tener una sola responsabilidad.
- Usa encapsulamiento adecuadamente: Utiliza convenciones de nomenclatura para indicar qué atributos y métodos son parte de la interfaz pública.
- Favorece la composición sobre la herencia: La composición (tener objetos como atributos) a menudo es más flexible que la herencia.
- Utiliza herencia cuando exista una relación "es un": Un perro es un animal, por lo que la herencia es apropiada.
- No abuses de la herencia múltiple: Puede complicar el código y crear problemas de resolución de métodos.
- Documenta tus clases y métodos: Usa docstrings para explicar el propósito y uso de cada clase y método.
- 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
- Crea una clase
CuentaBancaria
con métodos para depositar, retirar y consultar saldo. Luego, crea subclasesCuentaAhorro
yCuentaCorriente
con comportamientos específicos. - Implementa un sistema de gestión de empleados con una clase base
Empleado
y subclases comoGerente
,Desarrollador
yDiseñador
, cada una con atributos y métodos específicos. - Diseña una jerarquía de clases para representar diferentes tipos de vehículos, utilizando herencia y polimorfismo.
- Crea una clase
Tienda
que utilice composición para gestionar productos, inventario y ventas. - 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.
<AppApaSeptima