Pedir al usuario que dé su opinión hasta que dé una respuesta válida

python validation loops python-3.x user-input


Estoy escribiendo un programa que acepta una entrada 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 espera siempre y cuando el usuario introduzca datos significativos.

C:\Python\Projects> canyouvote.py
Please enter your age: 23
You are able to vote in the United States!

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

C:\Python\Projects> canyouvote.py
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 estrellarse,me gustaría que el programa volviera a pedir la entrada.Así:

C:\Python\Projects> canyouvote.py
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 puedo hacer que el programa pida entradas válidas en lugar de colapsar cuando se introducen datos sin sentido?

¿Cómo puedo rechazar valores como -1 , que es un int válido , pero sin sentido en este contexto?




Answer 1 Kevin


La forma más sencilla de lograr esto sería colocar el método de input en un ciclo while. Uso continue cuando recibe una entrada incorrecta, y break fuera del circuito cuando se está satisfecho.

Cuando su entrada podría plantear una excepción

Use try and except para detectar cuándo 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.")

Implementación de sus propias reglas de validación

Si quieres rechazar los valores que Python puede analizar con éxito,puedes añadir tu 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 la gestión de excepciones y la validación personalizada

Ambas técnicas pueden combinarse en un solo ciclo.

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.")

Encapsulando todo en una función

Si necesitas pedirle a tu 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: ")

Poniendo todo junto

Puedes extender esta idea para hacer 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:
                    print(template.format(" or ".join((", ".join(map(str,
                                                                     range_[:-1])),
                                                       str(range_[-1])))))
        else:
            return ui

Con el uso 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é debe evitarlos

El uso redundante de declaraciones de input 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 método while True , pero viola el principio de desarrollo de software Don't Repeat Yourself . Esto aumenta la probabilidad de errores en su sistema. ¿Qué raw_input si desea realizar un backport a 2.7 cambiando la input a raw_input , pero accidentalmente cambia solo la primera input anterior? Es un SyntaxError esperando a que suceda.

La recursión volará tu pila

Si acaba de aprender acerca de la recursividad, puede sentirse tentado a usarla en get_non_negative_int para que pueda deshacerse del ciclo 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 RuntimeError: maximum recursion depth exceeded . Puedes pensar que "ningún tonto cometería 1000 errores seguidos", ¡pero estás subestimando el ingenio de los tontos!




Answer 2 Steven Stip


¿Por qué harías un while True y 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 parar una vez que tienes 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 resultaría en 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 negocios"




Answer 3 aaveg


Aunque la respuesta aceptada es asombrosa.También me gustaría compartir un hackeo rápido para este problema.(Esto también se ocupa del problema de la edad negativa).

f=lambda age: (age.isdigit() and ((int(age)>=18  and "Can vote" ) or "Cannot vote")) or \
f(input("invalid input. Try again\nPlease enter your age: "))
print(f(input("Please enter your age: ")))

P.D.Este código es para pitón 3.x.




Answer 4 cat


Así que,estuve jugando con algo similar a esto recientemente,y se me ocurrió la siguiente solución,que utiliza una forma de obtener la entrada que rechaza la basura,antes de que se compruebe de forma lógica.

read_single_keypress() cortesía de read_single_keypress () https://stackoverflow.com/a/6599441/4532996

def read_single_keypress() -> str:
    """Waits for a single keypress on stdin.
    -- from :: https://stackoverflow.com/a/6599441/4532996
    """

    import termios, fcntl, sys, os
    fd = sys.stdin.fileno()
    # save old state
    flags_save = fcntl.fcntl(fd, fcntl.F_GETFL)
    attrs_save = termios.tcgetattr(fd)
    # make raw - the way to do this comes from the termios(3) man page.
    attrs = list(attrs_save) # copy the stored version to update
    # iflag
    attrs[0] &= ~(termios.IGNBRK | termios.BRKINT | termios.PARMRK
                  | termios.ISTRIP | termios.INLCR | termios. IGNCR
                  | termios.ICRNL | termios.IXON )
    # oflag
    attrs[1] &= ~termios.OPOST
    # cflag
    attrs[2] &= ~(termios.CSIZE | termios. PARENB)
    attrs[2] |= termios.CS8
    # lflag
    attrs[3] &= ~(termios.ECHONL | termios.ECHO | termios.ICANON
                  | termios.ISIG | termios.IEXTEN)
    termios.tcsetattr(fd, termios.TCSANOW, attrs)
    # turn off non-blocking
    fcntl.fcntl(fd, fcntl.F_SETFL, flags_save & ~os.O_NONBLOCK)
    # read a single keystroke
    try:
        ret = sys.stdin.read(1) # returns a single character
    except KeyboardInterrupt:
        ret = 0
    finally:
        # restore old state
        termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save)
        fcntl.fcntl(fd, fcntl.F_SETFL, flags_save)
    return ret

def until_not_multi(chars) -> str:
    """read stdin until !(chars)"""
    import sys
    chars = list(chars)
    y = ""
    sys.stdout.flush()
    while True:
        i = read_single_keypress()
        _ = sys.stdout.write(i)
        sys.stdout.flush()
        if i not in chars:
            break
        y += i
    return y

def _can_you_vote() -> str:
    """a practical example:
    test if a user can vote based purely on keypresses"""
    print("can you vote? age : ", end="")
    x = int("0" + until_not_multi("0123456789"))
    if not x:
        print("\nsorry, age can only consist of digits.")
        return
    print("your age is", x, "\nYou can vote!" if x >= 18 else "Sorry! you can't vote")

_can_you_vote()

Puede encontrar el módulo completo aquí .

Example:

$ ./input_constrain.py
can you vote? age : a
sorry, age can only consist of digits.
$ ./input_constrain.py 
can you vote? age : 23<RETURN>
your age is 23
You can vote!
$ _

Tenga en cuenta que la naturaleza de esta implementación es que cierra stdin tan pronto como se lee algo que no es un dígito. No presioné enter después de a , pero necesitaba después de los números.

Puede fusionar esto con la función thismany() en el mismo módulo para permitir solo, digamos, tres dígitos.




Answer 5 Georgy


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 "mala entrada" separado de un aviso 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

¿Cómo funciona?

  1. prompts = chain(["Enter a number: "], repeat("Not a number! Try again: "))
    Esta combinación de itertools.chain y itertools.repeat creará un iterador que producirá 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í map aplicará todas las cadenas de prompts del paso anterior a la función de input . 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 filter y str.isdigit para 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 primera cadena de solo dígitos que usamos a next .

Otras reglas de validación:

  1. Métodos de cadena: por supuesto, puede usar otros métodos de cadena como str.isalpha para obtener solo cadenas alfabéticas o str.isupper para obtener solo mayúsculas. Ver documentos para la lista completa.

  2. Prueba de membresía:
    Hay varias formas diferentes de realizarlo. Uno de ellos es mediante el método __contains__ :

    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 usar 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 le gusta usar los métodos dunder (dunder = double-subrayado), siempre puede definir su propia función o usar las del módulo del operator .

  4. Existencia del camino:
    Aquí se puede usar la biblioteca pathlib y su método Path.exists :

    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

Limitando el número de intentos:

Si no desea torturar a un usuario preguntándole algo un número infinito de veces, puede especificar un límite en una llamada de itertools.repeat . Esto se puede combinar con proporcionar un valor predeterminado a la next funció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!

Preprocesando los datos de entrada:

A veces no queremos rechazar una entrada si el usuario la suministró 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 los métodos str.lower y 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 para 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 las reglas de validación:

Para un caso simple, por ejemplo, cuando el programa solicita una edad entre 1 y 120, uno puede agregar otro 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)

Pero en el caso de que haya muchas reglas, es mejor implementar una función que realice una conjunción lógica . En el siguiente ejemplo, usaré uno listo desde aquí :

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

Desafortunadamente, si alguien necesita un mensaje personalizado para cada caso fallido, me temo que no hay una manera bastante funcional. O, al menos, no pude encontrar uno.