Запрашивает у пользователя ввод до тех пор, пока он не даст допустимый ответ
Я пишу программу, которая принимает вводимые пользователем данные.
#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.")
Программа работает должным образом, пока пользователь вводит значимые данные.
Please enter your age: 23
You are able to vote in the United States!
Но это не удается, если пользователь вводит неверные данные:
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'
Вместо сбоя я бы хотел, чтобы программа снова запросила ввод. Вот так:
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!
Как мне запросить корректный ввод вместо сбоя или принятия недопустимых значений (например, -1
)?
Переведено автоматически
Ответ 1
Самый простой способ добиться этого - поместить input
метод в цикл while . Используйте continue
, когда получаете неверные входные данные, и break
завершите цикл, когда будете удовлетворены.
Когда ваш ввод может вызвать исключение
Используйте try
и except
для определения того, когда пользователь вводит данные, которые не могут быть проанализированы.
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.")
Реализация ваших собственных правил проверки
Если вы хотите отклонить значения, которые Python может успешно проанализировать, вы можете добавить свою собственную логику проверки.
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
Сочетание обработки исключений и пользовательской проверки
Оба вышеперечисленных метода могут быть объединены в один цикл.
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.")
Инкапсулирование всего этого в функцию
Если вам нужно запросить у пользователя много разных значений, может быть полезно поместить этот код в функцию, чтобы вам не приходилось вводить его каждый раз заново.
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: ")
Собираем все это вместе
Вы можете расширить эту идею, создав очень универсальную функцию ввода:
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
С использованием, таким как:
age = sanitised_input("Enter your age: ", int, 1, 101)
answer = sanitised_input("Enter your answer: ", str.lower, range_=('a', 'b', 'c', 'd'))
Распространенные подводные камни и почему их следует избегать
Избыточное использование избыточных input
операторов
Этот метод работает, но обычно считается плохим стилем:
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): ")
Изначально это может выглядеть привлекательно, потому что оно короче, чем while True
метод, но оно нарушает принцип разработки программного обеспечения "Не повторяйся". Это увеличивает вероятность ошибок в вашей системе. Что, если вы хотите выполнить резервное копирование в 2.7, изменив input
на raw_input
, но случайно измените только первое input
выше? Это SyntaxError
только и ждет, чтобы это произошло.
Рекурсия взорвет ваш стек
Если вы только что узнали о рекурсии, у вас может возникнуть соблазн использовать ее в get_non_negative_int
, чтобы избавиться от цикла 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
Похоже, что большую часть времени это работает нормально, но если пользователь вводит неверные данные достаточное количество раз, скрипт завершится с RuntimeError: maximum recursion depth exceeded
. Вы можете подумать, что "ни один дурак не допустит 1000 ошибок подряд", но вы недооцениваете изобретательность дураков!
Ответ 2
Зачем вам выполнять while True
, а затем выходить из этого цикла, в то время как вы также можете просто указать свои требования в операторе while, поскольку все, что вы хотите, это остановиться, как только у вас будет возраст?
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.")
Это приведет к следующему:
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.
это сработает, поскольку возраст никогда не будет иметь значения, которое не будет иметь смысла, и код следует логике вашего "бизнес-процесса"
Ответ 3
Функциональный подход или "смотри, чтобы не было циклов!":
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
или если вы хотите, чтобы сообщение "неверный ввод" было отделено от приглашения ввода, как в других ответах:
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
Как это работает?
prompts = chain(["Enter a number: "], repeat("Not a number! Try again: "))
Эта комбинацияitertools.chain
иitertools.repeat
создаст итератор
который будет выдавать строки"Enter a number: "
один раз и"Not a number! Try again: "
бесконечное количество раз: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 onreplies = map(input, prompts)
- здесьmap
будут применены всеprompts
строки из предыдущего шага кinput
функции. Например.: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...- Мы используем
filter
иstr.isdigit
для фильтрации тех строк, которые содержат только цифры: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...
И чтобы получить строку только из первых цифр, мы используемnext
.
Другие правила проверки:
Строковые методы: Конечно, вы можете использовать другие строковые методы, такие как
str.isalpha
для получения только буквенных строк илиstr.isupper
для получения только прописных. Полный список смотрите в документах.Тестирование членства:
Есть несколько различных способов выполнить это. Один из них - с помощью__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Сравнение чисел:
Существуют полезные методы сравнения, которые мы можем использовать здесь. Например, для__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Или, если вам не нравится использовать методы dunder (dunder = двойное подчеркивание), вы всегда можете определить свою собственную функцию или использовать те, что из
operator
модуль.Существование пути:
Здесь можно использоватьpathlib
библиотеку и ее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
Ограниченное количество попыток:
Если вы не хотите мучить пользователя, задавая ему что-либо бесконечное количество раз, вы можете указать ограничение в вызове itertools.repeat
. Это можно комбинировать с предоставлением функции next
значения по умолчанию:
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!
Предварительная обработка входных данных:
Иногда мы не хотим отклонять ввод, если пользователь случайно ввел его ЗАГЛАВНЫМИ БУКВАМИ или с пробелом в начале или конце строки. Чтобы учесть эти простые ошибки, мы можем предварительно обработать входные данные, применив методы str.lower
и str.strip
. Например, для случая тестирования членства код будет выглядеть следующим образом:
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
В случае, когда у вас есть много функций для предварительной обработки, может быть проще использовать функцию, выполняющую композицию функций. Например, используя тот, что находится здесь:
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
Комбинирование правил проверки:
Для простого случая, например, когда программа запрашивает возраст от 1 до 120 лет, можно просто добавить другой 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)
Но в случае, когда существует много правил, лучше реализовать функцию, выполняющую логическую конъюнкцию. В следующем примере я буду использовать готовый из здесь:
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
К сожалению, если кому-то нужно пользовательское сообщение для каждого неудачного случая, то, боюсь, нет красивого функционального способа. Или, по крайней мере, я не смог его найти.
Ответ 4
Хотя принятый ответ потрясающий. Я также хотел бы поделиться быстрым взломом для решения этой проблемы. (Это также решает проблему отрицательного возраста.)
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.S. Этот код предназначен для python 3.x.