Lanzar excepciones y crear las tuyas
En resumen: Con raise una función se niega y lanza una excepción que cae a través de las llamadas hasta que algún except la atrapa; para tu propio caso escribes tu propia clase de excepción.
Problema
El cajero dice «recoge tu dinero»
La cuenta guarda su saldo. Si el importe a retirar es negativo o no hay dinero suficiente, el método simplemente no hace nada:
def withdraw(self, amount): if amount > 0 and amount <= self.balance: self.balance -= amountEl cajero lo usa así:
account = Account(100)account.withdraw(500)print("Recoge tu dinero.")print(f"Saldo: {account.balance} €")Recoge tu dinero.Saldo: 100 €El cliente pide 500 € y tiene 100 €. El método se niega en silencio, el cajero no se da cuenta y dice «Recoge tu dinero.». El error se pierde. El método podría devolver True o False, pero quien lo llama puede olvidarse fácilmente de comprobarlo, y False no dice por qué.
La idea
raise: me niego y digo por qué
En lugar de callarse, el método lanza una excepción:
def withdraw(self, amount): if amount <= 0: raise ValueError("El importe debe ser positivo") if amount > self.balance: raise ValueError("No alcanza el dinero") self.balance -= amountraise termina el método como return. A la línea self.balance -= amount nunca se llega. Pero en lugar de un valor, el método envía una excepción, y quien lo llama no puede pasarla por alto:
account = Account(100)try: account.withdraw(500) print("Recoge tu dinero.")except ValueError as e: print(f"Rechazado: {e}")print(f"Saldo: {account.balance} €")Rechazado: No alcanza el dineroSaldo: 100 €ValueError("No alcanza el dinero")crea el objeto de la excepción, como cualquier objeto en Python. El mensaje va a su constructor, yprint(e)y el f-stringf"{e}"lo devuelven.ValueErrores la excepción ya hecha para «el valor del argumento es incorrecto». La usas justo en este tipo de comprobación.
Más precisamente, para curiosos
- Otra excepción ya hecha muy común es
TypeError: el valor es de un tipo incorrecto, como texto en lugar de un número. Para una acción que el objeto no permite en su estado (retirar de una cuenta cerrada), Python no tiene una excepción ya hecha aparte. Entonces haces la tuya, como verás enseguida. raisefunciona solo con un objeto de una clase hija deBaseException. No puedes lanzar un string:raise "No alcanza el dinero"se detiene conTypeError: exceptions must derive from BaseException.
Míralo
La excepción cae
def withdraw(self, amount): if amount > self.balance: raise ValueError("No alcanza el dinero") self.balance -= amount
def pay_rent(account): account.withdraw(800) print("El alquiler está pagado.")
try: pay_rent(Account(500))except ValueError as e: print(f"Rechazado: {e}")Paso a paso, en palabras
- El programa llama a pay_rent para una cuenta con 500 €.
- pay_rent llama a withdraw(800): un marco nuevo encima.
- 800 > 500: el método lanza una excepción con raise.
- El saldo no cambia: el marco se descarta sin terminar sus líneas.
- pay_rent no tiene except: «El alquiler está pagado.» no se imprime, el marco se descarta.
- except en el programa atrapa la excepción y este sigue desde aquí.
Recuerda la pila de llamadas de la lección de recursión. Abajo está el programa: el código del nivel superior. Llama a pay_rent, y esta llama a withdraw. Los marcos están uno encima de otro.
Cuando withdraw lanza, la excepción busca un except bajando por la pila. Un marco sin un except que coincida se descarta enseguida, sin terminar sus líneas. Eso es lo que le pasa también a pay_rent. Solo el programa tiene un except, y sigue desde ahí.
Si tampoco hubiera except ahí, el programa se detendría e imprimiría un stack trace: justo estos marcos.
Más precisamente, para curiosos
Aquí está el mismo programa, pero nadie atrapa (el archivo rent.py):
class Account: def __init__(self, balance): self.balance = balance
def withdraw(self, amount): if amount > self.balance: raise ValueError("No alcanza el dinero") self.balance -= amount
def pay_rent(account): account.withdraw(800) print("El alquiler está pagado.")
pay_rent(Account(500))Traceback (most recent call last): File "/projects/rent/rent.py", line 16, in <module> pay_rent(Account(500)) File "/projects/rent/rent.py", line 12, in pay_rent account.withdraw(800) File "/projects/rent/rent.py", line 7, in withdraw raise ValueError("No alcanza el dinero")ValueError: No alcanza el dineroEn Python el stack trace se llama traceback, y los marcos van de fuera hacia dentro: arriba está el programa (<module>), abajo está withdraw, y la última línea es el tipo y el mensaje. «most recent call last» significa que la llamada más reciente está abajo. Así que en Python empieza a leer por abajo.
Pruébalo
¿Qué llega a la pantalla?
La cuenta tiene 50 €, y el pedido es de 120 €.
¿Qué imprimirá el programa?
def checkout(account): print("Pago el pedido") account.withdraw(120) print("El pedido está pagado")
account = Account(50)try: checkout(account) print("Envío la confirmación")except ValueError as e: print(f"Rechazado: {e}")print(f"Saldo: {account.balance} €")«Pago el pedido» va antes del retiro. Después withdraw lanza, y el marco de checkout se descarta: «El pedido está pagado» nunca aparece. En el programa, «Envío la confirmación» también se salta, porque está en try después de la llamada. except imprime el rechazo, y el saldo queda intacto.
Pruébalo
Encuentra el error
El alquiler es de 800 €, y la cuenta tiene 500 €. El programa dice «El alquiler está pagado.». ¿En qué línea está el error?
Elige la línea con el error.
except atrapa la excepción, y pass significa «nada»: Python no permite un bloque vacío, así que escribes pass. El error desaparece, igual que con el cajero de la primera tarjeta. Se atrapa donde se puede hacer algo sensato. Aquí pay_rent no puede encontrar el dinero, así que no debería atrapar. Quita el try/except: la excepción llegará al programa, y este avisará a la persona.Más precisamente, para curiosos
- A veces una función atrapa, hace algo, como escribir en un registro, y vuelve a lanzar la misma excepción. En Python eso es
raisesin nada detrás, enexcept. El traceback sigue siendo el original.
def pay_rent(account): try: account.withdraw(800) except ValueError: print("Registro: el alquiler no se pagó") raise
try: pay_rent(Account(500))except ValueError as e: print(f"Rechazado: {e}")Registro: el alquiler no se pagóRechazado: No alcanza el dinero- También puedes lanzar otra excepción que diga mejor qué pasó, y guardar la antigua como causa. En Python:
raise RuntimeError("El alquiler no está pagado") from e. La antigua está ene.__cause__, y el traceback muestra las dos, con la línea «The above exception was the direct cause of the following exception».
def pay_rent(account): try: account.withdraw(800) except ValueError as e: raise RuntimeError("El alquiler no está pagado") from e
try: pay_rent(Account(500))except RuntimeError as e: print(f"{e}, porque: {e.__cause__}")El alquiler no está pagado, porque: No alcanza el dineroLa idea
Tu propia excepción
«No alcanza el dinero» es texto. Si el cajero quiere proponer «Ingresa 30 € más», tiene que saber cuánto falta. Sacar el número del texto es frágil. Mejor haz tu propia clase de excepción con un campo:
class InsufficientFundsError(Exception): def __init__(self, shortfall): super().__init__(f"Faltan {shortfall} €") self.shortfall = shortfallTu propia excepción es una clase normal que hereda de Exception. El constructor pasa el mensaje a la clase padre con super().__init__(...). El método la lanza cuando no alcanza el dinero:
def withdraw(self, amount): if amount <= 0: raise ValueError("El importe debe ser positivo") if amount > self.balance: raise InsufficientFundsError(amount - self.balance) self.balance -= amountY quien lo llama atrapa los dos casos por separado:
account = Account(100)try: account.withdraw(130)except InsufficientFundsError as e: print(e) print(f"Ingresa {e.shortfall} € más o elige un importe menor.")except ValueError as e: print(f"Rechazado: {e}")Faltan 30 €Ingresa 30 € más o elige un importe menor.Más precisamente, para curiosos
- Los nombres de las excepciones siempre terminan en
Error. Así se llaman también las ya hechas:ValueError,TypeError. La guía de estilo PEP 8 lo pide para toda excepción que sea un error. - Tu propia excepción también puede heredar de una clase ya hecha más concreta, como
ValueError. Entoncesexcept ValueErrortambién la atrapa. Aquí es aparte, así que el orden de los dos bloquesexceptno importa.
La idea
¿Propia o ya hecha?
Aquí no hay excepciones comprobadas como en Java. Nadie comprueba antes de que arranque el programa si quien llama atrapa. Lo que lanza una función está escrito en su documentación: un docstring con una sección «Raises:».
def withdraw(self, amount): """Retira el importe de la cuenta.
Raises: ValueError: si el importe no es positivo. InsufficientFundsError: si no alcanza el dinero. """ if amount <= 0: raise ValueError("El importe debe ser positivo") if amount > self.balance: raise InsufficientFundsError(amount - self.balance) self.balance -= amountEsto es solo una costumbre, para las personas y para el editor. Un programa que llama a withdraw sin try arranca y se ejecuta hasta que la excepción de verdad se lanza. La pregunta es otra: ¿necesitas una excepción propia?
Una excepción ya hecha, cuando el significado coincide: ValueError para un valor incorrecto, TypeError para un tipo incorrecto. Una propia, cuando quien llama tiene que distinguirla de las demás y reaccionar a su manera, cuando la excepción tiene que llevar datos, como shortfall, o cuando no hay ninguna ya hecha con ese significado, como para retirar de una cuenta cerrada.
Pruébalo
Rellena el hueco
Una excepción para una edad no válida. ¿Qué va en el hueco para que el mensaje llegue a la clase padre?
class InvalidAgeError(Exception):
def __init__(self, message):
().__init__(message)super().__init__(message) llama al constructor de la clase padre. La clase padre guarda el mensaje, y str(e) lo devuelve. Una excepción con solo un mensaje puede pasar sin __init__: class InvalidAgeError(Exception): pass hereda el constructor de Exception. Necesitas __init__ cuando la excepción también lleva sus propios datos, como shortfall.
Ejercicio
Excepciones en clases
Resuélvelas en tu editor.
1. Tienda de música. La tienda toca instrumentos en una demostración, pero no todos se pueden tocar.
- Una clase
MusicalInstrumentcon un nombre, una marca y un precio, y__init__. - Una clase abstracta
Playable(ABC)con un método abstractoplay()(@abstractmethod). Es la manera de Python de hacer una interfaz. GuitaryPianoheredan tanto deMusicalInstrumentcomo dePlayable, y pueden tocar.Drumhereda solo del instrumento.class NotPlayableError(Exception).MusicShopcon un métododemo_instrument(self, instrument): si el instrumento puede tocar (isinstance(instrument, Playable)), lo toca; si no, lanzaNotPlayableError.
- En el programa, haz la demostración de una guitarra Fender Stratocaster (1299 €), una batería Pearl Export (849 €) y un piano Yamaha P-45 (499 €) desde un array. Atrapa la excepción de cada instrumento por separado, para que la demostración siga.
Fender Stratocaster, 1299.00 €La guitarra toca un acorde.Pearl Export, 849.00 €Pearl Export no se puede usar en una demostración.Yamaha P-45, 499.00 €El piano toca una escala.2. Una cuenta con reglas. Añade a Account un método deposit(self, amount) que lance ValueError con un importe de 0 o menos. Retirar lanza InsufficientFundsError de la lección. La cuenta empieza con 100 €. Lee el número de operaciones y después las propias operaciones, cada una en su propia línea: «ingresa 50» o «retira 500». Después de cada una, imprime el saldo o por qué se rechazó la operación.
4ingresa 50retira 500retira -5retira 120Saldo: 150 €Faltan 350 €. Saldo: 150 €El importe debe ser positivo. Saldo: 150 €Saldo: 30 €3. Edad no válida. Person tiene un nombre y una edad. La propiedad age (@property con setter) lanza InvalidAgeError de la tarjeta anterior si la edad es menor que 0 o mayor que 150. El constructor también pasa por self.age = age, para que nadie pueda crear una persona con una edad no válida. Crea a María (19), Pablo (−3) y Juan (200).
María, 19 añosPablo: Edad no válida: -3Juan: Edad no válida: 200Más tareas
- Reserva de mesa. Una clase
Restaurantcon un número de mesas libres y un método para reservar una. Si se han acabado las mesas, lanza tu propiaNoTablesError. Si el número de invitados es 0 o menos, lanza la excepción ya hecha para un argumento incorrecto. - Un producto con existencias. Una clase
Productcon una cantidad en existencias y un métodosell(self, qty). Si no hay suficiente en existencias, lanzaOutOfStockError, que lleva cuántas unidades hay. La tienda atrapa la excepción y ofrece vender las que hay en existencias. - Tienda de música con motivo. Añade a
NotPlayableErrorun campo con el instrumento que no puede tocar, e imprime su precio enexcept.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Interfaces como clases abstractas · Comprobaciones de tipo · Excepciones · try, except y finally
- Continúa en: Datos en archivos · dataclass, match y patrones · Patrones de diseño · Tests con unittest
- Ejercicios sobre el tema: Errores y excepciones