SECCIÓN 1 — Pilares de la POO
La Programación Orientada a Objetos se basa en cuatro pilares fundamentales. Cada uno resuelve un
problema concreto de diseño de software.
1.1 Abstracción
Mostrar solo lo esencial, ocultar la complejidad interna. Se define QUÉ hace algo sin revelar CÓMO lo
hace.
Concepto clave para el parcial: una clase abstracta no puede instanciarse directamente. Si
intentas hacerlo, Python lanza TypeError.
Mecanismo en Python
• Importar ABC (Abstract Base Class) y abstractmethod del módulo abc
• Decorar métodos con @abstractmethod para que sean obligatorios en subclases
• Toda subclase DEBE implementar todos los métodos abstractos o también será abstracta
Ejemplo comentado — Formas geométricas
abstraccion_formas.py
from abc import ABC, abstractmethod
import math
# ABC convierte la clase en abstracta — NO se puede instanciar
class Forma(ABC):
@abstractmethod # obliga a las subclases a implementarlo
def area(self) -> float:
"""Retorna el área de la forma."""
... # el cuerpo no importa, jamás se ejecuta
@abstractmethod
def perimetro(self) -> float: ...
# Método CONCRETO: se hereda sin necesidad de redefinir
def describir(self) -> str:
return (f'{type(self).__name__}: '
f'area={[Link]():.2f}, perimetro={[Link]():.2f}')
class Circulo(Forma):
def __init__(self, radio: float):
[Link] = radio
def area(self) -> float: # implementación obligatoria
return [Link] * [Link] ** 2
def perimetro(self) -> float:
return 2 * [Link] * [Link]
class Rectangulo(Forma):
def __init__(self, ancho: float, alto: float):
[Link] = ancho
[Link] = alto
def area(self) -> float:
return [Link] * [Link]
def perimetro(self) -> float:
return 2 * ([Link] + [Link])
# Polimorfismo en acción: el código no sabe qué forma concreta es
formas: list[Forma] = [Circulo(5), Rectangulo(4, 6)]
for f in formas:
print([Link]())
Si intentas hacer Forma() directamente, obtienes: TypeError: Can't instantiate abstract class
Forma with abstract methods area, perimetro
Un método abstracto puede tener cuerpo — pero solo se llama con super(). Lo que lo hace
'abstracto' es que OBLIGA a la subclase a redefinirlo.
1.2 Encapsulamiento
Controlar qué se expone y qué se protege del exterior. Los datos internos no deben modificarse
libremente desde fuera del objeto.
Convenciones de privacidad en Python
Sintaxis Nivel Significado
nombre Público Accesible desde cualquier
lugar. Uso libre.
_nombre Privado por convención Señal de 'no uses esto desde
fuera'. Python no lo impide.
__nombre Name mangling activo Python lo renombra a
_Clase__nombre. Dificulta
acceso externo.
@property Acceso controlado Permite validar al leer o
escribir. Sin setter = solo
lectura.
Ejemplo comentado — Cuenta bancaria
encapsulamiento_banco.py
from datetime import datetime
class CuentaBancaria:
def __init__(self, titular: str, saldo_inicial: float = 0.0):
self._titular = titular # convención: privado
self.__saldo = 0.0 # name mangling: real privado
self.__historial: list = []
if saldo_inicial > 0:
self.__depositar_interno(saldo_inicial, 'Deposito inicial')
# @property = getter: permite leer __saldo sin modificarlo
@property
def saldo(self) -> float:
return self.__saldo
# NO hay setter -> [Link] = 999 lanzaria AttributeError
def depositar(self, monto: float) -> None:
if monto <= 0:
raise ValueError('El monto debe ser positivo')
self.__depositar_interno(monto, 'Deposito')
def retirar(self, monto: float) -> None:
if monto <= 0:
raise ValueError('El monto debe ser positivo')
if monto > self.__saldo: # validación antes de modificar
raise ValueError(f'Saldo insuficiente ({self.__saldo:.2f})')
self.__saldo -= monto
# Doble guion = método privado real
def __depositar_interno(self, monto: float, concepto: str) -> None:
self.__saldo += monto
cuenta = CuentaBancaria('Ana', 1000.0)
[Link](500)
[Link](200)
print([Link]) # 1300.0 — OK
# cuenta.__saldo = 999999 # NO funciona (name mangling)
# [Link] = 999999 # AttributeError: no hay setter
@property con setter — Termostato
encapsulamiento_termostato.py
class Termostato:
TEMP_MIN = -273.15
TEMP_MAX = 1000.0
def __init__(self, temp_inicial: float = 20.0):
self.__temperatura = None
[Link] = temp_inicial # usa el setter desde __init__
@property
def temperatura(self) -> float:
return self.__temperatura
@[Link] # mismo nombre + .setter
def temperatura(self, valor: float) -> None:
if not (self.TEMP_MIN <= valor <= self.TEMP_MAX):
raise ValueError(f'Temperatura fuera de rango: {valor}°C')
self.__temperatura = valor
t = Termostato(22.0)
[Link] = 37.5 # usa el setter, valida
[Link] = -300 # ValueError!
Pregunta frecuente de parcial: ¿cuál es la diferencia entre _x y __x? La respuesta: _x es solo
convención (Python no la hace cumplir), __x activa name mangling (Python la renombra
internamente).
1.3 Herencia
Una clase adquiere atributos y métodos de otra. La subclase puede usar lo heredado, sobreescribirlo
con super() y agregar comportamiento nuevo.
Ejemplo comentado — Empleados
herencia_empleados.py
class Empleado:
_contador_ids: int = 0 # variable de clase compartida
def __init__(self, nombre: str, salario_base: float):
Empleado._contador_ids += 1
[Link] = Empleado._contador_ids
[Link] = nombre
self._salario_base = salario_base
def calcular_salario(self) -> float:
return self._salario_base
class EmpleadoComision(Empleado): # hereda de Empleado
def __init__(self, nombre, salario_base, ventas, tasa=0.10):
super().__init__(nombre, salario_base) # llama al padre
[Link] = ventas
self.tasa_comision = tasa
def calcular_salario(self) -> float:
# sobreescribe Y extiende el método del padre
return super().calcular_salario() + ([Link] * self.tasa_comision)
class Gerente(Empleado):
def __init__(self, nombre, salario_base, bono):
super().__init__(nombre, salario_base)
[Link] = bono
[Link]: list[Empleado] = []
def calcular_salario(self) -> float:
return super().calcular_salario() + [Link]
def info(self) -> str:
base = super().info() # reutiliza lógica del padre
return f'{base} | Equipo: {len([Link])} personas'
Herencia múltiple y MRO
herencia_vehiculos.py
class Vehiculo:
def __init__(self, marca, modelo, anio):
[Link] = marca
class Electrico:
def __init__(self, capacidad_bateria, **kwargs):
super().__init__(**kwargs) # MRO: pasa args al siguiente
self.capacidad_bateria = capacidad_bateria
# Herencia múltiple: orden importa para el MRO
class AutoElectrico(Electrico, Vehiculo):
def __init__(self, marca, modelo, anio, capacidad_bateria):
super().__init__(marca=marca, modelo=modelo, anio=anio,
capacidad_bateria=capacidad_bateria)
# Ver el orden de resolución de métodos:
print(AutoElectrico.__mro__)
# (<AutoElectrico> -> <Electrico> -> <Vehiculo> -> <object>)
MRO (Method Resolution Order): Python usa el algoritmo C3 para determinar el orden en que
busca métodos en herencia múltiple. __mro__ te muestra ese orden.
Regla para super() con herencia múltiple: siempre usa **kwargs para pasar argumentos
desconocidos hacia arriba en la cadena MRO.
1.4 Polimorfismo
El mismo mensaje, comportamientos distintos según quién lo recibe. Objetos de distintas clases
responden al mismo método de forma diferente.
Tipo Mecanismo Ejemplo
Por herencia Sobreescribir métodos class Perro(Animal): def
hacer_sonido(): return 'Guau'
Duck typing Mismo método, sin relación de Cualquier objeto
herencia con .renderizar() funciona
Dunder methods Operadores sobrecargados __add__, __len__, __str__,
__eq__, ...
Duck typing — sin herencia formal
duck_typing.py
# Estas clases NO tienen relación de herencia entre sí
class Titulo:
def renderizar(self) -> str:
return f'# {[Link]}'
class MensajeSimple:
def renderizar(self) -> str:
return f'>> {[Link]}'
# La función acepta CUALQUIER cosa que tenga .renderizar()
# Python no exige que hereden de la misma clase
def imprimir_todo(items) -> None:
for item in items: # duck typing puro
print([Link]())
imprimir_todo([Titulo(), MensajeSimple()]) # funciona!
Dunder methods — operadores sobrecargados
dunder_methods.py
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, otro): # v1 + v2
return Vector2D(self.x + otro.x, self.y + otro.y)
def __mul__(self, escalar): # v * 2
return Vector2D(self.x * escalar, self.y * escalar)
def __abs__(self): # abs(v)
return (self.x**2 + self.y**2) ** 0.5
def __len__(self): # len(v)
return 2
def __eq__(self, otro): # v1 == v2
return self.x == otro.x and self.y == otro.y
def __repr__(self): # repr(v) y en la consola
return f'Vector2D({self.x}, {self.y})'
v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
print(v1 + v2) # Vector2D(4, 6)
print(abs(v1)) # 5.0
print(len(v1)) # 2
Dunder methods más importantes: __init__, __str__, __repr__, __len__, __add__, __sub__,
__mul__, __eq__, __lt__, __getitem__, __iter__, __contains__
SECCIÓN 2 — Principios SOLID
SOLID es un acrónimo de cinco principios de diseño orientado a objetos que hacen el código más
mantenible, extensible y testeable.
Letra Nombre Regla
S Single Responsibility Una clase = una razón para
cambiar
O Open/Closed Abierto para extensión,
cerrado para modificación
L Liskov Substitution Una subclase debe poder
reemplazar al padre sin
romper nada
I Interface Segregation Interfaces pequeñas, no una
interfaz gigante
D Dependency Inversion Depender de abstracciones,
no de implementaciones
concretas
S — Single Responsibility Principle (SRP)
Una clase debe tener una sola razón para cambiar. Si puedes describir la clase usando 'y' en la mitad,
la estás dividiendo.
Violación ✗ Correcto ✓
class Pedido: class Pedido: # solo
# Hace TRES cosas: datos
def def
calcular_total(self): ... calcular_total(self): ...
def guardar_en_db(self): ...
def class RepositorioPedido: # solo
enviar_confirmacion(self): ... DB
def guardar(self, p): ...
class EmailPedido: # solo
email
def confirmar(self, p): ...
Ejemplo completo — Factura
srp_factura.py
from dataclasses import dataclass, field
@dataclass
class LineaFactura:
descripcion: str
cantidad: int
precio_unitario: float
@property
def subtotal(self) -> float:
return [Link] * self.precio_unitario
# CLASE 1: solo representa la factura y su lógica de negocio
@dataclass
class Factura:
numero: str
cliente: str
lineas: list[LineaFactura] = field(default_factory=list)
def total(self) -> float:
return sum([Link] for l in [Link])
# CLASE 2: solo se encarga de formatear/imprimir
class ImpresoraFactura:
def imprimir(self, factura: Factura) -> str:
lineas = [f'FACTURA #{[Link]}',
f'Cliente: {[Link]}', '-' * 30]
for l in [Link]:
[Link](f' {[Link]:<20} ${[Link]:>8.2f}')
[Link](f' TOTAL ${[Link]():>8.2f}')
return ' '.join(lineas)
# CLASE 3: solo se encarga de persistencia
class RepositorioFactura:
def __init__(self):
self._almacen: dict[str, Factura] = {}
def guardar(self, factura: Factura) -> None:
self._almacen[[Link]] = factura
def buscar(self, numero: str) -> Factura | None:
return self._almacen.get(numero)
Truco mental SRP: si para describir la clase necesitas decir 'y' ('guarda datos Y envía emails'),
entonces tiene más de una responsabilidad.
O — Open/Closed Principle (OCP)
Abierto para extensión, cerrado para modificación. Agregar nueva funcionalidad NO debe requerir
editar código existente.
Violación ✗ Correcto ✓
def calcular_descuento(tipo, class Descuento(ABC):
precio): @abstractmethod
if tipo == 'vip': def aplicar(self,
return precio * 0.8 precio): ...
elif tipo == 'estudiante':
return precio * 0.9 class DescuentoVIP(Descuento):
# Nuevo tipo = EDITAR esta def aplicar(self, p): return
función p * 0.8
# Nuevo tipo = nueva clase, sin
editar nada
Ejemplo completo — Exportadores extensibles
ocp_exportadores.py
from abc import ABC, abstractmethod
import json
# Contrato base: nunca cambia
class Exportador(ABC):
@abstractmethod
def exportar(self, datos: list[dict]) -> str: ...
class ExportadorCSV(Exportador):
def exportar(self, datos: list[dict]) -> str:
if not datos: return ''
cabecera = ','.join(datos[0].keys())
filas = [','.join(str(v) for v in [Link]()) for d in datos]
return ' '.join([cabecera] + filas)
class ExportadorJSON(Exportador):
def exportar(self, datos: list[dict]) -> str:
return [Link](datos, ensure_ascii=False, indent=2)
class ExportadorMarkdown(Exportador):
def exportar(self, datos: list[dict]) -> str:
cols = list(datos[0].keys())
sep = ' | '
divisor = [Link](['---'] * len(cols))
filas = [[Link](str(d[c]) for c in cols) for d in datos]
return ' '.join([[Link](cols), divisor] + filas)
# ServicioReportes NUNCA cambia aunque agreguemos exportadores
class ServicioReportes:
def __init__(self, exportador: Exportador):
[Link] = exportador
def generar(self, datos: list[dict]) -> str:
return [Link](datos)
# Agregar ExportadorXML no requiere tocar ServicioReportes
OCP y los if/elif: cuando una función tiene una cadena larga de if/elif basada en 'tipo', es
candidata a ser refactorizada con OCP. Cada rama se convierte en una clase.
L — Liskov Substitution Principle (LSP)
Una subclase debe poder reemplazar a su clase padre sin romper el programa. Si necesitas preguntar
'¿es un Perro o un Gato?' para comportarte diferente, algo está mal.
El problema clásico: Cuadrado hereda de Rectángulo
Violación ✗ Correcto ✓
class Rectangulo: class Forma(ABC): # padre
def __init__(self, ancho, común
alto): @abstractmethod
[Link] = ancho def area(self): ...
[Link] = alto
class Rectangulo(Forma): #
class Cuadrado(Rectangulo): hermano
@[Link] def area(self):
def ancho(self, v): return [Link] *
# cambiar ancho cambia [Link]
alto también
self._ancho = self._alto class Cuadrado(Forma): #
= v hermano
# duplicar_ancho() da resultado def area(self):
inesperado! return [Link] ** 2
Forma correcta — jerarquía con abstracción compartida
lsp_formas.py
from abc import ABC, abstractmethod
class Forma(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimetro(self) -> float: ...
class Rectangulo(Forma):
def __init__(self, ancho: float, alto: float):
[Link] = ancho
[Link] = alto
def area(self) -> float: return [Link] * [Link]
def perimetro(self) -> float: return 2 * ([Link] + [Link])
class Cuadrado(Forma): # HERMANO de Rectangulo, no hijo
def __init__(self, lado: float):
[Link] = lado
def area(self) -> float: return [Link] ** 2
def perimetro(self) -> float: return 4 * [Link]
# Esta función acepta CUALQUIER Forma sin importar el tipo concreto
def describir_forma(f: Forma) -> None:
print(f'{type(f).__name__}: area={[Link]():.2f}')
for forma in [Rectangulo(4, 5), Cuadrado(4)]:
describir_forma(forma) # funciona para ambos
Señales de violación LSP: una subclase lanza NotImplementedError en un método heredado;
una subclase tiene precondiciones más estrictas; el código cliente usa isinstance() para
comportarse diferente con cada subclase.
I — Interface Segregation Principle (ISP)
Ninguna clase debe depender de métodos que no usa. Prefiere muchas interfaces pequeñas sobre
una interfaz grande.
Violación ✗ Correcto ✓
class Trabajador(ABC): class Laborable(ABC):
@abstractmethod @abstractmethod
def trabajar(self): ... def trabajar(self): ...
@abstractmethod
def comer(self): ... class Biologico(ABC):
@abstractmethod
class Robot(Trabajador): def comer(self): ...
def trabajar(self): ...
def comer(self): pass # class Robot(Laborable): ...
forzado! # solo trabaja
class Humano(Laborable,
Biologico): ...
Ejemplo completo — Impresoras
isp_impresoras.py
from abc import ABC, abstractmethod
# Interfaces pequeñas y enfocadas
class Imprimible(ABC):
@abstractmethod
def imprimir(self, documento: str) -> None: ...
class Escaneable(ABC):
@abstractmethod
def escanear(self) -> str: ...
class Faxeable(ABC):
@abstractmethod
def enviar_fax(self, numero: str, doc: str) -> None: ...
# Implementa solo las interfaces que puede cumplir
class ImpresoraBasica(Imprimible):
def imprimir(self, documento: str) -> None:
print(f'[Basica] Imprimiendo: {documento[:40]}...')
class ImpresoraMultifuncion(Imprimible, Escaneable, Faxeable):
def imprimir(self, documento: str) -> None:
print(f'[Multi] Imprimiendo en alta calidad')
def escanear(self) -> str:
return '[Multi] Escaneado a 600 DPI'
def enviar_fax(self, numero: str, doc: str) -> None:
print(f'[Multi] Fax enviado a {numero}')
# Funciones que dependen solo de la interfaz que necesitan
def imprimir_reporte(imp: Imprimible, reporte: str) -> None:
[Link](reporte) # no importa si tiene fax o no
def digitalizar(scan: Escaneable) -> str:
return [Link]()
Señal de violación ISP: una clase implementa una interfaz y tiene que dejar métodos vacíos
(pass) o lanzar NotImplementedError. Es señal de que la interfaz es demasiado grande.
D — Dependency Inversion Principle (DIP)
Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de
abstracciones. Esto se logra con inyección de dependencias.
Violación ✗ Correcto ✓
class ServicioUsuario: class ServicioUsuario:
def __init__(self): def __init__(self,
# Acoplado a MySQL db: BaseDatos,
específicamente email:
[Link] = ServicioEmail):
MySQLConexion() [Link] = db
[Link] = GmailSMTP() [Link] = email
# Imposible testear sin # Facil de testear con mocks
MySQL real
Ejemplo completo con mocks para testing
dip_notificaciones.py
from abc import ABC, abstractmethod
# Abstracciones (contratos)
class RepositorioUsuarios(ABC):
@abstractmethod
def buscar_por_id(self, uid: int) -> dict | None: ...
@abstractmethod
def guardar(self, usuario: dict) -> None: ...
class ServicioEmail(ABC):
@abstractmethod
def enviar(self, destino: str, asunto: str, cuerpo: str) -> bool: ...
# Implementaciones concretas (bajo nivel)
class RepositorioMemoria(RepositorioUsuarios):
def __init__(self):
self._datos: dict[int, dict] = {}
def buscar_por_id(self, uid): return self._datos.get(uid)
def guardar(self, usuario): self._datos[usuario['id']] = usuario
class EmailConsola(ServicioEmail):
def enviar(self, destino, asunto, cuerpo) -> bool:
print(f'Email a {destino}: {asunto}')
return True
# Módulo de alto nivel: depende SOLO de abstracciones
class ServicioRegistro:
def __init__(self,
repo: RepositorioUsuarios, # abstracción
email: ServicioEmail): # abstracción
self._repo = repo
self._email = email
self._siguiente_id = 1
def registrar(self, nombre: str, email_addr: str) -> dict:
usuario = {'id': self._siguiente_id, 'nombre': nombre,
'email': email_addr}
self._siguiente_id += 1
self._repo.guardar(usuario)
self._email.enviar(email_addr, 'Bienvenido',
f'Hola {nombre}, tu cuenta fue creada.')
return usuario
# Para testing: se reemplazan dependencias sin tocar ServicioRegistro
class EmailMock(ServicioEmail):
def __init__(self): [Link] = []
def enviar(self, d, a, c) -> bool:
[Link]({'destino': d, 'asunto': a})
return True
mock_email = EmailMock()
servicio_test = ServicioRegistro(RepositorioMemoria(), mock_email)
servicio_test.registrar('Test User', 'test@[Link]')
print(mock_email.enviados) # [{'destino': 'test@[Link]', ...}]
La inyección de dependencias no requiere ningún framework. Es simplemente pasar las
dependencias como argumentos al constructor en lugar de crearlas internamente.
Ventaja clave para testing: al inyectar dependencias, puedes reemplazarlas con mocks
(objetos falsos) que simulan el comportamiento sin usar servicios reales (base de datos, email,
etc.).
SECCIÓN 3 — Patrones de Diseño en Python
Los patrones de diseño son soluciones reutilizables a problemas comunes de diseño de software. Se
dividen en tres categorías principales.
Categoría Propósito Ejemplos
Creacionales Cómo se crean los objetos Singleton, Factory, Builder,
Prototype
Estructurales Cómo se componen los Decorator, Adapter, Facade,
objetos Proxy
De comportamiento Cómo se comunican los Observer, Strategy,
objetos Command, Iterator
3.1 Singleton — Creacional
Garantiza que una clase tenga una sola instancia y proporciona un punto de acceso global a ella. Útil
para configuraciones, conexiones de base de datos, loggers.
Implementación con __new__
patron_singleton.py
class Configuracion:
_instancia = None # almacena la única instancia
def __new__(cls):
# Si no existe instancia, crearla; si existe, retornarla
if cls._instancia is None:
cls._instancia = super().__new__(cls)
cls._instancia._inicializado = False
return cls._instancia
def __init__(self):
if self._inicializado: # evitar re-inicializar
return
[Link] = False
self.db_url = 'postgresql://localhost/mydb'
self._inicializado = True
# Comprobación: ambas variables apuntan al mismo objeto
c1 = Configuracion()
c2 = Configuracion()
print(c1 is c2) # True — misma instancia
[Link] = True
print([Link]) # True — el cambio se refleja
print(id(c1) == id(c2)) # True — misma dirección en memoria
Alternativa más pythónica: usar un módulo Python. Los módulos son singletons por naturaleza
— solo se importan una vez.
3.2 Factory Method — Creacional
Define una interfaz para crear objetos, pero deja que las subclases decidan qué clase instanciar.
Desacopla la creación del objeto de su uso.
Implementación
patron_factory.py
from abc import ABC, abstractmethod
# Producto abstracto
class Notificacion(ABC):
@abstractmethod
def enviar(self, mensaje: str) -> None: ...
# Productos concretos
class NotificacionEmail(Notificacion):
def __init__(self, email: str):
[Link] = email
def enviar(self, mensaje: str) -> None:
print(f'Email a {[Link]}: {mensaje}')
class NotificacionSMS(Notificacion):
def __init__(self, telefono: str):
[Link] = telefono
def enviar(self, mensaje: str) -> None:
print(f'SMS a {[Link]}: {mensaje}')
class NotificacionPush(Notificacion):
def __init__(self, device_id: str):
self.device_id = device_id
def enviar(self, mensaje: str) -> None:
print(f'Push a {self.device_id}: {mensaje}')
# Factory: encapsula la lógica de creación
class FabricaNotificaciones:
_tipos = {
'email': NotificacionEmail,
'sms': NotificacionSMS,
'push': NotificacionPush,
}
@classmethod
def crear(cls, tipo: str, destino: str) -> Notificacion:
if tipo not in cls._tipos:
raise ValueError(f'Tipo desconocido: {tipo}')
return cls._tipos[tipo](destino) # instancia la clase correcta
# El código cliente no sabe qué clase concreta usa
n1 = [Link]('email', 'user@[Link]')
n2 = [Link]('sms', '+573001234567')
[Link]('Tu pedido fue confirmado')
[Link]('Tu pedido fue confirmado')
El Factory Method resuelve el mismo problema que el OCP: si necesitas agregar un nuevo tipo
de notificación, solo agregas la clase y la registras en el diccionario _tipos, sin modificar el código
existente.
3.3 Decorator — Estructural
Agrega comportamiento a un objeto dinámicamente, sin modificar su clase. En Python existe la sintaxis
@decorador que facilita su uso.
Decorador de funciones (sintaxis Python)
patron_decorador_funciones.py
import time
import functools
# Decorador que mide el tiempo de ejecución
def medir_tiempo(func):
@[Link](func) # preserva nombre y docstring
def wrapper(*args, **kwargs):
inicio = [Link]()
resultado = func(*args, **kwargs) # llama la función original
fin = [Link]()
print(f'{func.__name__} tardó {fin - inicio:.4f}s')
return resultado
return wrapper
# Decorador de caché simple
def cache(func):
_cache = {}
@[Link](func)
def wrapper(*args):
if args not in _cache:
_cache[args] = func(*args)
return _cache[args]
return wrapper
@medir_tiempo # equivale a: calcular_fib = medir_tiempo(calcular_fib)
@cache # se aplican de abajo hacia arriba: cache primero
def calcular_fib(n: int) -> int:
if n <= 1: return n
return calcular_fib(n - 1) + calcular_fib(n - 2)
print(calcular_fib(35)) # rápido gracias al cache
Decorador de clases (patrón GoF)
patron_decorador_clases.py
from abc import ABC, abstractmethod
class Cafe(ABC):
@abstractmethod
def costo(self) -> float: ...
@abstractmethod
def descripcion(self) -> str: ...
class CafeSimple(Cafe):
def costo(self) -> float: return 1.0
def descripcion(self) -> str: return 'Cafe'
# Decorador base: envuelve un Cafe y delega
class DecoradorCafe(Cafe):
def __init__(self, cafe: Cafe):
self._cafe = cafe # referencia al objeto decorado
def costo(self) -> float:
return self._cafe.costo() # delega al envuelto
def descripcion(self) -> str:
return self._cafe.descripcion()
# Decoradores concretos: agregan comportamiento
class ConLeche(DecoradorCafe):
def costo(self) -> float:
return self._cafe.costo() + 0.5 # base + extra
def descripcion(self) -> str:
return self._cafe.descripcion() + ' + Leche'
class ConVainilla(DecoradorCafe):
def costo(self) -> float: return self._cafe.costo() + 0.75
def descripcion(self) -> str:
return self._cafe.descripcion() + ' + Vainilla'
# Encadenamiento dinámico de decoradores
pedido = ConVainilla(ConLeche(CafeSimple()))
print([Link]()) # Cafe + Leche + Vainilla
print([Link]()) # 2.25
3.4 Observer — Comportamiento
Define una dependencia uno-a-muchos entre objetos: cuando uno cambia de estado, todos sus
dependientes son notificados automáticamente. Implementa el patrón publicador/suscriptor.
Implementación
patron_observer.py
from abc import ABC, abstractmethod
# Interfaz del observador
class Observador(ABC):
@abstractmethod
def actualizar(self, evento: str, datos: dict) -> None: ...
# Sujeto: el objeto que notifica
class TiendaOnline:
def __init__(self):
self._observadores: list[Observador] = []
self._stock: dict[str, int] = {}
def suscribir(self, obs: Observador) -> None:
self._observadores.append(obs)
def desuscribir(self, obs: Observador) -> None:
self._observadores.remove(obs)
def _notificar(self, evento: str, datos: dict) -> None:
for obs in self._observadores:
[Link](evento, datos) # notifica a todos
def actualizar_stock(self, producto: str, cantidad: int) -> None:
self._stock[producto] = cantidad
if cantidad == 0:
self._notificar('sin_stock', {'producto': producto})
elif cantidad < 5:
self._notificar('stock_bajo', {'producto': producto, 'cantidad':
cantidad})
# Observadores concretos: reaccionan al evento
class NotificadorEmail(Observador):
def actualizar(self, evento: str, datos: dict) -> None:
print(f'[Email] Evento {evento}: {datos}')
class SistemaReposicion(Observador):
def actualizar(self, evento: str, datos: dict) -> None:
if evento == 'stock_bajo':
print(f'[Reposicion] Solicitar mas {datos["producto"]}')
tienda = TiendaOnline()
[Link](NotificadorEmail())
[Link](SistemaReposicion())
tienda.actualizar_stock('Laptop', 3) # notifica a ambos
3.5 Strategy — Comportamiento
Define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Permite cambiar el
algoritmo de un objeto en tiempo de ejecución.
Implementación — Ordenamiento
patron_strategy.py
from abc import ABC, abstractmethod
# Interfaz de la estrategia
class EstrategiaOrdenamiento(ABC):
@abstractmethod
def ordenar(self, datos: list) -> list: ...
# Estrategias concretas: distintos algoritmos
class OrdenarAscendente(EstrategiaOrdenamiento):
def ordenar(self, datos: list) -> list:
return sorted(datos)
class OrdenarDescendente(EstrategiaOrdenamiento):
def ordenar(self, datos: list) -> list:
return sorted(datos, reverse=True)
class OrdenarPorLongitud(EstrategiaOrdenamiento):
def ordenar(self, datos: list) -> list:
return sorted(datos, key=len)
# Contexto: usa la estrategia sin conocer sus detalles
class Procesador:
def __init__(self, estrategia: EstrategiaOrdenamiento):
self._estrategia = estrategia
def cambiar_estrategia(self, nueva: EstrategiaOrdenamiento) -> None:
self._estrategia = nueva # cambio en tiempo de ejecución
def procesar(self, datos: list) -> list:
return self._estrategia.ordenar(datos)
p = Procesador(OrdenarAscendente())
print([Link]([3, 1, 4, 1, 5])) # [1, 1, 3, 4, 5]
p.cambiar_estrategia(OrdenarDescendente())
print([Link]([3, 1, 4, 1, 5])) # [5, 4, 3, 1, 1]
# En Python también se puede pasar una función directamente
p.cambiar_estrategia(OrdenarPorLongitud())
print([Link](['banana', 'kiwi', 'fresa'])) # por longitud
Strategy vs if/elif: si tienes un if/elif largo que elige entre distintos algoritmos, es candidato a
refactorizarse con Strategy. Cada rama se convierte en una clase concreta.
3.6 Builder — Creacional
Separa la construcción de un objeto complejo de su representación, permitiendo que el mismo proceso
de construcción genere distintas representaciones. Útil para objetos con muchos parámetros
opcionales.
Implementación
patron_builder.py
from dataclasses import dataclass, field
@dataclass
class Pizza:
tamanio: str
masa: str
salsa: str
ingredientes: list[str] = field(default_factory=list)
extra_queso: bool = False
def __str__(self):
ings = ', '.join([Link])
return (f'Pizza {[Link]} | Masa: {[Link]} | '
f'Salsa: {[Link]} | Toppings: {ings}')
# Builder: construye la Pizza paso a paso
class PizzaBuilder:
def __init__(self):
self._tamanio = 'mediana'
self._masa = 'clasica'
self._salsa = 'tomate'
self._ingredientes = []
self._extra_queso = False
def tamanio(self, t: str) -> 'PizzaBuilder':
self._tamanio = t
return self # retorna self para encadenamiento fluido
def masa(self, m: str) -> 'PizzaBuilder':
self._masa = m
return self
def salsa(self, s: str) -> 'PizzaBuilder':
self._salsa = s
return self
def agregar_ingrediente(self, i: str) -> 'PizzaBuilder':
self._ingredientes.append(i)
return self
def extra_queso(self) -> 'PizzaBuilder':
self._extra_queso = True
return self
def construir(self) -> Pizza: # crea el objeto final
return Pizza(self._tamanio, self._masa, self._salsa,
self._ingredientes[:], self._extra_queso)
# API fluida: cada método retorna self
pizza = (PizzaBuilder()
.tamanio('grande')
.masa('delgada')
.salsa('barbacoa')
.agregar_ingrediente('pollo')
.agregar_ingrediente('cebolla')
.extra_queso()
.construir())
print(pizza)
3.7 Adapter — Estructural
Convierte la interfaz de una clase en otra interfaz que los clientes esperan. Permite que clases
incompatibles trabajen juntas.
Implementación
patron_adapter.py
# Clase existente con interfaz incompatible
class SistemaViejoTemperatura:
def obtener_temp_fahrenheit(self) -> float:
return 98.6 # devuelve en Fahrenheit
# Interfaz que espera el sistema nuevo
class SensorTemperatura:
def obtener_celsius(self) -> float: ...
# Adapter: traduce entre las dos interfaces
class AdaptadorTemperatura(SensorTemperatura):
def __init__(self, sistema_viejo: SistemaViejoTemperatura):
self._viejo = sistema_viejo
def obtener_celsius(self) -> float:
fahrenheit = self._viejo.obtener_temp_fahrenheit()
return (fahrenheit - 32) * 5 / 9 # conversión
# El sistema nuevo solo conoce SensorTemperatura
def mostrar_temperatura(sensor: SensorTemperatura) -> None:
print(f'Temperatura: {sensor.obtener_celsius():.1f}°C')
viejo = SistemaViejoTemperatura()
adaptado = AdaptadorTemperatura(viejo) # envuelve el viejo
mostrar_temperatura(adaptado) # 37.0°C
El Adapter es útil cuando trabajas con librerías de terceros cuya interfaz no coincide con la que
espera tu sistema. Envuelves la librería en un adapter en lugar de modificarla.
SECCIÓN 4 — Resumen y Puntos Clave para el Parcial
4.1 Preguntas frecuentes de parcial
Sobre los pilares de POO
• ¿Por qué no se puede instanciar una clase abstracta? → Porque tiene métodos sin
implementación. Python lanza TypeError al intentarlo.
• ¿Diferencia entre _x y __x? → _x es solo convención; __x activa name mangling (Python lo
renombra a _Clase__x).
• ¿Qué hace super()? → Llama al método del padre inmediato según el MRO. Esencial para no
duplicar código en herencia.
• ¿Qué es el MRO? → Method Resolution Order. El orden en que Python busca métodos en
herencia múltiple. Ver con Clase.__mro__
• ¿Diferencia entre polimorfismo por herencia y duck typing? → Herencia requiere clase padre
común; duck typing no requiere relación, solo el mismo método.
Sobre SOLID
• SRP: ¿cuándo violo SRP? → Cuando una clase tiene más de una razón para cambiar (guarda
datos Y envía emails).
• OCP: ¿cómo agrego funcionalidad sin modificar? → Creando nuevas clases que implementan
la abstracción existente.
• LSP: ¿cuándo viola LSP una subclase? → Cuando lanza NotImplementedError, cuando usa
isinstance() para comportarse diferente, o cuando tiene precondiciones más estrictas.
• ISP: ¿señal de violación? → Una clase implementa una interfaz y deja métodos vacíos (pass) o
lanza NotImplementedError.
• DIP: ¿diferencia entre inyección de dependencias y dependencia directa? → En DI las
dependencias vienen del exterior; en directa la clase las crea con MySQLConexion() etc.
Sobre Patrones de Diseño
• Singleton: garantiza una única instancia. Se implementa con __new__. Los módulos Python
son singletons naturales.
• Factory: desacopla la creación del uso. El cliente no sabe qué clase concreta recibe.
• Decorator: agrega comportamiento sin modificar la clase. @[Link] preserva
metadatos de la función original.
• Observer: notificación automática de cambios. Implementa el patrón publicador/suscriptor.
• Strategy: familia de algoritmos intercambiables. Similar a OCP — cada algoritmo es una clase.
• Builder: construcción paso a paso de objetos complejos. API fluida gracias a return self.
• Adapter: compatibilidad entre interfaces incompatibles. Útil con librerías de terceros.
4.2 Tabla comparativa — Cuándo usar cada patrón SOLID
Principio Señal de violación Solución
SRP Clase con múltiples razones Separar en clases con
de cambio responsabilidades únicas
OCP Función con if/elif para cada Jerarquía de clases, cada tipo
tipo = clase
LSP Subclase sobreescribe con Rediseñar jerarquía, usar
raise NotImplementedError abstracción común
ISP Clase con métodos vacíos Dividir en interfaces pequeñas
(pass)
DIP Clase crea sus dependencias Inyectar dependencias como
con ConcreteClass() parámetros
4.3 Tabla comparativa — Patrones de diseño
Patrón Tipo Problema que Casos de uso típicos
resuelve
Singleton Creacional Una sola instancia Config, Logger, Pool
de conexiones
Factory Creacional Crear sin saber qué Notificaciones,
clase concreta parsers, exportadores
Builder Creacional Objeto con muchos Constructores de
parámetros queries, pizzas,
opcionales reportes
Decorator Estructural Agregar Logging, caché,
comportamiento autenticación
dinámicamente
Adapter Estructural Incompatibilidad de Integración con APIs
interfaces legacy o librerías
Observer Comportamiento Notificación Eventos, UI, sistemas
automática de de alertas
cambios
Strategy Comportamiento Algoritmos Ordenamiento,
intercambiables descuentos,
validaciones
4.4 Relaciones entre conceptos
Entender cómo se conectan estos conceptos es clave para el parcial:
• Abstracción + Polimorfismo → base de OCP y del patrón Strategy
• Encapsulamiento → base del patrón Singleton y de LSP (el contrato del objeto no se rompe)
• Herencia + Polimorfismo → permiten LSP y el patrón Decorator
• Abstracción + DIP → permiten el patrón Factory y el testing con mocks
• ISP + SRP → interfaces y clases pequeñas, de responsabilidad única
Error común: confundir el patrón Decorator de Python (@decorador) con el patrón GoF
Decorator. Son conceptos relacionados pero distintos. En el parcial especifica cuál es cuál.
Recuerda: los principios SOLID no son reglas absolutas, son guías. A veces es válido ignorar
uno en favor de simplicidad, especialmente en proyectos pequeños.