G A C E T A   D E   L I N U X
...haciendo a Linux un poco más divertido!
Salvando a los Usuarios de Sí Mismos
- o - Tratando con la Entrada de Usuario en Python

Por Paul Evans
Traducción al español por José Gregorio Del Sol Cobos
el día 24 de Marzo 2003, para La Gaceta de Linux

Usted no habrá estado usando Python mucho tiempo antes de escribir un programa que necesite la entrada por el usuario. Como un programador de Python novel y ancho de miras, usted probablemente espera ingenuamente que puede pedirles la entrada a los usuarios y que ellos le darán justo eso...

AVISO: Mostrar la frase anterior a programadores veteranos puede causar que se revuelquen por el suelo de risa, desesperadamente.

Los usuarios no funcionan así.

Por ejemplo, si usted pide una simple respuesta "s o n", su usuario puede escribir cuidadosamente su nombre - o el orden de su comida, o nada en absoluto, y su programa puede caer. Ellos no lo hacen a propósito (bueno, la mayoría de las veces). Es simplemente que los pobrecillos se distraen fácilmente, ignoran por completo sus prompts de entrada tan cuidadosamente creados y a menudo escriben algo completamente alejado de lo que concierne a su programa. Seguido, bastante curiosamente, se le quejarán a usted, el programador. Entonces usted parecerá un loco y se sentirá infeliz.

Para impedir tanta miseria, la primerísima cosa que usted necesita es asegurarse de que cualquier cosa que venga del usuario es probada para ver si es siquiera vagamente similar a lo que usted esperaba. Python tiene abundancia de funciones para ayudarle con esto, y empezaremos a recorrer juntos algunas de ellas abajo.

Otra cosa que usted puede hacer es usar los validadores en sus widgets (ventanitas) de entrada. El modo como funcionan es que simplemente desechan cualquier pulsación de teclas que no concierna a lo que usted quiere. Como un ejemplo, si usted establece un validador numérico en un widget de cadena, los usuarios podrán presionar "ABC", etc., tanto como quieran, que no se mostrará nada en el widget. Las únicas teclas que pueden presionar que tengan efecto son 0-9 y, quizás, un punto decimal o símbolo del dólar. Jugaremos también con éstos más tarde.

Finalmente, incluso si usted es lo suficientemente afortunado como para encontrarse en posesión de un usuario obediente y particularmente bien entrenado, que siempre escriba lo que le pide, la entrada no será fácilmente formateada del modo que usted quiere. El escribir sin cuidado a menudo produce cadenas como "joHN sMith" (bloqueo de las mayúsculas) o números de teléfono parecidos a "604555-1212".

Aparte de bromas, es efectivamente su trabajo como programados hacer la entrada de datos para el ususario tan fácil y rápida como sea posible, de modo que pueda presentarse y almacenarse con un formato consistente. Además, usted puede tener la recompensa de la satisfacción personal e incluso, ¿lo diré?, la gratitud de usuarios a los que salve del infierno de escribir adecuadamente algo así como un código postal canadiense.

Adquirir la entrada

Primero, su programa necesitará adquirir alguna entrada por parte del usuario. Desde la consola, Python ofrece dos métodos para esto: "raw_input("Prompt")", y "input("Prompt")". (No use "input", vea más abajo). Usted puede obtener también la entrada de los viejos buenos argumentos de la línea de comandos o de las variables de entorno.

Otros métodos, más gráficos, están disponibles, sin ir demasiado lejos, como Xdialog, Gdialog (parte de las gnome-utils) o Kaptain.

El acceso a completas herramientas de GUI está disponible en Python mediante PyQT , TKinter, WxPython y PyGTK entre otras

Éste es probablemente un buen momento para decirle unas cuantas palabras de precaución. La mayoría de lso usuarios son criaturas dóciles y fáciles de contentar, a las que les gusta ser educadamente corregidas, pero usted encontrará tipos sin escrúpulos inclinados a la destucción.

Por esta razón, usted no puede permitir nunca que la entrada de usuario invada su espaco de comandos:

Está bien. Relájese. La peor parte ha pasado.

Abra una xterm y escriba "python" para entrar al intérprete. Observe: muchos de estos ejemplos requierenque usted esté usando una versión de Python mayor o igual que la "2". Redhat aún viene con la 1.5x por defecto, así que si es usted usuario de Redhat probablemente tendrá que escribir "python2" (y probablemente instalar los rpm a partir de los añadidos). A modo de información general, la versión "1.5" fue publicada en un año que empezaba por los dígitos "1" y "9".

Comprobando el Contenido de los Objetos Cadena

Los lenguajes de programación a menudo incluyen métodos para este tipo de comprobación, y Python no es una excepción. Considere uno de nuestros primeros objetivos tal y como afirmamos arriba: asegurarnos de que el usuario nos da un número válido cuando le pedimos uno.

Ocurre que todos los objetos cadena en Python tienen métodos preconstruídos ("built-in") que hacen esto casi indoloro. Escriba estas líneas en el prompt ">>>":

>>>input = '9'
>>>input.isdigit()
1

Esto devolverá un "1" (verdadero), así que usted puede usarlo fácilmente en una sentencia "if" como una condición. Algunos otros atributos de este tipo son:

s.isa1num() devuelve verdadero si todos los caracteres en s son alfanuméricos, y falso
en cualquier otro caso.
s.isalpha() devuelve verdadero si todos los cracteres son alfabéticos, y falso en cualquier
otro caso.

Para una lista completa de éstos y mucho más, recomiendo encarecidamente la Referencia Rápida de Python2.1 Empleo ésta todo el tiempo e incluso tengo una versión en texto más vieja incluida en HNB por rapidez.

Esto nos llevará a través de casos sencillos como elecciones de menú, pero ¿qué pasa si queríamos un número de coma flotante o real?

Considere esto:

input = '9.9' or
input = '-9'

Ambos son números válidos, pero input.isdigit() devolverá "0" (falso), porque el signo negativo y el punto decimal no son "dígitos". Nuestro pobre usuario quedará muy confuso cuando le demos un mensaje de error si estas entradas son válidas.

Así pues, asumamos que son lo que queremos e intentemos convertirlos explícitamente. Para ello emplearemos la construcción de Python try/except. Python lanza excepciones de diferentes tipos sobre los errores y podemos atrapar esos errores individualmente por su nombre.

Suponga que queremos un entero como "-9", podemos usar el operador "int()" para intentar explícitamente la conversión.

try:
    someVar = int(input)
    print 'Is an integer'
except (TypeError, ValueError):
    print 'Not an integer'

Hay que avisar de dos cosas aquí. La primera es que estamos comprobando dos excepciones diferentes, Tipo (Type) y Valor (Value). De este modo no sólo se manejará el caso en que el usuario ingrese un número de coma flotante (como "9.9"), sino que también permitimos la posibilidad de que ni siquiera ingresen un número de nigún tipo, quizás ingresaron "Jamón". El segundo aviso es que efectivamente incluimos los tipos de excepciones que nos interesaba atrapar.Es muy fácil escribir excepciones de extremo abierto sin preocuparnos de mirar qué errores estás atrapando, cómo aquí:

try:
    someVar = int(input)
    print 'Is an integer'
except:
    print 'Not an integer'

NO HAGA ESO. Python le dejará hacerlo, pero desde el momento en que no está atrapandotodaslas excepciones la revisión puede ser una pesadilla para usted si algo se rompe.Simplemente confíe en mí sobre esto; busque los errores que quiere atrapar y ahorrarátiempo a largo plazo.

Otros operadores que encontrará útiles son long() y float(). En el otro lado, str()puede convertir cualquier cosa a una cadena.

No se olvide de comprobar el rango - no será una buena felicitación para usted asegurarse de que su programa siempre obtiene un entero del usuario si inocentemente acepta el entero "42" como un día del mes válido... Asegúrese de que el número cae dentro del rango esperado usando los operadores de comparación ">, <, >=", etc.

Validando la Entrada

Como ya hemos visto, podemos validar la entrada después de obtenerla, pero ¿no sería bonito que pudiésemos prevenir al usuario de ingresar errores en primer lugar?

Validadores de widgets enter

Son cosas construidas en los conjuntos de herramientas de interfaces gráficas de usuario que previenen pulsaciones de teclas no deseadas incluso de aparecer enla cadena del widget. Estos programas generalmente vienen con algunos validadorespreconstruídos para valores numéricos, alfabéticos y alfanuméricos, etc., y sonbastante fáciles de usar. Ahora mismo el entorno que más uso yo es PyQTpara guis (interfaces gráficas de usuario), pero TKinter, WxPythone incluso Kaptaintienen todos ellos validadores. Podría equivocarme, pero PyGTK parece no tenerlos aún. Quizás podría ponerse a trabajar y crearse el suyo propio si resulta que está usando un entorno que no los posee.

Si los validadores preconstruidos no le sirven, entonces, PyQt, por ejemplo, le permite especificar los suyos propios, a medida.

Claramente, no puedo entrar en detalles con cada entorno aquí, pero aquí hay un ejemplo de cómo adjuntar un validador numérico a un widget en PyQt. El nombre del widget es "self.rate", adjuntamos el "QDoubleValidator" y le decimos que acepte números entre 0.0 y 999.0 con hasta dos lugares decimales:

self.rate.setValidator(QDoubleValidator(0.0, 999.0, 2, self.rate) )

¿Bonito, eh? ¡Observe que se ocupa por nosotros de comprobar el rango también!

Otros modos de ayudar a los usuarios a ingresar información incluye los menús desplegables, las listas para cliquear y los cuadros combinados, pero eso ya losabía usted.

Formateando la Entrada

¿Recuerda el ejemplo "jOHN sMith" de la introducción? Aquí está el modo de arreglarlo:

>>>'jOHN sMith'.title()
'John Smith'

En efecto, otro atributo más de todos los objetos cadena de Python es "title()", que gustosamente pasará a mayúsculas cada palabra por usted."capitalize()" es parecido, pero sólo lo hace con el primer carácter:

>>> 'jOHN sMith'.capitalize()
'John smith'

Vaya más legos e intente "upper()", "lower()" y "swapcase()" por su cuenta si quiere.Creo que puede adivinar su comportamiento.

>>> 'John Smith'.rjust(15)
'     John Smith'

>Nuestra cadena ha sido justificada a la derecha para nosotros en una cadena de 15caracteres de longitud. Perfecto. Como probablemente ya habrá adivinado, existen también"center(n)" y "ljust(n)". De nuevo, eche un vistazo a la Referencia Rápida de Python 2.1 para verlos todos.

Otro operador muy importante en Python es el operador tanto por ciento "%". La descripciónde éste en combinación con objetos lista y códigos de formato al estilo printf podríaconsumir fácilmente varias páginas, así que sólo voy a glosarlo con algunos ejemplos paradespertar su interés hoy.

En su forma más simple, el operador "%" te permite escribir, por ejemplo, una frase en tiempo de ejecución:

>>> 'Éste es un %s ejemplo de su %s' % ('buen', 'uso')
"Éste es un buen ejemplo de su uso"

Al menos, espero que lo sea. Éste es sólo elcomienzo de su poder. Además de la simple sustitucióndel objeto cadena con "%s" hay también "&r" y los amigos de printf que vienen del lenguaje C:c, d, i, u, o, x, X, e, E, f, g, G.

Aquí hay un ejemplo de la Referencia Rápida de Python 2.1

>>> '%s has %03d quote types.' % ('Python', 2)
'Python has 002 quote types.'

El lado derecho puede ser también un mapeo que le permita referirse a los campos por su nombre.

Avancemos a algo un poco más novedoso, pero bastante común.

Números de Teléfono

Los números de teléfono son variables en longitud. Algunas veces sólo tienen 2 ó 3 dígitos si usted está detrás de un sistema corporativo PBX. Otras veces pueden extenderse hasta los 15 dígitos o más para las llamadas internacionales. Incluso podrían contener símbolos "#" o asteriscos. Puede que incluso comas. Peor aún, el usuario puede intentar imponer un formato según los va ingresando. O un formato parcial. O no.

Ahora, lo único que hará será frustrar a los usuarios si no les deja al menos intentar ingresarlo adecuadamente, así que su validador sería mejor que aceptara todos los #, *, comas, -, ), (, así como los dígitos 0-9. Desde luego, usted puede acabar con:

'250-(555)-12-12'

en lugar de la cadena:

'(250) 555-1212'

que efectivamente quiere (para un número de teléfono norteamericano). No se preocupe, haremos la solución lo suficientemente genérica como para manejar prácticamente cualquier cosa.

Mi primer intuición cuando necesito algo como esto es copiar el trabajo de otro excavando en Google, especialmente en Grupos Google. Resulta una buena intuición, puesto que la pieza de código que suelo encontrar será bastante mejor que la que podría hacer por mi cuenta. Desafortunadamente, esta vez resultó en un mensaje de Guido van Rossum (el inventor de Python) explicándole a alguien que Python no tenía tal cosa y que quizás podría usar algo como:

import string
def fmtstr(fmt, str):
    res = [] i = 0
    for c in fmt:
        if c == '#':
            res.append(str[i:i+1]) i = i+1
        else:
            res.append(c)
    res.append(str[i:])
    return string.join(res)

Esto es un buen comienzo, desde luego, y no puedes quejarte de las crdenciales de su autor, pero no maneja todos los casos sin un montón de constructores "if/then" para contar cuántos dígitos te dan y elegir una cadena con la longitud adecuada. Vaya más lejos y cópielo en su xterm y llame entonces como sigue:

>>> fmtstr('###-####', '5551212')
'5 5 5 - 1 2 1 2 '

De hecho, yo lo copié y lo pegué en mi editor y entonces construí una larga secuencia de "if/then"-s, para números de teléfono, fechas y otros tipos de entradas, pero aún no estaba manejando nada. Más: tenía docenas y docenas de líneas haciendo cosas similares. Han superado su recompensa.

De acuerdo, allá vamos... Primero, filtremos cualesquiera caracteres de formato "extra" que le permitamos meter al usuario:

def filter(inStr, allowed):
    outStr = ''
    for c in inStr:
        if c in allowed:
            outStr += c
    return outStr

Podríamos llamarlo así:

>>>filter('250-(555)-12-12', string.digits)
'2505551212'

O podíamos definir el segundo argumento nosotros mismos como "0123456789#*," para incluir todos los caracteres permisibles.

Ahora simplemente cogemos la pieza de código de Guido y (ésta es la buena) invertimos ambos argumentos de entrada. De este modo podemos especificar justo una cadena larga para formato y casará hasta que la ejecutemos fuera de la entrada. Cualquier entrada extra será simplemente bordeada, de modo que nunca perdemos caracteres.

# import the regular expression module
import re

def formatStr(inStr, fmtStr, p = '^'):
    inList = [x for x in inStr] #list from strings..
    fmtList = [x for x in fmtStr]
    # the good bit
    inList.reverse(); fmtList.reverse()
    outList = []
    i = 0
    for c in fmtList:
        if c == p:
            try:
                outList.append(inList[i])
                i += 1
            # break if fmtStr longer than inStr
            except IndexError:
                break
        else:
            outList.append(c)
    # handle inStr longer than fmtStr
    while i < len(inList):
        outList.append(inList[i])
        i += 1
    # put it back the way we found it
    outList.reverse()
    outStr = ''.join(outList)
    # remove stray parens/- etc
    while re.match('[)|-| ]', outStr[0]):
        outStr = outStr[1:]
    # close any legit parens
    while outStr.count(')') > outStr.count('('):
        outStr = '(' + outStr
    return outStr

[Versión en texto de este listado.]

Es básicamente lo mismo de Guido salvo que el contenedor de carácter por defecto es ahora un "^" (acento circunflejo, o caret), porque podemos necesitar usar el "#". Alternativamente, esto se puede especificar como un tercer argumento, opcional, si incluso leemos carets reales de la entrada.

He aquí alguna salida de muestra:

>>> formatStr('51212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'5-1212'
>>> formatStr('045551212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'(04) 555-1212'
>>> formatStr('16045551212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'1 (604) 555-1212'
>>> formatStr('1011446045551212', ' ^^^ ^^ (^^^) ^^^-^^^^')
'1 011 44 (604) 555-1212'

En la práctica, usted probablemente querrá simplemente definir su formato de número de teléfono como, por ejemplo:

phone_format_str = ' ^^^ ^^ (^^^) ^^^-^^^^'

Hay un espacio al comienzo de la cadena para que cualesquiera caracteres adicionales no se amontonen. Podría llamarlo perfectamente así:

formatStr(input, phone_format_str)

... después usted limpia su "entrada" con algo como la función "filter()".

Códigos Postales

En caso de que usted no esté (desgraciadamente) familiarizado con los códigos postales canadienses, son como el siguiente:

'V8G 4L2'

Que parece bastante inocuo hasta que intenta escribirlo. Especialmente para los que no son mecanógrafos (como yo). Usted puede activar el bloqueo de las mayúsculas, y luego olvidarse de desactivarlo, o puede escribir [shift]+letra, número, [shift]+letra, etc. y muy a menudo acabr con "v*g $1@" cuando acaba la secuencia. No hace falta decirlo, los usuarios odian escribirlos y casi nunca aparecen bien. La mayoría de las veces su aplicación ni siquiera cogerá los códigos postales, simplemente porque los usuarios no querrán molestarse. Otros países tienen códigos postales similares. Lástima.

Ahora, con nuestra nueva función formateadora, están acabados. Primero, bien validamos, bien filtramos lo que nos den, luego simplemente usamos el atributo de cadena de Python preconstruido "upper()" para establecer el tipo de los caracteres alfabétcos adecuadamente, finalmente:

>>>formatStr('V8G4L2', ' ^^^ ^^^')
'V8G 4L2'

Si los códigos postales precisos son vitales para su aplicación, usted necesitará hacer más verificación mediante el conteo de caracteres y verificando el patrón. Sin embargo, para uso general, usted necesita permitir los códigos postales de otros países. Creo que normalmente sólo formateo si el número de caracteres ==6 y después limpio.

¿Y con los Números de la Seguridad Social? Lo mismo:

>>> formatStr('716555123', '^^^-^^^-^^^')
'716-555-123'

Debería ejecutar una rutina de comprobación de dígitos sobre los Números de la Seguridad Social primero, para verificar que son válidos. Y lo mismo para las tarjetas de crédito.

Espero que estos ejemplos le ahorren algo de tiempo al programar interfaces de usuario. Me gustaría mucho recibir ejemplos o mejoras de ustedes. En particular, modos de tratar con fechas 1 y usuarios. Siempre son divertidas.

Al mismo tiempo, es muy importante que no mantenga estas ayudas al formateo como un secreto ante sus usuarios. Póngalos en la "ayuda", emplee "notitas" ("tooltips") o el "qué es" para hacerles saber que las facilidades son para ellos. Si lo averiguan después de meses de escribir las cosas al modo largo es probable que se amohinen y usted terminará de saborear el café de la tarde rascándoles detrás de las orejas (el café de la mañana está supuesto)

¡Diviértase con esto!

1Esto es, fechas del calendario...

Paul Evans

Paul Evans adora todo sobre la electrónica y los ordenadores en particular. Es mayor como para recordar babear con un Altair 8080A en su adolescencia. Él y sus dos hijos viven en los Wilds de la Columbia Británica del Norte; no son leñadores, pero están bien.
Copyright © 2002, Paul Evans. Licencia de copia http://www.linuxgazette.com/copying.html
Publicado en el número 83 de Linux Gazette, Octubre de 2002