Siempre que escribimos un programa vamos a encontrar algún tipo de error. Puede que el programa no haga estricatamente aquello para lo cual fue diseñado, o que no se hayan previsto ciertos escenarios de ejecución posibles. En principio, podemos considerar al menos dos tipos bien diferenciados de errores: errores de sintaxis y excepciones.
Los errores de sintaxis son probablemente los más comunes, principalmente cuando aún se está aprendiendo el lenguaje. Son detectados al analizar sintácticamente las instrucciones del programa, antes de ejecutarlas. De hecho, una instrucción que presenta un error de sintaxis no puede ejecutarse. Veamos un ejemplo a continuación.
while True print("Error de sintaxis")
El intérprete repite la línea que genera el error y se indica con una flecha el punto más cercano a su detección (en este caso la función print dado que falta los dos puntos ':' inmediatamente antes). También se indica el archivo y el número de línea para ubicar el error dentro de un programa.
Aún cuando una sentencia es sintácticamente correcta, puede generar un error al ser ejecutada. Los errores que se detectan durante la ejecución se conocen como excepciones. Si no se manejan correctamente hacen que el programa no pueda seguir funcionando y se fuerza la salida del flujo de ejecución. Existen mecanismos previstos en el lenguaje Python para manejar las excepciones adecuadamente, lo que puede proporcionar acciones para mitigar los efectos y permitir que el programa siga funcionando. En algunos casos los errores son irremediables, pero aún así, el manejo de excepciones permite salir del programa de forma más elegante, disminuyendo los daños.
Los siguientes ejemplos muestran dos tipos de excepción habituales, y lo que ocurre cuando no son manejadas.
10 * (1/0)
La excepciones son de diferente tipo, por ejemplo en este caso del tipo ZeroDivisionError, que corresponde a la división por cero.
4 + spam * 3
En este caso, la excepción es del tipo NameError, que identifica un nombre no definido. El resto de la línea agrega detalles sobre el tipo de excepción y qué la generó. Uno de los elementos más importantes de esta información es la pila de invocación (el stack), es decir, la sucesión de llamadas a funciones involucradas.
Como se puede ver, existen excepciones definidas en el lenguaje (denominadas incorporadas o built-in) que pueden ser generadas por el intérprete o por funciones propias del lenguaje. Se organizan en una estructura jerárquica tal como se muestra a continuación.
%load exceptions.txt
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
Es posible escribir programas que manejen las excepciones deseadas. El siguiente ejemplo muestra como manejar la excepción ValueError.
while True:
try:
x = int(input("Ingrese un número: "))
break
except ValueError:
print("Opa! Esto no es un número válido. Intente nuevamente ...")
Es interesante señalar que este programa no termina nunca, mientras no se ingresa un número. La forma de cortar la ejecución es usando el mecanismo provisto por el sistema operativo, por ejemplo Control-C. De hecho, esta interrupción generada por el usuario produce una excepción del tipo KeybordInterrupt (que no está siendo manejada).
La instrucción try funciona de la siguiente forma.
Una de las principales ventajas de esta forma de manejar excepciones es que divide el código correspondiente al flujo normal del programa y el código destinado a manejar flujos alternativos o no previstos, lo que resulta más claro, ordenado y fácil de corregir.
Una instrucción try puede estar acompañada de varias instrucciones except, para especificar el manejo de diferentes excepciones. Pero se ejecutará a lo sumo un único bloque except. Además, una misma instrucción except puede indicar varios tipos de excepción, como una tupla entre paréntesis.
except (RuntimeError, TypeError, NameError):
A continuación un ejemplo con varias instrucciones except.
import sys
try:
f = open('archivo.txt')
s = f.readline()
i = int(s.strip())
except IOError:
print('No se pudo abrir el archivo.')
except ValueError:
print("No se pudo convertir el dato a entero.")
except:
print("Error inesperado:", sys.exc_info()[0])
raise
La última instrucción except omite el nombre de la excepción, lo que genera que se capture cualquier tipo de excepción que ocurra. Sin embargo, si ocurre una excepción de los tipos indicados en las instrucciones except previas el código ejecutado es el correspondiente y se omite el manejo de la excepción genérica. Es por esta razón que la instrucción except no específica debe ir al final. Es importante manejar esta funcionalidad con cuidado. Uno de los usos habituales es el capturar la excepción para desplegar un mensaje y volver a lanzarla (más detalles luego) para que otro bloque de código sea el responable de manejarla adecuadamente.
El bloque try-except tiene la posibilidad de especificar una clausula else. Cuando se incluye, debe ir luego de todas las instrucciones except. Se utiliza para código que debe ser ejecutado cuando no se lanza ninguna excepción. Es diferente a agregar código dentro del bloque try-except ya que en caso de incluírlo podría generar que se capturen excepciones diferentes a las que se quiere manejar originalmente. A continuación un ejemplo de una instrucción else.
try:
f = open('archivo.txt', 'r')
except IOError:
print('No se pudo abrir el archivo.')
else:
print('El archivo tiene ', len(f.readlines()), 'líneas.')
f.close()
El manejo de excepciones permite capturar excepciones que ocurran no solo estricamente en el código dentro de un bloque try-except si no también dentro de las funciones que sean invocadas (incluso indirectamente). A continuación un ejemplo del manejo de una excepción que ocurre dentro de una función.
def this_fails():
x = 1/0
try:
this_fails()
except ZeroDivisionError as err:
print('Error dentro de una función:', err)
Existe la posibilidad de especificar una clausula finally para definir tareas que deben ejecutarse bajo cualquier circunstancia, es decir, en el curso normal de ejecución o cuando ocurre alguna excepción. En el siguiente ejemplo se puede ver el comportamiento del bloque finally en diferentes situaciones.
def divide(x, y):
try:
result = x / y
except ZeroDivisionError:
print("División por cero!")
else:
print("El resultado es: ", result)
finally:
print("Ejecutando el bloque finally.")
# divide(2, 1)
divide(2, 0)
#divide('2', '0')
Es interesante notar que cuando se especifican dos cadenas de caracteres como entrada a la función divide ocurre una excepción (porque no está definida la operación para cadenas de caracteres). Dicha excepción no es capturada en un bloque except, pero aún en ese caso se ejecuta el código del bloque finally.
Una de las aplicaciones más habituales de un bloque finally es la de liberar recursos externos (como archivos, memoria o conecciones de red), independientemente de si el uso de dichos recursos fue exitoso.
Este tipo de acciones está predefinido en algunos casos, para liberar los recursos asociados a una cierta parte del código cuando estos ya no son necesarios. Uno de estos casos es la instrucción with que vimos anteriormente para manejo de archivos, y se muestra en el siguiente ejemplo.
for line in open("archivo.txt"):
print(line, end="")
El problema con el código anterior es que deja el archivo abierto por un tiempo indeterminado, luego de que el código ha sido ejecutado. Esto puede no tener un impacto significativo en la mayoría de los casos, pero se puede tornar crítico en aplicaciones grandes. La instrucción with permite usar archivos (u otro tipo de objetos) en una forma que asegura que sean liberados los recursos asociados de inmediato y en forma correcta.
with open("archivo.txt") as f:
for line in f:
print(line, end="")
Luego de que se ejecutó la instrucción with el archivo f siempre es cerrado correctamente, incluso cuando ocurrió un error al procesar sus líneas. Los objetos que, como ocurre con los archivos, proveen de acciones predefinidas de cierre lo indican en su documentación.
Existe la posibilidad de que el usuario del lenguaje cree sus propias excepciones y las utilize en su propio código. Esto se hace típicamente a través de extender la clase Exception, es decir, crear nuevos tipos de excepción en la estructura jerárquica. Las nuevas excepciones deberían reflejar situaciones excepcionales que pueden darse en el contexto de la aplicación. Cuando estas situaciones ocurren, el programador puede crear una excepción y lanzarla, de modo de que el bloque de codigo destinado a ella la maneje correctamente.
Veremos a continuación dos temas adicionales referidos a funciones en Python: funciones con número variable de argumentos, y funciones anidadas.
Hay ocasiones en las que necesitamos crear una función que pueda recibir un número arbitrario de argumentos. En python eso puede implementarse fácilmente con los argumentos con asterisco: *arg.
En el ejemplo siguiente se puede observar el funcionamiento de una función con número variable de argumentos:
def funcion(*args):
'''número variable de argumentos, a cada uno aplica la operación *=3'''
for arg in args:
arg *= 3
print(arg)
return None
v = 5 # variable numérica
s = "AB" # cadena
t = (1, 2, 3) # tuple
l = [1, 2, 3] # lista
print("valor inicial de las variables:")
print(v)
print(s)
print(t)
print(l)
print("\nsalida de la función:")
funcion(v, s, t, l)
print("\nvalor de las variables después de llamar la función:")
print(v)
print(s)
print(t)
print(l)
Se pueden observar también dos aspectos importantes:
Se pueden combinar argumentos fijos con un argumento de número variable, pero en este caso el argumento de número variable debe ir al final. Sólo se puede usar un argumento de número variable en la definición de una función.
Toda función devuelve un valor de algún tipo, y podemos hacer que devuelva una función. Es decir que podemos definir una función dentro de la definición de otra función, a lo que se le llama funciones anidadas.
def acorde(intervalos):
'''define tipo de acorde con una estructura de intervalos'''
def transporte(fundamental):
'''transporta el tipo de acorde a una fundamental'''
pcset = [fundamental+i for i in intervalos]
return pcset
return transporte
mayor = acorde([0, 4, 7])
menor = acorde([0, 3, 7])
print(mayor(60))
print(menor(60))
print(menor(66))