Ejercicios — Programación II
Programación II combina estructuras (listas, dicts, sets) + paradigmas (OOP, recursión) + I/O. Estos ejercicios mezclan los temas; resolvelos en Python 3.
Cap. 1 — Listas y tuplas
1.1 — Manejo básico (básico)
Dada nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]:
a) Sin usar set, devolvé los elementos únicos preservando el orden de aparición.
b) Devolvé los elementos que aparecen exactamente una vez.
✅ Solución
nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
# a) únicos preservando orden
unicos = []
for x in nums:
if x not in unicos:
unicos.append(x)
# Para n grande, mejor con set auxiliar:
visto = set(); unicos = []
for x in nums:
if x not in visto:
unicos.append(x); visto.add(x)
# b) los que aparecen 1 vez
from collections import Counter
once = [x for x, c in Counter(nums).items() if c == 1]
# → [4, 9, 2, 6]
1.2 — Comprehension (intermedio)
Reescribí estos bucles como list comprehensions:
# a)
result = []
for x in range(20):
if x % 2 == 0:
result.append(x * x)
# b) matriz traspuesta
m = [[1, 2, 3], [4, 5, 6]]
t = []
for i in range(len(m[0])):
fila = []
for f in m:
fila.append(f[i])
t.append(fila)
✅ Solución
# a)
result = [x*x for x in range(20) if x % 2 == 0]
# b)
t = [[f[i] for f in m] for i in range(len(m[0]))]
# Más Pythonico: t = list(zip(*m)) (devuelve tuplas)
zip(*m) se basa en el operador unpack * para descomponer m y volverlo a juntar transpuesto.
Cap. 2 — Diccionarios y conjuntos
2.1 — Conteo de palabras (intermedio)
Dada una cadena, devolvé las 5 palabras más frecuentes (case-insensitive, ignorando puntuación).
✅ Solución
import re
from collections import Counter
def top5(texto):
palabras = re.findall(r'\w+', texto.lower())
return Counter(palabras).most_common(5)
print(top5("El gato y el perro. El gato es feliz, el perro también."))
# [('el', 4), ('gato', 2), ('perro', 2), ('y', 1), ('es', 1)]
Counter.most_common(n) ya está optimizado.
2.2 — Set operations (básico)
Dadas las listas de estudiantes inscritos en MAT115 y PRO115, devolvé:
a) Los que están en ambos. b) Los que están solo en MAT. c) Los que están en alguno (sin duplicados).
✅ Solución
mat = {"Ana", "Bryan", "Carla", "Daniel"}
pro = {"Bryan", "Daniel", "Erika", "Fer"}
ambos = mat & pro # {"Bryan", "Daniel"}
solo_mat = mat - pro # {"Ana", "Carla"}
alguno = mat | pro # los 6
solo_uno = mat ^ pro # XOR: {"Ana", "Carla", "Erika", "Fer"}
Cap. 3 — Recursión
3.1 — Suma de dígitos (básico)
Sin convertir a string, sumá los dígitos de un entero positivo. Recursivo.
✅ Solución
def suma_digitos(n: int) -> int:
if n < 10:
return n
return n % 10 + suma_digitos(n // 10)
# suma_digitos(2024) → 4 + 2 + 0 + 2 = 8
3.2 — Permutaciones (intermedio)
Devolvé todas las permutaciones de una lista. Recursivo.
💡 Pista
Para cada elemento, "fijalo" como primero y permutá el resto.
✅ Solución
def permutaciones(lista):
if len(lista) <= 1:
return [lista]
result = []
for i, x in enumerate(lista):
resto = lista[:i] + lista[i+1:]
for p in permutaciones(resto):
result.append([x] + p)
return result
# permutaciones([1,2,3]) →
# [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
permutaciones, así que la complejidad es factorial. Bonus: itertools.permutations ya lo hace.
3.3 — Torre de Hanoi (avanzado)
Imprimí los movimientos para resolver la Torre de Hanoi con discos.
✅ Solución
def hanoi(n, origen, auxiliar, destino):
if n == 1:
print(f"Mover disco 1 de {origen} a {destino}")
return
hanoi(n-1, origen, destino, auxiliar)
print(f"Mover disco {n} de {origen} a {destino}")
hanoi(n-1, auxiliar, origen, destino)
hanoi(3, 'A', 'B', 'C')
Mínimo de movimientos: . Para 64 discos: ~5.8 mil millones de años a un movimiento por segundo (la leyenda de Brahma).
Cap. 4 — Archivos y excepciones
4.1 — Procesar CSV (básico)
Dado un CSV con columnas nombre, edad, ciclo, calculá la edad promedio por ciclo.
✅ Solución
import csv
from collections import defaultdict
def promedio_por_ciclo(path):
suma = defaultdict(int)
cuenta = defaultdict(int)
with open(path, encoding='utf-8') as f:
reader = csv.DictReader(f)
for fila in reader:
ciclo = fila['ciclo']
suma[ciclo] += int(fila['edad'])
cuenta[ciclo] += 1
return {c: suma[c] / cuenta[c] for c in suma}
Usá with open siempre — garantiza cierre del archivo aunque haya excepción.
4.2 — Manejo robusto de errores (intermedio)
Implementá una función que lea un archivo y devuelva su contenido, manejando estos casos:
- archivo no existe → devolver
None - archivo existe pero está vacío → devolver
"" - error de permisos → re-lanzar
- otros errores → log y
None
✅ Solución
import logging
from pathlib import Path
def leer_robusto(path: Path | str) -> str | None:
try:
with open(path, encoding='utf-8') as f:
return f.read()
except FileNotFoundError:
return None
except PermissionError:
raise # propagar — es un bug del sistema, no del archivo
except Exception as e:
logging.exception(f"Error leyendo {path}")
return None
Trampa común: capturar Exception indiscriminadamente esconde bugs. Capturá lo específico y dejá pasar lo inesperado (o, como acá, lo logueás antes).
Cap. 5 — Clases y POO
5.1 — Clase Persona (básico)
Implementá una clase Persona con atributos nombre, edad, método saludar() que devuelva "Hola, soy <nombre>". Implementá __str__ y __eq__ (dos personas son iguales si nombre y edad coinciden).
✅ Solución
from dataclasses import dataclass
@dataclass
class Persona:
nombre: str
edad: int
def saludar(self) -> str:
return f"Hola, soy {self.nombre}"
# __init__, __repr__, __eq__ ya los da @dataclass
# Sin dataclass:
class PersonaManual:
def __init__(self, nombre: str, edad: int):
self.nombre, self.edad = nombre, edad
def saludar(self):
return f"Hola, soy {self.nombre}"
def __str__(self):
return f"Persona({self.nombre}, {self.edad})"
def __eq__(self, otra):
if not isinstance(otra, PersonaManual): return NotImplemented
return self.nombre == otra.nombre and self.edad == otra.edad
@dataclass te ahorra mucho boilerplate. Para POO en producción, usalo.
5.2 — Herencia (intermedio)
Modelá una jerarquía: Cuenta (base, con saldo, métodos depositar/retirar), CuentaAhorro (interés mensual del 0.5 %), CuentaCorriente (descubierto autorizado de $500).
✅ Solución
class Cuenta:
def __init__(self, titular: str, saldo: float = 0):
self.titular = titular
self.saldo = saldo
def depositar(self, monto: float):
if monto <= 0: raise ValueError("Monto debe ser positivo")
self.saldo += monto
def retirar(self, monto: float):
if monto <= 0: raise ValueError("Monto debe ser positivo")
if monto > self.saldo:
raise ValueError("Saldo insuficiente")
self.saldo -= monto
class CuentaAhorro(Cuenta):
INTERES_MENSUAL = 0.005
def aplicar_interes(self):
self.saldo *= (1 + self.INTERES_MENSUAL)
class CuentaCorriente(Cuenta):
DESCUBIERTO = 500
def retirar(self, monto: float):
if monto > self.saldo + self.DESCUBIERTO:
raise ValueError("Excede descubierto autorizado")
self.saldo -= monto # puede quedar negativo
Notá que CuentaCorriente.retirar sobreescribe el de Cuenta: el descubierto cambia la regla.
5.3 — Composición vs herencia (avanzado)
Convertí la siguiente jerarquía profunda en composición:
class Animal:
def comer(self): pass
class Volador(Animal):
def volar(self): pass
class Nadador(Animal):
def nadar(self): pass
# ¿Y si un animal nada Y vuela? ¿Pato?
💡 Pista
En vez de heredar capacidades, inyectalas como atributos componibles.
✅ Solución
class HabilidadComer:
def comer(self): print("come")
class HabilidadVolar:
def volar(self): print("vuela")
class HabilidadNadar:
def nadar(self): print("nada")
class Animal:
def __init__(self, nombre, habilidades):
self.nombre = nombre
self.habilidades = habilidades
def __getattr__(self, nombre):
for h in self.habilidades:
if hasattr(h, nombre):
return getattr(h, nombre)
raise AttributeError(nombre)
pato = Animal("pato", [HabilidadComer(), HabilidadVolar(), HabilidadNadar()])
pato.volar() # vuela
pato.nadar() # nada
Composición es más flexible que herencia para combinar comportamientos.
Reto integrador
R.1 — Sistema de biblioteca con persistencia
Implementá un mini-sistema de biblioteca:
- Clases:
Libro,Usuario,Prestamo,Biblioteca. Biblioteca.prestar(libro_id, usuario_id)→ registra y baja stock.Biblioteca.devolver(prestamo_id)→ registra devolución y sube stock.- Persistencia: guardar/cargar todo de un
biblioteca.json. - CLI interactiva con menú.
- Manejo de errores: usuario inexistente, libro sin stock, doble devolución, etc.
💡 Pista de arquitectura
@dataclasspara los modelos.Bibliotecacomo agregador con métodosprestar,devolver,save,load.json.dumppara persistir. Convertí dataclasses conasdict.- Excepciones custom:
LibroNoExiste,UsuarioNoExiste,SinStock.
✅ Solución (bosquejo)
import json
from dataclasses import dataclass, asdict
from datetime import date
from pathlib import Path
@dataclass
class Libro:
id: int; titulo: str; autor: str; stock: int = 1
@dataclass
class Usuario:
id: int; nombre: str
@dataclass
class Prestamo:
id: int; libro_id: int; usuario_id: int
fecha_prestamo: str
fecha_devolucion: str | None = None
class Biblioteca:
def __init__(self):
self.libros: dict[int, Libro] = {}
self.usuarios: dict[int, Usuario] = {}
self.prestamos: dict[int, Prestamo] = {}
self._next_id = 1
def prestar(self, libro_id, usuario_id):
if libro_id not in self.libros: raise KeyError("libro")
if usuario_id not in self.usuarios: raise KeyError("usuario")
libro = self.libros[libro_id]
if libro.stock <= 0: raise ValueError("sin stock")
libro.stock -= 1
p = Prestamo(self._next_id, libro_id, usuario_id, str(date.today()))
self.prestamos[p.id] = p
self._next_id += 1
return p.id
def devolver(self, prestamo_id):
p = self.prestamos.get(prestamo_id)
if not p: raise KeyError("prestamo")
if p.fecha_devolucion: raise ValueError("ya devuelto")
p.fecha_devolucion = str(date.today())
self.libros[p.libro_id].stock += 1
def save(self, path):
Path(path).write_text(json.dumps({
'libros': {k: asdict(v) for k, v in self.libros.items()},
'usuarios': {k: asdict(v) for k, v in self.usuarios.items()},
'prestamos': {k: asdict(v) for k, v in self.prestamos.items()},
'next_id': self._next_id,
}, indent=2))
@classmethod
def load(cls, path):
data = json.loads(Path(path).read_text())
b = cls()
b.libros = {int(k): Libro(**v) for k, v in data['libros'].items()}
b.usuarios = {int(k): Usuario(**v) for k, v in data['usuarios'].items()}
b.prestamos = {int(k): Prestamo(**v) for k, v in data['prestamos'].items()}
b._next_id = data['next_id']
return b
Bonus: agregá tests con pytest. Crear biblioteca, prestar, intentar prestar sin stock, devolver, guardar, cargar, verificar igualdad.
Más práctica: implementá los proyectos en GitHub. La práctica > la teoría.