Las validaciones son técnicas que permiten asegurar que los valores con los que se vaya a operar estén dentro de determinado dominio.
Estas técnicas son particularmente importantes al momento de utilizar entradas del usuario o de un archivo (o entradas externas en general) en nuestro código, y también se las utiliza para comprobar precondiciones. Al uso intensivo de estas técnicas se lo suele llamar programación defensiva.
Si bien quien invoca una función debe preocuparse de cumplir con las precondiciones de ésta, si las validaciones están hechas correctamente pueden devolver información valiosa para que el invocante pueda actuar en consecuencia.
Hay distintas formas de comprobar el dominio de un dato. Se puede comprobar el contenido; que una variable sea de un tipo en particular; o que el dato tenga determinada característica, como que deba ser "comparable", o "iterable".
También se debe tener en cuenta qué hará nuestro código cuando una
validación falle, ya que queremos darle información al invocante que le
sirva para procesar el error. El error producido tiene que ser
fácilmente reconocible. En algunos casos, como por ejemplo cuando se
quiere devolver una posición, devolver -1
nos puede asegurar que el
invocante lo vaya a reconocer. En otros casos, levantar una excepción es
una solución más elegante.
En cualquier caso, lo importante es que el resultado generado por
nuestro código cuando funciona correctamente y el resultado generado
cuando falla debe ser claramente distinto. Por ejemplo, si el código
debe devolver un elemento de una secuencia, no es una buena idea que
devuelva None
en el caso de que la secuencia esté vacía, ya que None es
un elemento válido dentro de una secuencia.
12.3.1. Comprobaciones por contenido
Cuando queremos validar que los datos provistos a una porción de código contengan la información apropiada, ya sea porque esa información la ingresó un usuario, fue leída de un archivo, o porque por cualquier motivo es posible que sea incorrecta, es deseable comprobar que el contenido de las variables a utilizar estén dentro de los valores con los que se puede operar.
Estas comprobaciones no siempre son posibles, ya que en ciertas situaciones puede ser muy costoso corroborar las precondiciones de una función. Es por ello que este tipo de comprobaciones se realizan sólo cuando sea posible.
Por ejemplo, la función factorial está definida para los números
naturales incluyendo el 0
. Es posible utilizar assert
(que es otra forma
de levantar una excepción) para comprobar las precondiciones de
factorial.
def factorial(n):
""" Calcula el factorial de n.
Pre: n debe ser un entero, mayor igual a 0
Post: se devuelve el valor del factorial pedido
"""
assert n >= 0, "n debe ser mayor igual a 0"
fact=1
for i in xrange(2,n+1):
fact*=i
return fact
12.3.2. Entrada del usuario
En el caso particular de una porción de código que trate con entrada del usuario, no se debe asumir que el usuario vaya a ingresar los datos correctamente, ya que los seres humanos tienden a cometer errores al ingresar información.
Por ejemplo, si se desea que un usuario ingrese un número, no se debe
asumir que vaya a ingresarlo correctamente. Se lo debe guardar en una
cadena y luego convertir a un número, es por eso que es recomendable el
uso de la función raw_input
ya que devuelve una cadena que puede ser
procesada posteriormente.
def lee_entero():
""" Solicita un valor entero y lo devuelve.
Si el valor ingresado no es entero, lanza una excepción. """
valor = raw_input("Ingrese un número entero: ")
return int(valor)
Esta función devuelve un valor entero, o lanza una excepción si la conversión no fue posible. Sin embargo, esto no es suficiente. En el caso en el que el usuario no haya ingresado la información correctamente, es necesario volver a solicitarla.
def lee_entero():
""" Solicita un valor entero y lo devuelve.
Mientras el valor ingresado no sea entero, vuelve a solicitarlo. """
while True:
valor = raw_input("Ingrese un número entero: ")
try:
valor = int(valor)
return valor
except ValueError:
print "ATENCIÓN: Debe ingresar un número entero."
Podría ser deseable, además, poner un límite a la cantidad máxima de intentos que el usuario tiene para ingresar la información correctamente y, superada esa cantidad máxima de intentos, levantar una excepción para que sea manejada por el código invocante.
def lee_entero():
""" Solicita un valor entero y lo devuelve.
Si el valor ingresado no es entero, da 5 intentos para ingresarlo
correctamente, y de no ser así, lanza una excepción. """
intentos = 0
while intentos < 5:
valor = raw_input("Ingrese un número entero: ")
try:
valor = int(valor)
return valor
except ValueError:
intentos += 1
raise ValueError, "Valor incorrecto ingresado en 5 intentos"
Por otro lado, cuando la entrada ingresada sea una cadena, no es esperable que el usuario la vaya a ingresar en mayúsculas o minúsculas, ambos casos deben ser considerados.
def lee_opcion():
""" Solicita una opción de menú y la devuelve. """
while True:
print "Ingrese A (Altas) - B (Bajas) - M (Modificaciones): ",
opcion = raw_input().upper()
if opcion in ["A", "B", "M"]:
return opcion
12.3.3. Comprobaciones por tipo
En esta clase de comprobaciones nos interesa el tipo del dato que
vamos a tratar de validar, Python nos indica el tipo de una variable
usando la función type(variable)
. Por ejemplo, para comprobar que una
variable contenga un tipo entero podemos hacer:
if type(i) != int:
raise TypeError, "i debe ser del tipo int"
Sin embargo, ya hemos visto que tanto las listas como las tuplas y las cadenas son secuencias, y muchas de las funciones utilizadas puede utilizar cualquiera de estas secuencias. De la misma manera, una función puede utilizar un valor numérico, y que opere correctamente ya sea entero, flotante, o complejo.
Es posible comprobar el tipo de nuestra variable contra una secuencia de tipos posibles.
if type(i) not in (int, float, long, complex):
raise TypeError, "i debe ser numérico"
Si bien esto es bastante más flexible que el ejemplo anterior, también
puede ser restrictivo ya que - como se verá más adelante - cada
programador puede definir sus propios tipos utilizando como base los que
ya están definidos. Con este código se están descartando todos los tipos
que se basen en int
, float
, long
o complex
.
Para poder incluir estos tipos en la comprobación a realizar, Python nos
provee de la función isinstance(variable, tipos)
.
if not isinstance(i, (int, float, long, complex) ):
raise TypeError, "i debe ser numérico"
Con esto comprobamos si una variable es de determinado tipo o subtipo de éste. Esta opción es bastante flexible, pero existen aún más opciones.
Advertencia Hacer comprobaciones sobre los tipos de las variables suele resultar demasiado restrictivo, ya que es muy posible que una porción de código que opere con un tipo en particular funcione correctamente con otros tipos de variables que se comporten de forma similar.
Es por eso que hay que tener mucho cuidado al limitar el uso de una variable por su tipo, y en muchos casos es preferible limitarlas por sus propiedades, como el ejemplo anterior, en que se requería que se pudiera convertir a un entero.
Para la mayoría de los tipos básicos de Python existe una función que se
llama de la misma manera que el tipo que devuelve un elemento de ese
tipo, por ejemplo, int()
devuelve 0
, dict()
devuelve {}
y así. Además,
estas funciones suelen poder recibir un elemento de otro tipo para
tratar de convertirlo, por ejemplo, int(3.0)
devuelve 3
, list("Hola")
devuelve ['H', 'o', 'l', 'a']
.
Usando está conversión conseguimos dos cosas: podemos convertir un tipo recibido al que realmente necesitamos, a la vez que tenemos una copia de este, dejando el original intacto, que es importante cuando estamos tratando con tipos mutables.
Por ejemplo, si se quiere contar con una función de división entera que pueda recibir diversos parámetros, podría hacerse de la siguiente manera.
def division_entera(x,y):
""" Calcula la división entera después de convertir los parámetros a
enteros. """
try:
dividendo = int(x)
divisor = int(y)
return dividendo/divisor
except ValueError:
raise ValueError, "x e y deben poder convertirse a enteros"
except ZeroDivisionError:
raise ZeroDivisionError, "y no puede ser cero"
De esta manera, la función division_entera
puede ser llamada incluso
con cadenas que contengan expresiones enteras. Que este comportamiento
sea deseable o no, depende siempre de cada caso.
12.3.4. Comprobaciones por características
Otra posible comprobación, dejando de lado los tipos, consiste en
verificar si una variable tiene determinada característica o no. Python
promueve este tipo de programación, ya que el mismo intérprete utiliza
este tipo de comprobaciones. Por ejemplo, para imprimir una variable,
Python convierte esa variable a una cadena, no hay en el interprete una
verificación para cada tipo, sino que busca una función especial, llamada __str__
, en la variable a imprimir, y si existe, la utiliza para convertir la variable a una cadena.
Nota Python utiliza la idea de duck typing, que viene del concepto de que si algo parece un pato, camina como un pato y grazna como un pato, entonces, se lo puede considerar un pato.
Esto se refiere a no diferenciar las variables por los tipos a los que pertenecen, sino por las funciones que tienen.
Para comprobar si una variable tiene o no una función Python provee la
función hasattr(variable, atributo)
, donde atributo
puede ser el nombre
de la función o de la variable que se quiera verificar. Se verá más
sobre atributos en la unidad de Programación Orientada a Objetos.
Por ejemplo, existe la función __add__
para realizar operaciones de
suma entre elementos. Si se quiere corroborar si un elemento es sumable,
se lo haría de la siguiente forma.
if not hasattr(i,"__add__"):
raise TypeError, "El elemento no es sumable"
Sin embargo, que el atributo exista no quiere decir que vaya a funcionar correctamente en todos los casos. Por ejemplo, tanto las cadenas como los números definen su propia "suma", pero no es posible sumar cadenas y números, de modo que en este caso sería necesario tener en cuenta una posible excepción.
Por otro lado, en la mayoría de los casos se puede aplicar la frase: es
más fácil pedir perdón que permiso, atribuída a la programadora Grace
Hopper. Es decir, en este caso es más sencillo hacer la suma dentro de
un bloque try
y manejar la excepción en caso de error, que saber cuáles
son los detalles de la implementación de __add__
de cada tipo
interactuante.