📚 Unidad 10. Programación Orientada a Objetos
La Programación Orientada a Objetos (POO) es un paradigma que organiza el código en torno a objetos que combinan datos (atributos) y comportamiento (métodos).
10.1. Conceptos Fundamentales
¿Qué es un Objeto?
Un objeto es una entidad que tiene:
- Atributos: Características o datos (variables).
- Métodos: Comportamientos o acciones (funciones).
Ejemplo del mundo real:
Un coche tiene:
- Atributos: color, marca, velocidad, combustible
- Métodos: arrancar(), acelerar(), frenar(), apagar()
¿Qué es una Clase?
Una clase es un molde o plantilla para crear objetos. Define qué atributos y métodos tendrán los objetos.
10.2. Crear Clases y Objetos
Sintaxis Básica
class Coche:
"""Clase que representa un coche."""
pass # Clase vacía
# Crear objetos (instancias)
mi_coche = Coche()
tu_coche = Coche()
print(type(mi_coche)) # <class '__main__.Coche'>
El Método __init__
El método __init__ es el constructor. Se ejecuta automáticamente al crear un objeto.
class Coche:
def __init__(self, marca, modelo, año):
self.marca = marca # Atributo de instancia
self.modelo = modelo
self.año = año
# Crear objetos
mi_coche = Coche("Toyota", "Corolla", 2020)
tu_coche = Coche("Honda", "Civic", 2022)
# Acceder a atributos
print(mi_coche.marca) # Toyota
print(tu_coche.modelo) # Civic
¿Qué es self?
self es una referencia al objeto actual. Permite acceder a sus atributos y métodos.
class Persona:
def __init__(self, nombre):
self.nombre = nombre # self.nombre es el atributo del objeto
def saludar(self):
print(f"Hola, soy {self.nombre}") # Accede al atributo
ana = Persona("Ana")
ana.saludar() # Hola, soy Ana
10.3. Atributos
Atributos de Instancia
Pertenecen a cada objeto individual.
class Estudiante:
def __init__(self, nombre, edad):
self.nombre = nombre # Cada estudiante tiene su nombre
self.edad = edad
self.notas = [] # Lista vacía para cada estudiante
ana = Estudiante("Ana", 20)
luis = Estudiante("Luis", 22)
ana.notas.append(9)
print(ana.notas) # [9]
print(luis.notas) # [] (independiente)
Atributos de Clase
Compartidos por todos los objetos de la clase.
class Estudiante:
# Atributo de clase
escuela = "IES Python"
total_estudiantes = 0
def __init__(self, nombre):
self.nombre = nombre # Atributo de instancia
Estudiante.total_estudiantes += 1
ana = Estudiante("Ana")
luis = Estudiante("Luis")
print(ana.escuela) # IES Python
print(luis.escuela) # IES Python
print(Estudiante.total_estudiantes) # 2
# Modificar atributo de clase
Estudiante.escuela = "IES Nuevo"
print(ana.escuela) # IES Nuevo
print(luis.escuela) # IES Nuevo
10.4. Métodos
Métodos de Instancia
Operan sobre un objeto específico usando self.
class CuentaBancaria:
def __init__(self, titular, saldo=0):
self.titular = titular
self.saldo = saldo
def depositar(self, cantidad):
if cantidad > 0:
self.saldo += cantidad
print(f"Depositados {cantidad}€. Saldo: {self.saldo}€")
def retirar(self, cantidad):
if cantidad > self.saldo:
print("Saldo insuficiente")
else:
self.saldo -= cantidad
print(f"Retirados {cantidad}€. Saldo: {self.saldo}€")
def obtener_saldo(self):
return self.saldo
# Uso
cuenta = CuentaBancaria("Ana", 1000)
cuenta.depositar(500) # Depositados 500€. Saldo: 1500€
cuenta.retirar(200) # Retirados 200€. Saldo: 1300€
print(cuenta.obtener_saldo()) # 1300
Métodos de Clase
Operan sobre la clase, no sobre instancias. Usan @classmethod y cls.
class Empleado:
aumento_anual = 1.05 # 5%
def __init__(self, nombre, salario):
self.nombre = nombre
self.salario = salario
@classmethod
def establecer_aumento(cls, porcentaje):
cls.aumento_anual = 1 + porcentaje / 100
@classmethod
def desde_string(cls, cadena):
"""Constructor alternativo."""
nombre, salario = cadena.split("-")
return cls(nombre, float(salario))
def aplicar_aumento(self):
self.salario *= self.aumento_anual
# Uso
Empleado.establecer_aumento(10) # 10% de aumento
emp1 = Empleado("Ana", 30000)
emp2 = Empleado.desde_string("Luis-35000") # Constructor alternativo
emp1.aplicar_aumento()
print(emp1.salario) # 33000
Métodos Estáticos
No reciben self ni cls. Son funciones relacionadas con la clase.
class Matematicas:
@staticmethod
def es_par(numero):
return numero % 2 == 0
@staticmethod
def factorial(n):
if n <= 1:
return 1
return n * Matematicas.factorial(n - 1)
@staticmethod
def es_primo(n):
if n < 2:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
# No necesitan instancia
print(Matematicas.es_par(4)) # True
print(Matematicas.factorial(5)) # 120
print(Matematicas.es_primo(17)) # True
10.5. Encapsulamiento
El encapsulamiento oculta los detalles internos y protege los datos.
Convenciones de Nombres
class Ejemplo:
def __init__(self):
self.publico = "Accesible" # Público
self._protegido = "Convención" # Protegido (convención)
self.__privado = "Oculto" # Privado (name mangling)
obj = Ejemplo()
print(obj.publico) # Accesible
print(obj._protegido) # Accesible (por convención no debería)
# print(obj.__privado) # AttributeError
print(obj._Ejemplo__privado) # Accesible (name mangling)
Propiedades (Getters y Setters)
class Temperatura:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Getter para celsius."""
return self._celsius
@celsius.setter
def celsius(self, valor):
"""Setter para celsius."""
if valor < -273.15:
raise ValueError("Temperatura por debajo del cero absoluto")
self._celsius = valor
@property
def fahrenheit(self):
"""Propiedad calculada (solo lectura)."""
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, valor):
self._celsius = (valor - 32) * 5/9
# Uso
temp = Temperatura(25)
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
temp.celsius = 30
print(temp.fahrenheit) # 86.0
temp.fahrenheit = 100
print(temp.celsius) # 37.77...
# temp.celsius = -300 # ValueError
Ejemplo Completo de Encapsulamiento
class CuentaBancaria:
def __init__(self, titular, saldo_inicial=0):
self._titular = titular
self.__saldo = saldo_inicial
self.__historial = []
@property
def titular(self):
return self._titular
@property
def saldo(self):
return self.__saldo
@property
def historial(self):
return self.__historial.copy() # Devuelve copia
def depositar(self, cantidad):
if cantidad <= 0:
raise ValueError("La cantidad debe ser positiva")
self.__saldo += cantidad
self.__historial.append(f"Depósito: +{cantidad}€")
def retirar(self, cantidad):
if cantidad <= 0:
raise ValueError("La cantidad debe ser positiva")
if cantidad > self.__saldo:
raise ValueError("Saldo insuficiente")
self.__saldo -= cantidad
self.__historial.append(f"Retiro: -{cantidad}€")
# Uso
cuenta = CuentaBancaria("Ana", 1000)
cuenta.depositar(500)
cuenta.retirar(200)
print(cuenta.saldo) # 1300
print(cuenta.historial) # ['Depósito: +500€', 'Retiro: -200€']
# No se puede modificar directamente
# cuenta.saldo = 999999 # AttributeError
# cuenta.__saldo = 0 # No afecta al atributo real
10.6. Herencia
La herencia permite crear nuevas clases basadas en clases existentes.
Sintaxis Básica
class Animal:
def __init__(self, nombre):
self.nombre = nombre
def hablar(self):
print("El animal hace un sonido")
class Perro(Animal): # Perro hereda de Animal
def hablar(self):
print(f"{self.nombre} dice: ¡Guau!")
class Gato(Animal):
def hablar(self):
print(f"{self.nombre} dice: ¡Miau!")
# Uso
perro = Perro("Max")
gato = Gato("Luna")
perro.hablar() # Max dice: ¡Guau!
gato.hablar() # Luna dice: ¡Miau!
# Verificar herencia
print(isinstance(perro, Perro)) # True
print(isinstance(perro, Animal)) # True
print(issubclass(Perro, Animal)) # True
Método super()
super() permite acceder a la clase padre.
class Vehiculo:
def __init__(self, marca, modelo):
self.marca = marca
self.modelo = modelo
def info(self):
return f"{self.marca} {self.modelo}"
class Coche(Vehiculo):
def __init__(self, marca, modelo, puertas):
super().__init__(marca, modelo) # Llama al __init__ del padre
self.puertas = puertas
def info(self):
return f"{super().info()} - {self.puertas} puertas"
coche = Coche("Toyota", "Corolla", 4)
print(coche.info()) # Toyota Corolla - 4 puertas
Herencia Múltiple
Python permite heredar de múltiples clases.
class Volador:
def volar(self):
print("Estoy volando")
class Nadador:
def nadar(self):
print("Estoy nadando")
class Pato(Volador, Nadador):
def graznar(self):
print("¡Cuac!")
pato = Pato()
pato.volar() # Estoy volando
pato.nadar() # Estoy nadando
pato.graznar() # ¡Cuac!
MRO (Method Resolution Order)
class A:
def metodo(self):
print("A")
class B(A):
def metodo(self):
print("B")
class C(A):
def metodo(self):
print("C")
class D(B, C):
pass
d = D()
d.metodo() # B (busca en B antes que en C)
# Ver orden de resolución
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
10.7. Polimorfismo
El polimorfismo permite que objetos de diferentes clases respondan al mismo método de forma diferente.
class Forma:
def area(self):
raise NotImplementedError("Subclases deben implementar este método")
class Rectangulo(Forma):
def __init__(self, base, altura):
self.base = base
self.altura = altura
def area(self):
return self.base * self.altura
class Circulo(Forma):
def __init__(self, radio):
self.radio = radio
def area(self):
import math
return math.pi * self.radio ** 2
class Triangulo(Forma):
def __init__(self, base, altura):
self.base = base
self.altura = altura
def area(self):
return self.base * self.altura / 2
# Polimorfismo en acción
formas = [
Rectangulo(4, 5),
Circulo(3),
Triangulo(4, 6)
]
for forma in formas:
print(f"{type(forma).__name__}: área = {forma.area():.2f}")
# Rectangulo: área = 20.00
# Circulo: área = 28.27
# Triangulo: área = 12.00
Duck Typing
"Si camina como un pato y hace cuac como un pato, es un pato."
class Archivo:
def leer(self):
return "Contenido del archivo"
class BaseDatos:
def leer(self):
return "Datos de la base de datos"
class API:
def leer(self):
return "Respuesta de la API"
def procesar_datos(fuente):
"""No importa el tipo, solo que tenga método leer()."""
datos = fuente.leer()
print(f"Procesando: {datos}")
# Funciona con cualquier objeto que tenga leer()
procesar_datos(Archivo())
procesar_datos(BaseDatos())
procesar_datos(API())
10.8. Clases Abstractas
Las clases abstractas definen una interfaz que las subclases deben implementar.
from abc import ABC, abstractmethod
class FiguraGeometrica(ABC):
"""Clase abstracta para figuras geométricas."""
@abstractmethod
def area(self):
"""Calcula el área de la figura."""
pass
@abstractmethod
def perimetro(self):
"""Calcula el perímetro de la figura."""
pass
def descripcion(self):
"""Método concreto (no abstracto)."""
return f"Soy una {self.__class__.__name__}"
class Cuadrado(FiguraGeometrica):
def __init__(self, lado):
self.lado = lado
def area(self):
return self.lado ** 2
def perimetro(self):
return 4 * self.lado
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 clase abstracta
# figura = FiguraGeometrica() # TypeError
# Pero sí las subclases concretas
cuadrado = Cuadrado(5)
circulo = Circulo(3)
print(cuadrado.descripcion()) # Soy una Cuadrado
print(f"Área: {cuadrado.area()}") # 25
print(f"Perímetro: {cuadrado.perimetro()}") # 20
10.9. Métodos Mágicos (Dunder Methods)
Los métodos mágicos (double underscore) permiten personalizar el comportamiento de los objetos.
Representación
class Punto:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
"""Para print() y str()"""
return f"Punto({self.x}, {self.y})"
def __repr__(self):
"""Para representación técnica"""
return f"Punto(x={self.x}, y={self.y})"
punto = Punto(3, 4)
print(punto) # Punto(3, 4)
print(repr(punto)) # Punto(x=3, y=4)
Operadores Aritméticos
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, otro):
"""Para + """
return Vector(self.x + otro.x, self.y + otro.y)
def __sub__(self, otro):
"""Para - """
return Vector(self.x - otro.x, self.y - otro.y)
def __mul__(self, escalar):
"""Para * (con escalar)"""
return Vector(self.x * escalar, self.y * escalar)
def __rmul__(self, escalar):
"""Para escalar * vector"""
return self.__mul__(escalar)
def __abs__(self):
"""Para abs()"""
return (self.x**2 + self.y**2) ** 0.5
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 2) # Vector(6, 8)
print(3 * v1) # Vector(9, 12)
print(abs(v1)) # 5.0
Comparación
class Estudiante:
def __init__(self, nombre, nota):
self.nombre = nombre
self.nota = nota
def __eq__(self, otro):
"""Para == """
return self.nota == otro.nota
def __lt__(self, otro):
"""Para < """
return self.nota < otro.nota
def __le__(self, otro):
"""Para <= """
return self.nota <= otro.nota
def __gt__(self, otro):
"""Para > """
return self.nota > otro.nota
def __str__(self):
return f"{self.nombre}: {self.nota}"
estudiantes = [
Estudiante("Ana", 8),
Estudiante("Luis", 9),
Estudiante("María", 7)
]
# sorted() usa __lt__
ordenados = sorted(estudiantes)
for e in ordenados:
print(e)
# María: 7
# Ana: 8
# Luis: 9
Contenedores
class MiLista:
def __init__(self, elementos=None):
self._datos = elementos if elementos else []
def __len__(self):
"""Para len()"""
return len(self._datos)
def __getitem__(self, indice):
"""Para [] lectura"""
return self._datos[indice]
def __setitem__(self, indice, valor):
"""Para [] escritura"""
self._datos[indice] = valor
def __contains__(self, item):
"""Para 'in'"""
return item in self._datos
def __iter__(self):
"""Para iteración"""
return iter(self._datos)
def append(self, item):
self._datos.append(item)
lista = MiLista([1, 2, 3])
print(len(lista)) # 3
print(lista[0]) # 1
print(2 in lista) # True
for item in lista:
print(item)
lista[0] = 10
print(lista[0]) # 10
Contexto (with)
class GestorArchivo:
def __init__(self, nombre, modo):
self.nombre = nombre
self.modo = modo
self.archivo = None
def __enter__(self):
"""Al entrar en 'with'"""
print(f"Abriendo {self.nombre}")
self.archivo = open(self.nombre, self.modo)
return self.archivo
def __exit__(self, tipo_exc, valor_exc, tb):
"""Al salir de 'with'"""
print(f"Cerrando {self.nombre}")
if self.archivo:
self.archivo.close()
return False # No suprimir excepciones
# Uso
with GestorArchivo("test.txt", "w") as f:
f.write("Hola mundo")
# Abriendo test.txt
# Cerrando test.txt
10.10. Composición vs Herencia
Herencia ("es un")
class Vehiculo:
def mover(self):
print("El vehículo se mueve")
class Coche(Vehiculo): # Un coche ES UN vehículo
pass
Composición ("tiene un")
class Motor:
def arrancar(self):
print("Motor arrancado")
def detener(self):
print("Motor detenido")
class Rueda:
def rodar(self):
print("La rueda rueda")
class Coche: # Un coche TIENE UN motor y TIENE ruedas
def __init__(self):
self.motor = Motor()
self.ruedas = [Rueda() for _ in range(4)]
def arrancar(self):
self.motor.arrancar()
def conducir(self):
for rueda in self.ruedas:
rueda.rodar()
coche = Coche()
coche.arrancar()
coche.conducir()
Cuándo Usar Cada Una
- Herencia: Cuando hay una relación clara "es un".
- Composición: Cuando hay una relación "tiene un" o para mayor flexibilidad.
10.11. Ejercicio Completo: Sistema de Gestión
from abc import ABC, abstractmethod
from datetime import datetime
class Persona(ABC):
"""Clase abstracta base para personas."""
def __init__(self, nombre, email):
self.nombre = nombre
self.email = email
self._fecha_registro = datetime.now()
@abstractmethod
def descripcion(self):
pass
def __str__(self):
return f"{self.__class__.__name__}: {self.nombre}"
class Empleado(Persona):
"""Empleado de la empresa."""
_contador = 0
def __init__(self, nombre, email, puesto, salario):
super().__init__(nombre, email)
Empleado._contador += 1
self.id = f"EMP{Empleado._contador:04d}"
self.puesto = puesto
self._salario = salario
self.proyectos = []
@property
def salario(self):
return self._salario
@salario.setter
def salario(self, valor):
if valor < 0:
raise ValueError("El salario no puede ser negativo")
self._salario = valor
def descripcion(self):
return f"{self.nombre} - {self.puesto} ({self.id})"
def asignar_proyecto(self, proyecto):
self.proyectos.append(proyecto)
proyecto.miembros.append(self)
class Cliente(Persona):
"""Cliente de la empresa."""
_contador = 0
def __init__(self, nombre, email, empresa):
super().__init__(nombre, email)
Cliente._contador += 1
self.id = f"CLI{Cliente._contador:04d}"
self.empresa = empresa
self.pedidos = []
def descripcion(self):
return f"{self.nombre} de {self.empresa} ({self.id})"
class Proyecto:
"""Proyecto de la empresa."""
def __init__(self, nombre, descripcion, presupuesto):
self.nombre = nombre
self.descripcion = descripcion
self.presupuesto = presupuesto
self.miembros = []
self.tareas = []
self._completado = False
def agregar_tarea(self, tarea):
self.tareas.append(tarea)
@property
def progreso(self):
if not self.tareas:
return 0
completadas = sum(1 for t in self.tareas if t.completada)
return (completadas / len(self.tareas)) * 100
def __str__(self):
return f"Proyecto: {self.nombre} ({self.progreso:.0f}% completado)"
class Tarea:
"""Tarea de un proyecto."""
def __init__(self, titulo, descripcion):
self.titulo = titulo
self.descripcion = descripcion
self.completada = False
def completar(self):
self.completada = True
def __str__(self):
estado = "✓" if self.completada else "○"
return f"{estado} {self.titulo}"
# --- Uso del sistema ---
# Crear empleados
emp1 = Empleado("Ana García", "ana@empresa.com", "Desarrolladora Senior", 45000)
emp2 = Empleado("Luis Martínez", "luis@empresa.com", "Diseñador UX", 38000)
# Crear proyecto
proyecto = Proyecto("App Móvil", "Desarrollo de app para clientes", 50000)
# Asignar empleados
emp1.asignar_proyecto(proyecto)
emp2.asignar_proyecto(proyecto)
# Agregar tareas
proyecto.agregar_tarea(Tarea("Diseño de interfaz", "Crear mockups"))
proyecto.agregar_tarea(Tarea("Backend API", "Desarrollar endpoints"))
proyecto.agregar_tarea(Tarea("Testing", "Pruebas unitarias"))
# Completar tareas
proyecto.tareas[0].completar()
proyecto.tareas[1].completar()
# Mostrar información
print(emp1.descripcion())
print(proyecto)
for tarea in proyecto.tareas:
print(f" {tarea}")
# Crear cliente
cliente = Cliente("María López", "maria@cliente.com", "TechCorp")
print(cliente.descripcion())
10.12. Resumen de POO
| Concepto | Descripción |
|---|---|
| Clase | Plantilla para crear objetos |
| Objeto | Instancia de una clase |
| Atributo | Variable de un objeto |
| Método | Función de un objeto |
__init__ |
Constructor |
self |
Referencia al objeto actual |
| Encapsulamiento | Ocultar datos internos |
| Herencia | Crear clases basadas en otras |
| Polimorfismo | Mismo método, diferente comportamiento |
| Abstracción | Definir interfaces |
📅 Fecha de creación: Enero 2026
✍️ Autor: Fran García