U2 - Programación Orientada a Objetos
1 Problemas con el descuento
Para inicializar el precio llamamos a super().__init__(precio) desde la subclase. Para calcular el precio final reutilizamos super().precio_final() y aplicamos el descuento al resultado. Así no repetimos la lógica del impuesto.
Aplicar primero el descuento también da 100 * 0.8 * 1.21 = 96.8, porque multiplicamos por los mismos factores.
class Articulo:
def __init__(self, precio):
self.precio = precio
def precio_final(self):
return self.precio * 1.21
class ArticuloEnOferta(Articulo):
def __init__(self, precio, descuento):
super().__init__(precio)
self.descuento = descuento
def precio_final(self):
return super().precio_final() * (1 - self.descuento)
# Precio final debe ser igual a 100 * 1.21 * 0.8 = 96.8.
# Por precisión numérica de la computadora, necesitamos usar un margen de error.
eps = 0.0000001
assert 96.8 - eps < ArticuloEnOferta(100, 0.20).precio_final() < 96.8 + eps
print(round(ArticuloEnOferta(100, 0.20).precio_final(), 2))96.8
2 Reportes
Para agregar otro tipo de reporte implementamos exportar en una nueva subclase. Como guardar_todos llama al mismo método para cada reporte, no necesitamos modificar la función.
Si agregamos un str a la lista, aparece un AttributeError en reporte.exportar(), porque las cadenas no tienen ese método. La clase abstracta impide crear subclases que no implementen exportar, pero no controla qué objetos ponemos en la lista. Las anotaciones de tipo tampoco verifican los datos durante la ejecución. Para detectar el problema antes de exportar podemos recorrer la lista y verificar cada elemento con isinstance(reporte, Reporte).
from abc import ABC, abstractmethod
class Reporte(ABC):
@abstractmethod
def exportar(self):
raise NotImplementedError
class ReporteTexto(Reporte):
def exportar(self):
return "texto plano"
class ReporteCSV(Reporte):
def exportar(self):
return "a,b,c"
class ReporteJSON(Reporte):
def exportar(self):
return '{"datos": []}'
def guardar_todos(reportes):
exportados = []
for reporte in reportes:
exportados.append(reporte.exportar())
return exportados
reportes = [ReporteTexto(), ReporteCSV(), ReporteJSON()]
print(guardar_todos(reportes))['texto plano', 'a,b,c', '{"datos": []}']
3 Notificaciones
Para que todos los notificadores puedan usarse de la misma manera definimos el método enviar(mensaje) en la clase base. Cada subclase implementa su propia forma de enviar el mensaje. Así podemos agregar nuevos canales sin modificar notificar_a_todos.
Si un envío puede fallar, podemos capturar la excepción dentro del for para continuar con los demás. Si queremos detener los envíos ante un error, dejamos que la excepción se propague.
from abc import ABC, abstractmethod
class Notificador(ABC):
@abstractmethod
def enviar(self, mensaje):
raise NotImplementedError
class Email(Notificador):
def enviar(self, mensaje):
print(f"Email: {mensaje}")
class Consola(Notificador):
def enviar(self, mensaje):
print(mensaje)
def notificar_a_todos(notificadores, mensaje):
for notificador in notificadores:
notificador.enviar(mensaje)
notificar_a_todos([Email(), Consola()], "Hola")Email: Hola
Hola
4 Coordenadas
class Coordenada:
def __init__(self, x, y):
self.x = x
self.y = y
origen_a = Coordenada(0, 0)
origen_b = Coordenada(0, 0)
print(origen_a == origen_b)
print(origen_a is origen_b)
print(origen_a.__dict__ == origen_b.__dict__)False
False
True
Como no implementamos __eq__, == compara la identidad de los objetos. origen_a y origen_b son objetos distintos, por lo que tanto == como is devuelven False. En cambio, sus diccionarios de atributos tienen los mismos valores y la comparación devuelve True.
Para que dos coordenadas con los mismos valores sean iguales implementamos __eq__:
class Coordenada:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
if not isinstance(other, Coordenada):
return NotImplemented
return self.x == other.x and self.y == other.y
print(Coordenada(0, 0) == Coordenada(0, 0))
print(Coordenada(0, 0).__eq__((0, 0)))
print(Coordenada(0, 0) == (0, 0))True
NotImplemented
False
Verificamos el tipo con isinstance para comparar los atributos solamente cuando el otro objeto también es una Coordenada. Si es de otro tipo, devolvemos NotImplemented para indicar que el método no define esa comparación.
La llamada Coordenada(0, 0).__eq__((0, 0)) devuelve NotImplemented. En cambio, la expresión Coordenada(0, 0) == (0, 0) devuelve False: el operador == intenta resolver la comparación y no encuentra una igualdad definida entre esos objetos.
5 Terrenos
class Terreno:
def __init__(self, frente, fondo):
self.frente = frente
self.fondo = fondo
def area(self):
return self.frente * self.fondo
def __lt__(self, other):
if not isinstance(other, Terreno):
return NotImplemented
return self.area() < other.area()terreno_a = Terreno(2, 6)
terreno_b = Terreno(3, 4)
terreno_c = Terreno(5, 5)
terrenos = [terreno_c, terreno_a, terreno_b]
areas = []
for terreno in sorted(terrenos):
areas.append(terreno.area())
print(areas)[12, 12, 25]
print(terreno_a < terreno_b)False
print(terreno_b < terreno_a)False
print(terreno_a.area() == terreno_b.area())True
print(terreno_a == terreno_b)False
Para ordenar los terrenos con sorted implementamos __lt__, que compara sus áreas. Los terrenos de 2 × 6 y 3 × 4 tienen la misma área, por lo que ninguno es menor que el otro. En ese caso, sorted conserva el orden que tenían en la lista.
Como no implementamos __eq__, == compara la identidad de los objetos y devuelve False para estos dos terrenos. No es necesario que tener la misma área implique ser iguales: también podrían importar las dimensiones o la ubicación. En otro dominio donde solo interese la superficie, podríamos implementar __eq__ comparando las áreas.
6 Compras
Para que len(carrito) devuelva la cantidad de productos en el carrito implementamos __len__. En este caso, también cuenta los duplicados.
Para utilizar el operador in se implementa el método __contains__, que devuelve un booleano. La búsqueda se hace por nombre para que funcione la expresión "yerba" in carrito. Se podría implementar búsqueda por Producto pero escribir Producto("yerba") in carrito puede resultar más incómodo para el usuario.
Finalmente, para obtener una descripción legible al usar print() podemos implementar el método __str__.
class Producto:
def __init__(self, nombre):
self.nombre = nombre
class Carrito:
def __init__(self, productos):
# Me aseguro que la secuencia es una lista internamente
self.productos = list(productos)
def __len__(self):
return len(self.productos)
def __contains__(self, nombre):
for producto in self.productos:
if producto.nombre == nombre:
return True
return False
def __str__(self):
nombres = []
for producto in self.productos:
nombres.append(producto.nombre)
return "Carrito con: " + ", ".join(nombres)carrito = Carrito([Producto("yerba"), Producto("azúcar")])
print(len(carrito))2
print("yerba" in carrito)True
print(carrito)Carrito con: yerba, azúcar
7 Tiempo
Para sumar dos duraciones implementamos __add__, que devuelve una nueva Duracion con la suma de los minutos. Para mostrarla con print() usamos __str__, y para obtener una representación como Duracion(60) usamos __repr__.
En este caso decidimos permitir solamente la suma entre duraciones. Si el otro objeto es un entero, __add__ devuelve NotImplemented y la expresión Duracion(15) + 10 produce un TypeError.
class Duracion:
def __init__(self, minutos):
self.minutos = minutos
def __add__(self, other):
if not isinstance(other, Duracion):
return NotImplemented
return Duracion(self.minutos + other.minutos)
def __repr__(self):
return f"Duracion({self.minutos})"
def __str__(self):
return f"{self.minutos // 60} h {self.minutos % 60} min"
total = Duracion(15) + Duracion(45)
print(total)
print(repr(total))1 h 0 min
Duracion(60)
8 Contador
class Contador:
def __init__(self, valor_inicial=0):
self._inicial = valor_inicial
self._valor = valor_inicial
def incrementar(self, paso=1):
self._valor += paso
return self._valor
def decrementar(self, paso=1):
self._valor -= paso
return self._valor
def valor(self):
return self._valor
def reiniciar(self):
self._valor = self._inicial
return self._valor
def __str__(self):
return f"Contador(valor={self._valor})"Creamos un contador en cero y cambiamos su valor:
c = Contador()
print(c.valor())
print(c.incrementar(5))
print(c.decrementar(2))
print(c.reiniciar())0
5
3
0
Si no indicamos el paso, incrementamos o decrementamos de a uno:
print(c.incrementar())
print(c.incrementar())
print(c.decrementar())
print(c)1
2
1
Contador(valor=1)
También podemos empezar en otro valor. Al reiniciar volvemos a ese valor inicial:
contador_iniciado_en_10 = Contador(10)
print(contador_iniciado_en_10.valor())
print(contador_iniciado_en_10.incrementar(3))
print(contador_iniciado_en_10.reiniciar())10
13
10
Guardamos el valor inicial en _inicial para poder recuperarlo al llamar a reiniciar, aunque sea distinto de cero.
Los valores por defecto permiten crear un contador en cero e incrementar o decrementar de a uno cuando no indicamos otra cantidad.
9 Magia para programadores
import math
class Pocion:
def __init__(self, color, volumen):
self.color = color
self.volumen = volumen
def mezclar(self, other):
volumen_total = self.volumen + other.volumen
w1 = self.volumen / volumen_total
w2 = other.volumen / volumen_total
color = []
for i in range(3):
componente = math.ceil(self.color[i] * w1 + other.color[i] * w2)
color.append(componente)
return Pocion(color=color, volumen=volumen_total)
felix_felicis = Pocion([255, 255, 255], 7)
pocion_multijugos = Pocion([51, 102, 51], 12)
nueva_pocion = felix_felicis.mezclar(pocion_multijugos)
assert nueva_pocion.color == [127, 159, 127]
assert nueva_pocion.volumen == 19
print(nueva_pocion.color)
print(nueva_pocion.volumen)[127, 159, 127]
19
Para obtener el volumen de la mezcla sumamos los volúmenes de las dos pociones. Para el color calculamos un promedio de cada componente RGB, dando más peso a la poción que tiene mayor volumen. Usamos math.ceil para redondear hacia arriba y devolvemos una nueva Pocion con el resultado.
10 Mensaje secreto
class Cifrado:
def __init__(self, original, alternativo):
if len(original) != len(alternativo):
raise ValueError(
f"len(original) != len(alternativo) ({len(original)} vs {len(alternativo)})"
)
self.original = tuple(original)
self.alternativo = tuple(alternativo)
def codificar(self, texto):
caracteres_codificados = []
for caracter in texto:
indice = self.original.index(caracter)
caracteres_codificados.append(self.alternativo[indice])
return "".join(caracteres_codificados)
def decodificar(self, texto):
caracteres_decodificados = []
for caracter in texto:
indice = self.alternativo.index(caracter)
caracteres_decodificados.append(self.original[indice])
return "".join(caracteres_decodificados)Creamos el cifrado y codificamos algunos textos:
alfabeto = "abcdefghijklmnopqrstuvwxyz"
alfabeto_mezclado = "etaoinshrdlucmfwypvbgkjqxz"
mi_cifrado = Cifrado(alfabeto, alfabeto_mezclado)
print(mi_cifrado.codificar("abc"))
print(mi_cifrado.codificar("xyz"))
print(mi_cifrado.codificar("aeiou"))eta
qxz
eirfg
Para recuperar los textos originales usamos decodificar:
print(mi_cifrado.decodificar("eta"))
print(mi_cifrado.decodificar("qxz"))
print(mi_cifrado.decodificar("eirfg"))abc
xyz
aeiou
Para codificar buscamos la posición de cada carácter en original y tomamos el carácter de esa misma posición en alternativo. Para decodificar hacemos lo mismo en sentido inverso. Finalmente, usamos "".join(...) para unir los caracteres en una cadena.
En __init__ verificamos que los alfabetos tengan la misma longitud. Si no la tienen, levantamos un ValueError con un mensaje que indica el problema.
11 Real envido
class ManoDeTruco:
def __init__(self, cartas):
self.cartas = cartas
def comparar_con(self, other):
if self.puntos() >= other.puntos():
return self
return other
def puntos(self):
n_altas = 0
for carta in self.cartas:
if carta >= 10:
n_altas += 1
if n_altas == 3:
return 20
elif n_altas == 2:
return 20 + min(self.cartas)
elif n_altas == 1:
return 20 + sum(sorted(self.cartas)[:2])
return 20 + sum(sorted(self.cartas)[-2:])mano1 = ManoDeTruco([7, 5, 6])
mano2 = ManoDeTruco([4, 11, 2])
print(mano1.puntos())33
print(mano2.puntos())26
print(mano1.comparar_con(mano2) is mano1)True
Para calcular los puntos contamos las cartas de valor 10 o más, que no suman puntos individuales. A los 20 puntos por tener cartas del mismo palo les sumamos los dos valores más altos que correspondan.
En comparar_con usamos >= para devolver la mano que llamó al método cuando hay empate.
12 La muestra infinita
class Muestra:
def __init__(self, iterable):
self._datos = list(iterable)
def agregar(self, x):
self._datos.append(x)
def n(self):
return len(self._datos)
def suma(self):
return sum(self._datos)
def media(self):
n = self.n()
return self.suma() / n
def varianza(self, muestral=False):
"""Calcula la varianza poblacional o muestral."""
n = self.n()
mu = self.media()
ssd = 0
for x in self._datos:
ssd += (x - mu) ** 2
if muestral:
denom = n - 1
else:
denom = n
return ssd / denom
@property
def valores(self):
"""Devuelve los datos como una tupla."""
return tuple(self._datos)m = Muestra([10, 12, 13, 15])
m.agregar(20)
print(m.n())5
print(m.suma())70
print(m.media())14.0
print(m.varianza())11.6
print(m.varianza(muestral=True))14.5
print(m.valores)(10, 12, 13, 15, 20)
Guardamos los datos en una lista para poder agregar valores con append. Para calcular la varianza dividimos la suma de las desviaciones al cuadrado por n o por n - 1, según sea poblacional o muestral.
Para consultar los datos sin modificar la lista mediante valores, usamos una propiedad sin setter que devuelve una tupla. El prefijo _ indica que _datos es de uso interno, aunque Python no impide acceder al atributo.
13 ¡Orden en el laboratorio!
class Experimento:
total_creados = 0
def __init__(self, nombre, responsable=None):
Experimento.total_creados += 1
self.id = Experimento.total_creados
self.nombre = nombre
self.responsable = responsable
@classmethod
def desde_dict(cls, datos):
return cls(datos.get("nombre"), responsable=datos.get("responsable"))
def __repr__(self):
import json
nombre = json.dumps(self.nombre, ensure_ascii=False)
responsable = ("None" if self.responsable is None else
json.dumps(self.responsable, ensure_ascii=False))
return (f"Experimento(id={self.id}, nombre={nombre}, "
f"responsable={responsable})")e1 = Experimento("Piloto A", responsable="Dolores")
e2 = Experimento("Piloto Z")
e3 = Experimento.desde_dict({"nombre": "Piloto B", "responsable": "Ana"})
print(Experimento.total_creados)3
print(repr(e1))Experimento(id=1, nombre="Piloto A", responsable="Dolores")
print(repr(e3))Experimento(id=3, nombre="Piloto B", responsable="Ana")
Para asignar un identificador distinto a cada experimento usamos el atributo de clase total_creados. Como lo comparten todas las instancias, lo incrementamos cada vez que creamos una nueva y guardamos ese valor en id.
Para crear un experimento desde un diccionario implementamos desde_dict con @classmethod. Usamos cls(...) para construir la instancia y get para que el responsable sea None si no se indica. Finalmente, __repr__ devuelve la representación con los datos del experimento.
14 Sensores descalibrados
import random
class Sensor:
def __init__(self, nombre):
self.nombre = nombre
self._offset = 0.0
def calibrar(self, offset):
self._offset = float(offset)
def leer(self):
raise NotImplementedError("Esta clase no implementa 'leer'. Use una subclase.")
class SensorTemperatura(Sensor):
def leer(self):
base = random.uniform(18, 28)
return base + self._offset
class SensorHumedad(Sensor):
def leer(self):
base = random.uniform(30, 70)
return base + self._offset
def promedio_lecturas(sensores, n=5):
"""Devuelve el promedio de n lecturas para cada sensor."""
if not isinstance(n, int):
raise TypeError("n debe ser un entero")
if n <= 0:
raise ValueError("n debe ser positivo")
resultados = {}
for sensor in sensores:
acumulado = 0
for _ in range(n):
acumulado += sensor.leer()
resultados[sensor.nombre] = acumulado / n
return resultados
random.seed(0)
t = SensorTemperatura("T1")
h = SensorHumedad("H1")
t.calibrar(0.5)
promedios = promedio_lecturas([t, h], n=3)
print(promedios){'T1': 25.24315945098732, 'H1': 45.66834145482648}
Guardamos el ajuste en _offset para sumarlo a cada lectura. Cada subclase implementa leer con el rango que corresponde a su sensor. El método de la clase base levanta NotImplementedError para indicar que las subclases deben implementarlo.
En promedio_lecturas llamamos a leer la cantidad de veces indicada y guardamos cada promedio en un diccionario. Como ambos sensores tienen ese método, no necesitamos distinguirlos por tipo. Verificamos que n sea un entero positivo para poder calcular el promedio.
15 Python para matemáticos
def mcd(a, b):
while b != 0:
a, b = b, a % b
return abs(a)
class Fraccion:
def __init__(self, numerador, denominador):
if denominador == 0:
raise ValueError("El denominador no puede ser cero")
self.numerador = numerador
self.denominador = denominador
self._simplificar()
def _simplificar(self):
divisor = mcd(self.numerador, self.denominador)
self.numerador = self.numerador // divisor
self.denominador = self.denominador // divisor
if self.denominador < 0:
self.numerador *= -1
self.denominador *= -1
def __str__(self):
return f"{self.numerador}/{self.denominador}"
def __repr__(self):
return f"Fraccion({self.numerador}, {self.denominador})"
def __add__(self, other):
if not isinstance(other, Fraccion):
return NotImplemented
num = self.numerador * other.denominador + other.numerador * self.denominador
den = self.denominador * other.denominador
return Fraccion(num, den)
def __sub__(self, other):
if not isinstance(other, Fraccion):
return NotImplemented
num = self.numerador * other.denominador - other.numerador * self.denominador
den = self.denominador * other.denominador
return Fraccion(num, den)
def __mul__(self, other):
if not isinstance(other, Fraccion):
return NotImplemented
num = self.numerador * other.numerador
den = self.denominador * other.denominador
return Fraccion(num, den)
def __truediv__(self, other):
if not isinstance(other, Fraccion):
return NotImplemented
if other.numerador == 0:
raise ZeroDivisionError("No se puede dividir por cero")
num = self.numerador * other.denominador
den = self.denominador * other.numerador
return Fraccion(num, den)f1 = Fraccion(4, 5)
f2 = Fraccion(1, 8)
print(f1 + f2)37/40
print(f1 - f2)27/40
print(f1 * f2)1/10
print(f1 / f2)32/5
print(f1 / Fraccion(2, 1))2/5
print(f1 * Fraccion(5, 1))4/1
Para simplificar la fracción dividimos el numerador y el denominador por su máximo común divisor. Además, dejamos el signo en el numerador para que el denominador sea positivo.
Para usar los operadores +, -, * y / implementamos __add__, __sub__, __mul__ y __truediv__. Cada método devuelve una nueva Fraccion, por lo que el constructor también simplifica el resultado. Verificamos que el denominador no sea cero y que no se intente dividir por una fracción de valor cero.
16 Tiempo al tiempo ⏳
DOM = {1: 31, 2: 28, 3: 31, 4: 30, 5: 31, 6: 30,
7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31}
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@property
def year(self):
return self._year
@year.setter
def year(self, value):
if type(value) is not int:
raise ValueError("year debe ser un entero")
if value < 0:
raise ValueError("year debe ser mayor o igual que cero")
if hasattr(self, "_day") and self._day > self._dias_del_mes(value, self.month):
raise ValueError("day no es válido para el nuevo year")
self._year = value
@property
def month(self):
return self._month
@month.setter
def month(self, value):
if type(value) is not int:
raise ValueError("month debe ser un entero")
if not 1 <= value <= 12:
raise ValueError("month debe estar entre 1 y 12")
if hasattr(self, "_day") and self._day > self._dias_del_mes(self.year, value):
raise ValueError("day no es válido para el nuevo month")
self._month = value
@property
def day(self):
return self._day
@day.setter
def day(self, value):
if type(value) is not int:
raise ValueError("day debe ser un entero")
if not 1 <= value <= self._dias_del_mes(self.year, self.month):
raise ValueError("day no es válido para el mes")
self._day = value
@classmethod
def from_str(cls, value):
if not isinstance(value, str):
raise TypeError("la fecha debe ser texto")
try:
year, month, day = map(int, value.split("-"))
except (ValueError, TypeError) as error:
raise ValueError("use el formato YYYY-MM-DD") from error
return cls(year, month, day)
@staticmethod
def _es_bisiesto(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
@staticmethod
def _dias_del_mes(year, month):
if month == 2 and Date._es_bisiesto(year):
return 29
return DOM[month]
def _clave(self):
return self.year, self.month, self.day
def __eq__(self, other):
if not isinstance(other, Date):
return NotImplemented
return self._clave() == other._clave()
def __ne__(self, other):
result = self.__eq__(other)
return NotImplemented if result is NotImplemented else not result
def __lt__(self, other):
if not isinstance(other, Date):
return NotImplemented
return self._clave() < other._clave()
def __le__(self, other):
if not isinstance(other, Date):
return NotImplemented
return self._clave() <= other._clave()
def __gt__(self, other):
if not isinstance(other, Date):
return NotImplemented
return self._clave() > other._clave()
def __ge__(self, other):
if not isinstance(other, Date):
return NotImplemented
return self._clave() >= other._clave()
def __str__(self):
return f"{self.year:04d}-{self.month:02d}-{self.day:02d}"
def __repr__(self):
return f"Date(year={self.year}, month={self.month}, day={self.day})"d1 = Date(1990, 5, 20)
d2 = Date(1998, 9, 21)
d3 = Date(1998, 9, 20)
d4 = Date(2022, 12, 18)
fecha = Date(1995, 11, 6)
print((fecha.year, fecha.month, fecha.day))(1995, 11, 6)
print(str(fecha))1995-11-06
print(repr(fecha))Date(year=1995, month=11, day=6)
print(Date.from_str("2025-03-08"))2025-03-08
print(d1 < d2)True
print(d1 > d2)False
print(d1 != d3)True
print(d4 >= d3)True
print(Date(2000, 2, 29))2000-02-29
print(sorted([d1, d2, d3, d4]))[Date(year=1990, month=5, day=20), Date(year=1998, month=9, day=20), Date(year=1998, month=9, day=21), Date(year=2022, month=12, day=18)]
Para mostrar la fecha implementamos __str__ con el formato YYYY-MM-DD y __repr__ con los argumentos de la clase. El formato 02d agrega ceros a la izquierda hasta completar dos dígitos; 04d hace lo mismo con cuatro. Para crear una fecha desde texto, from_str separa los valores por -, los convierte a enteros y llama al constructor.
Para comparar fechas usamos las tuplas (year, month, day). Python compara primero el año; si coincide, compara el mes y luego el día. Así también podemos ordenar fechas con sorted.
Guardamos los valores en _year, _month y _day, y usamos propiedades con setters para verificar cada asignación. Si el valor no es válido, levantamos un ValueError antes de modificar el atributo. Para validar el día consultamos los días del mes y verificamos si el año es bisiesto cuando se trata de febrero. También revisamos el día al cambiar el mes o el año, para no dejar una fecha inválida.
17 Buscando el error
nombre = "Ada"
print(nombre)Ada
edades = [19, 21, 23]
ultima_edad = edades[2]
print(ultima_edad)23
total = 1500
print("Total: $" + str(total))Total: $1500
temperaturas = [18.5, 19.2, 20.0]
temperaturas.append(20.4)
print(temperaturas)[18.5, 19.2, 20.0, 20.4]
def saludar(nombre):
return f"¡Hola, {nombre}!"
print(saludar(nombre))¡Hola, Ada!
En cada caso corregimos la causa del error:
NameError: escribimosnombreen lugar denomrbe. El mensaje indica el nombre que no está definido.IndexError: usamos el índice 2, porque la lista tiene tres elementos y los índices empiezan en cero. El mensaje indica que el índice está fuera del rango.TypeError: convertimostotala texto constrpara concatenarlo. El mensaje indica que no podemos concatenar una cadena y un entero.AttributeError: usamos una lista en lugar de una tupla para poder llamar aappend. El mensaje indica el método que la tupla no tiene.SyntaxError: agregamos los dos puntos después dedef saludar(nombre). El mensaje señala dónde se esperaba:.
18 Cada problema con su nombre
class Inventario:
def __init__(self, stock):
if not isinstance(stock, dict):
raise TypeError("stock debe ser un diccionario")
for producto, cantidad in stock.items():
if not isinstance(producto, str):
raise TypeError("el nombre del producto debe ser texto")
if not producto:
raise ValueError("el nombre del producto no puede estar vacío")
if type(cantidad) is not int:
raise TypeError("la cantidad debe ser entera")
if cantidad < 0:
raise ValueError("la cantidad no puede ser negativa")
self.stock = stock.copy()
def hay_stock(self, producto, cantidad):
if not isinstance(producto, str):
raise TypeError("producto debe ser texto")
if not producto:
raise ValueError("producto no puede estar vacío")
if type(cantidad) is not int:
raise TypeError("cantidad debe ser entera")
if cantidad <= 0:
raise ValueError("cantidad debe ser positiva")
if producto not in self.stock:
raise KeyError(f"producto inexistente: {producto}")
return self.stock[producto] >= cantidad
inventario = Inventario({"cuadernos": 12, "lápices": 30, "carpetas": 5})
print(inventario.hay_stock("cuadernos", 8))
print(inventario.hay_stock("carpetas", 9))True
False
Guardamos una copia del diccionario para que los cambios en el original no modifiquen el stock del inventario. En hay_stock verificamos los argumentos antes de comparar la cantidad pedida con la disponible. Si los datos son válidos, devolvemos True o False según alcance el stock.
Usamos TypeError cuando el dato tiene un tipo incorrecto, ValueError cuando el tipo es correcto pero el valor no es válido y KeyError cuando el producto no existe. El stock puede ser cero, pero la cantidad que pedimos debe ser mayor que cero.
19 Un cajero insistente
class CuentaError(Exception):
pass
class MontoInvalidoError(CuentaError):
pass
class SaldoInsuficienteError(CuentaError):
pass
class CuentaBancaria:
def __init__(self, saldo):
self.saldo = saldo
def extraer(self, monto):
if monto <= 0:
raise MontoInvalidoError("el monto debe ser mayor que cero")
if monto > self.saldo:
raise SaldoInsuficienteError("saldo insuficiente")
self.saldo -= monto
return monto
def solicitar_extraccion(cuenta):
while True:
try:
monto = float(input("Monto a extraer: "))
extraido = cuenta.extraer(monto)
except ValueError as error:
print(f"Entrada no numérica: {error}")
except MontoInvalidoError as error:
print(f"Monto inválido: {error}")
except SaldoInsuficienteError as error:
print(f"Saldo insuficiente: {error}")
else:
return extraido
if __name__ == "__main__":
cuenta = CuentaBancaria(100)
solicitar_extraccion(cuenta)
print(f"Saldo restante: {cuenta.saldo}")Monto a extraer: abc
Entrada no numérica: could not convert string to float: 'abc'
Monto a extraer: -5
Monto inválido: el monto debe ser mayor que cero
Monto a extraer: 150
Saldo insuficiente: saldo insuficiente
Monto a extraer: 40
Saldo restante: 60.0
Para distinguir los problemas de la cuenta definimos dos excepciones que heredan de CuentaError. En extraer verificamos el monto antes de descontarlo, así un intento inválido no modifica el saldo.
Usamos while True para seguir pidiendo un monto y un except para cada problema. El else se ejecuta cuando no hubo una excepción y devuelve el monto extraído, terminando la función. Para probarlo, ingresamos un texto no numérico, un monto negativo, uno mayor al saldo y finalmente uno válido.