Solicitar información al usuario hasta que dé una respuesta válida
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
)?
La forma más sencilla de lograr esto es poner el input
método en un bucle while. Úselo continue
cuando reciba malas entradas y break
fuera del circuito cuando esté satisfecho.
Cuando su entrada podría generar una excepción
Utilice try
yexcept
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 input
declaraciones 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 True
mé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 input
a raw_input
, pero accidentalmente cambia solo el primero input
de arriba? Es SyntaxError
só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_int
para 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!
¿ Por qué harías a 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 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".
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?
-
Esta combinación deprompts = chain(["Enter a number: "], repeat("Not a number! Try again: "))
itertools.chain
yitertools.repeat
creará 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
replies = map(input, prompts)
- aquímap
se aplicarán todas lasprompts
cadenas del paso anterior a lainput
funció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...
- Usamos
filter
ystr.isdigit
para filtrar aquellas cadenas que contienen solo dígitos:only_digits = filter(str.isdigit, replies) for reply in only_digits: print(reply)
Y para obtener solo la cadena de primeros dígitos que usamosEnter 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...
next
.
Otras reglas de validación:
Métodos de cadena: por supuesto, puede utilizar otros métodos de cadena, como
str.isalpha
obtener solo cadenas alfabéticas ostr.isupper
solo mayúsculas. Consulte los documentos para obtener la lista completa.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
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
operator
módulo.Existencia de ruta:
aquí se puede usarpathlib
la biblioteca y suPath.exists
mé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 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!
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.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 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.