Ir al contenido
wannacode

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 -= amount

El cajero lo usa así:

account = Account(100)
account.withdraw(500)
print("Recoge tu dinero.")
print(f"Saldo: {account.balance} €")
Salida
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 -= amount

raise 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} €")
Salida
Rechazado: No alcanza el dinero
Saldo: 100 €
  • ValueError("No alcanza el dinero") crea el objeto de la excepción, como cualquier objeto en Python. El mensaje va a su constructor, y print(e) y el f-string f"{e}" lo devuelven.
  • ValueError es 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.
  • raise funciona solo con un objeto de una clase hija de BaseException. No puedes lanzar un string: raise "No alcanza el dinero" se detiene con TypeError: 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
  1. El programa llama a pay_rent para una cuenta con 500 €.
  2. pay_rent llama a withdraw(800): un marco nuevo encima.
  3. 800 > 500: el método lanza una excepción con raise.
  4. El saldo no cambia: el marco se descarta sin terminar sus líneas.
  5. pay_rent no tiene except: «El alquiler está pagado.» no se imprime, el marco se descarta.
  6. 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))
Error
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 dinero

En 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} €")
Respuesta

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.

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 raise sin nada detrás, en except. 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}")
Salida
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á en e.__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__}")
Salida
El alquiler no está pagado, porque: No alcanza el dinero

La 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 = shortfall

Tu 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 -= amount

Y 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}")
Salida
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. Entonces except ValueError también la atrapa. Aquí es aparte, así que el orden de los dos bloques except no 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 -= amount

Esto 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)

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 MusicalInstrument con un nombre, una marca y un precio, y __init__.
  • Una clase abstracta Playable(ABC) con un método abstracto play() (@abstractmethod). Es la manera de Python de hacer una interfaz.
  • Guitar y Piano heredan tanto de MusicalInstrument como de Playable, y pueden tocar. Drum hereda solo del instrumento.
  • class NotPlayableError(Exception).
  • MusicShop con un método demo_instrument(self, instrument): si el instrumento puede tocar (isinstance(instrument, Playable)), lo toca; si no, lanza NotPlayableError.
  • 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.
Salida
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.

Entrada
4
ingresa 50
retira 500
retira -5
retira 120
Salida
Saldo: 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).

Salida
María, 19 años
Pablo: Edad no válida: -3
Juan: Edad no válida: 200

Más tareas

  • Reserva de mesa. Una clase Restaurant con un número de mesas libres y un método para reservar una. Si se han acabado las mesas, lanza tu propia NoTablesError. 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 Product con una cantidad en existencias y un método sell(self, qty). Si no hay suficiente en existencias, lanza OutOfStockError, 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 NotPlayableError un campo con el instrumento que no puede tocar, e imprime su precio en except.

Test

Ponte a prueba

1. ¿Qué hace raise ValueError("...")?
2. El programa llama a a, a llama a b, b lanza una excepción. Solo el código del nivel superior tiene un except. ¿Qué pasa con las líneas de a después de la llamada a b?
3. ¿Dónde es mejor atrapar la excepción?
4. ¿De qué clase hereda tu propia excepción en Python?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·