Solicitar información al usuario hasta que dé una respuesta válida

Resuelto Kevin asked hace 10 años • 22 respuestas

Estoy escribiendo un programa que acepta entradas del usuario.

#note: Python 2.7 users should use `raw_input`, the equivalent of 3.X's `input`
age = int(input("Please enter your age: "))
if age >= 18: 
    print("You are able to vote in the United States!")
else:
    print("You are not able to vote in the United States.")

El programa funciona como se esperaba siempre que el usuario ingrese datos significativos.

Please enter your age: 23
You are able to vote in the United States!

Pero falla si el usuario ingresa datos no válidos:

Please enter your age: dickety six
Traceback (most recent call last):
  File "canyouvote.py", line 1, in <module>
    age = int(input("Please enter your age: "))
ValueError: invalid literal for int() with base 10: 'dickety six'

En lugar de fallar, me gustaría que el programa volviera a solicitar la información. Como esto:

Please enter your age: dickety six
Sorry, I didn't understand that.
Please enter your age: 26
You are able to vote in the United States!

¿Cómo solicito una entrada válida en lugar de fallar o aceptar valores no válidos (por ejemplo -1)?

Kevin avatar Apr 25 '14 20:04 Kevin
Aceptado

La forma más sencilla de lograr esto es poner el inputmétodo en un bucle while. Úselo continuecuando reciba malas entradas y breakfuera del circuito cuando esté satisfecho.

Cuando su entrada podría generar una excepción

Utilice tryyexcept para detectar cuando el usuario ingresa datos que no se pueden analizar.

while True:
    try:
        # Note: Python 2.x users should use raw_input, the equivalent of 3.x's input
        age = int(input("Please enter your age: "))
    except ValueError:
        print("Sorry, I didn't understand that.")
        #better try again... Return to the start of the loop
        continue
    else:
        #age was successfully parsed!
        #we're ready to exit the loop.
        break
if age >= 18: 
    print("You are able to vote in the United States!")
else:
    print("You are not able to vote in the United States.")

Implementar sus propias reglas de validación

Si desea rechazar valores que Python puede analizar correctamente, puede agregar su propia lógica de validación.

while True:
    data = input("Please enter a loud message (must be all caps): ")
    if not data.isupper():
        print("Sorry, your response was not loud enough.")
        continue
    else:
        #we're happy with the value given.
        #we're ready to exit the loop.
        break

while True:
    data = input("Pick an answer from A to D:")
    if data.lower() not in ('a', 'b', 'c', 'd'):
        print("Not an appropriate choice.")
    else:
        break

Combinando manejo de excepciones y validación personalizada

Ambas técnicas anteriores se pueden combinar en un solo bucle.

while True:
    try:
        age = int(input("Please enter your age: "))
    except ValueError:
        print("Sorry, I didn't understand that.")
        continue

    if age < 0:
        print("Sorry, your response must not be negative.")
        continue
    else:
        #age was successfully parsed, and we're happy with its value.
        #we're ready to exit the loop.
        break
if age >= 18: 
    print("You are able to vote in the United States!")
else:
    print("You are not able to vote in the United States.")

Encapsulándolo todo en una función

Si necesita pedirle a su usuario muchos valores diferentes, puede ser útil poner este código en una función, para no tener que volver a escribirlo cada vez.

def get_non_negative_int(prompt):
    while True:
        try:
            value = int(input(prompt))
        except ValueError:
            print("Sorry, I didn't understand that.")
            continue

        if value < 0:
            print("Sorry, your response must not be negative.")
            continue
        else:
            break
    return value

age = get_non_negative_int("Please enter your age: ")
kids = get_non_negative_int("Please enter the number of children you have: ")
salary = get_non_negative_int("Please enter your yearly earnings, in dollars: ")

Poniendolo todo junto

Puedes ampliar esta idea para crear una función de entrada muy genérica:

def sanitised_input(prompt, type_=None, min_=None, max_=None, range_=None):
    if min_ is not None and max_ is not None and max_ < min_:
        raise ValueError("min_ must be less than or equal to max_.")
    while True:
        ui = input(prompt)
        if type_ is not None:
            try:
                ui = type_(ui)
            except ValueError:
                print("Input type must be {0}.".format(type_.__name__))
                continue
        if max_ is not None and ui > max_:
            print("Input must be less than or equal to {0}.".format(max_))
        elif min_ is not None and ui < min_:
            print("Input must be greater than or equal to {0}.".format(min_))
        elif range_ is not None and ui not in range_:
            if isinstance(range_, range):
                template = "Input must be between {0.start} and {0.stop}."
                print(template.format(range_))
            else:
                template = "Input must be {0}."
                if len(range_) == 1:
                    print(template.format(*range_))
                else:
                    expected = " or ".join((
                        ", ".join(str(x) for x in range_[:-1]),
                        str(range_[-1])
                    ))
                    print(template.format(expected))
        else:
            return ui

Con usos como:

age = sanitised_input("Enter your age: ", int, 1, 101)
answer = sanitised_input("Enter your answer: ", str.lower, range_=('a', 'b', 'c', 'd'))

Errores comunes y por qué debería evitarlos

El uso redundante de inputdeclaraciones redundantes

Este método funciona, pero generalmente se considera de mal estilo:

data = input("Please enter a loud message (must be all caps): ")
while not data.isupper():
    print("Sorry, your response was not loud enough.")
    data = input("Please enter a loud message (must be all caps): ")

Puede parecer atractivo inicialmente porque es más corto que el while Truemétodo, pero viola el principio de no repetirse del desarrollo de software. Esto aumenta la probabilidad de que se produzcan errores en su sistema. ¿Qué sucede si desea realizar una copia de seguridad a 2.7 cambiando inputa raw_input, pero accidentalmente cambia solo el primero inputde arriba? Es SyntaxErrorsólo una espera que suceda.

La recursividad arruinará tu pila

Si acaba de aprender sobre la recursividad, es posible que tenga la tentación de utilizarla get_non_negative_intpara poder deshacerse del bucle while.

def get_non_negative_int(prompt):
    try:
        value = int(input(prompt))
    except ValueError:
        print("Sorry, I didn't understand that.")
        return get_non_negative_int(prompt)

    if value < 0:
        print("Sorry, your response must not be negative.")
        return get_non_negative_int(prompt)
    else:
        return value

Esto parece funcionar bien la mayor parte del tiempo, pero si el usuario ingresa datos no válidos suficientes veces, el script terminará con un archivo RuntimeError: maximum recursion depth exceeded. Quizás pienses que "ningún tonto cometería 1000 errores seguidos", ¡pero estás subestimando el ingenio de los tontos!

Kevin avatar Apr 25 '2014 13:04 Kevin

¿ Por qué harías a while Truey luego saldrías de este ciclo mientras también puedes poner tus requisitos en la declaración while ya que todo lo que quieres es detenerte una vez que tengas la edad?

age = None
while age is None:
    input_value = input("Please enter your age: ")
    try:
        # try and convert the string input to a number
        age = int(input_value)
    except ValueError:
        # tell the user off
        print("{input} is not a number, please enter a number only".format(input=input_value))
if age >= 18:
    print("You are able to vote in the United States!")
else:
    print("You are not able to vote in the United States.")

Esto daría como resultado lo siguiente:

Please enter your age: *potato*
potato is not a number, please enter a number only
Please enter your age: *5*
You are not able to vote in the United States.

Esto funcionará ya que la edad nunca tendrá un valor que no tenga sentido y el código sigue la lógica de su "proceso de negocio".

Steven Stip avatar Jan 14 '2016 12:01 Steven Stip

Enfoque funcional o " ¡ mira mamá, sin bucles! ":

from itertools import chain, repeat

prompts = chain(["Enter a number: "], repeat("Not a number! Try again: "))
replies = map(input, prompts)
valid_response = next(filter(str.isdigit, replies))
print(valid_response)
Enter a number:  a
Not a number! Try again:  b
Not a number! Try again:  1
1

o si desea tener un mensaje de "entrada incorrecta" separado de un mensaje de entrada como en otras respuestas:

prompt_msg = "Enter a number: "
bad_input_msg = "Sorry, I didn't understand that."
prompts = chain([prompt_msg], repeat('\n'.join([bad_input_msg, prompt_msg])))
replies = map(input, prompts)
valid_response = next(filter(str.isdigit, replies))
print(valid_response)
Enter a number:  a
Sorry, I didn't understand that.
Enter a number:  b
Sorry, I didn't understand that.
Enter a number:  1
1

¿Como funciona?

  1. prompts = chain(["Enter a number: "], repeat("Not a number! Try again: "))
    
    Esta combinación de itertools.chainy itertools.repeatcreará un iterador que generará cadenas "Enter a number: "una vez y "Not a number! Try again: "un número infinito de veces:
    for prompt in prompts:
        print(prompt)
    
    Enter a number: 
    Not a number! Try again: 
    Not a number! Try again: 
    Not a number! Try again: 
    # ... and so on
    
  2. replies = map(input, prompts)- aquí mapse aplicarán todas las promptscadenas del paso anterior a la inputfunción. P.ej:
    for reply in replies:
        print(reply)
    
    Enter a number:  a
    a
    Not a number! Try again:  1
    1
    Not a number! Try again:  it doesn't care now
    it doesn't care now
    # and so on...
    
  3. Usamos filtery str.isdigitpara filtrar aquellas cadenas que contienen solo dígitos:
    only_digits = filter(str.isdigit, replies)
    for reply in only_digits:
        print(reply)
    
    Enter a number:  a
    Not a number! Try again:  1
    1
    Not a number! Try again:  2
    2
    Not a number! Try again:  b
    Not a number! Try again: # and so on...
    
    Y para obtener solo la cadena de primeros dígitos que usamos next.

Otras reglas de validación:

  1. Métodos de cadena: por supuesto, puede utilizar otros métodos de cadena, como str.isalphaobtener solo cadenas alfabéticas o str.isuppersolo mayúsculas. Consulte los documentos para obtener la lista completa.

  2. Prueba de membresía:
    hay varias formas diferentes de realizarla. Uno de ellos es mediante el uso __contains__del método:

    from itertools import chain, repeat
    
    fruits = {'apple', 'orange', 'peach'}
    prompts = chain(["Enter a fruit: "], repeat("I don't know this one! Try again: "))
    replies = map(input, prompts)
    valid_response = next(filter(fruits.__contains__, replies))
    print(valid_response)
    
    Enter a fruit:  1
    I don't know this one! Try again:  foo
    I don't know this one! Try again:  apple
    apple
    
  3. Comparación de números:
    existen métodos de comparación útiles que podemos utilizar aquí. Por ejemplo, para __lt__( <):

    from itertools import chain, repeat
    
    prompts = chain(["Enter a positive number:"], repeat("I need a positive number! Try again:"))
    replies = map(input, prompts)
    numeric_strings = filter(str.isnumeric, replies)
    numbers = map(float, numeric_strings)
    is_positive = (0.).__lt__
    valid_response = next(filter(is_positive, numbers))
    print(valid_response)
    
    Enter a positive number: a
    I need a positive number! Try again: -5
    I need a positive number! Try again: 0
    I need a positive number! Try again: 5
    5.0
    

    O, si no te gusta usar métodos dunder (dunder = doble guión bajo), siempre puedes definir tu propia función o usar las del operatormódulo.

  4. Existencia de ruta:
    aquí se puede usar pathlibla biblioteca y su Path.existsmétodo:

    from itertools import chain, repeat
    from pathlib import Path
    
    prompts = chain(["Enter a path: "], repeat("This path doesn't exist! Try again: "))
    replies = map(input, prompts)
    paths = map(Path, replies)
    valid_response = next(filter(Path.exists, paths))
    print(valid_response)
    
    Enter a path:  a b c
    This path doesn't exist! Try again:  1
    This path doesn't exist! Try again:  existing_file.txt
    existing_file.txt
    

Limitar el número de intentos:

Si no quieres torturar a un usuario preguntándole algo un número infinito de veces, puedes especificar un límite en una llamada de itertools.repeat. Esto se puede combinar con proporcionar un valor predeterminado a la nextfunción:

from itertools import chain, repeat

prompts = chain(["Enter a number:"], repeat("Not a number! Try again:", 2))
replies = map(input, prompts)
valid_response = next(filter(str.isdigit, replies), None)
print("You've failed miserably!" if valid_response is None else 'Well done!')
Enter a number: a
Not a number! Try again: b
Not a number! Try again: c
You've failed miserably!

Preprocesamiento de datos de entrada:

A veces no queremos rechazar una entrada si el usuario la proporcionó accidentalmente EN MAYÚSCULAS o con un espacio al principio o al final de la cadena. Para tener en cuenta estos errores simples, podemos preprocesar los datos de entrada aplicando métodos str.lowery str.strip. Por ejemplo, para el caso de prueba de membresía, el código se verá así:

from itertools import chain, repeat

fruits = {'apple', 'orange', 'peach'}
prompts = chain(["Enter a fruit: "], repeat("I don't know this one! Try again: "))
replies = map(input, prompts)
lowercased_replies = map(str.lower, replies)
stripped_replies = map(str.strip, lowercased_replies)
valid_response = next(filter(fruits.__contains__, stripped_replies))
print(valid_response)
Enter a fruit:  duck
I don't know this one! Try again:     Orange
orange

En el caso de que tenga muchas funciones para usar en el preprocesamiento, podría ser más fácil usar una función que realice una composición de funciones . Por ejemplo, usando el de aquí :

from itertools import chain, repeat

from lz.functional import compose

fruits = {'apple', 'orange', 'peach'}
prompts = chain(["Enter a fruit: "], repeat("I don't know this one! Try again: "))
replies = map(input, prompts)
process = compose(str.strip, str.lower)  # you can add more functions here
processed_replies = map(process, replies)
valid_response = next(filter(fruits.__contains__, processed_replies))
print(valid_response)
Enter a fruit:  potato
I don't know this one! Try again:   PEACH
peach

Combinando reglas de validación:

Para un caso simple, por ejemplo, cuando el programa solicita una edad entre 1 y 120, simplemente se puede agregar otra filter:

from itertools import chain, repeat

prompt_msg = "Enter your age (1-120): "
bad_input_msg = "Wrong input."
prompts = chain([prompt_msg], repeat('\n'.join([bad_input_msg, prompt_msg])))
replies = map(input, prompts)
numeric_replies = filter(str.isdigit, replies)
ages = map(int, numeric_replies)
positive_ages = filter((0).__lt__, ages)
not_too_big_ages = filter((120).__ge__, positive_ages)
valid_response = next(not_too_big_ages)
print(valid_response)

But in the case when there are many rules, it's better to implement a function performing a logical conjunction. In the following example I will use a ready one from here:

from functools import partial
from itertools import chain, repeat

from lz.logical import conjoin


def is_one_letter(string: str) -> bool:
    return len(string) == 1


rules = [str.isalpha, str.isupper, is_one_letter, 'C'.__le__, 'P'.__ge__]

prompt_msg = "Enter a letter (C-P): "
bad_input_msg = "Wrong input."
prompts = chain([prompt_msg], repeat('\n'.join([bad_input_msg, prompt_msg])))
replies = map(input, prompts)
valid_response = next(filter(conjoin(*rules), replies))
print(valid_response)
Enter a letter (C-P):  5
Wrong input.
Enter a letter (C-P):  f
Wrong input.
Enter a letter (C-P):  CDE
Wrong input.
Enter a letter (C-P):  Q
Wrong input.
Enter a letter (C-P):  N
N

Unfortunately, if someone needs a custom message for each failed case, then, I'm afraid, there is no pretty functional way. Or, at least, I couldn't find one.

Georgy avatar May 10 '2019 16:05 Georgy