Попросить пользователя ввести данные до тех пор,пока он не даст правильный ответ.

python validation loops python-3.x user-input


Я пишу программу,которая принимает входные данные от пользователя.

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

Программа работает так,как ожидается,до тех пор,пока пользователь вводит значимые данные.

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

Но это не удается,если пользователь вводит недействительные данные:

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'

Вместо того,чтобы ломаться,я бы хотел,чтобы программа снова попросила ввести данные.Вот так:

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!

Как заставить программу запрашивать действительные входы вместо сбоя при вводе несенсорных данных?

Как я могу отклонить значения, такие как -1 , который является допустимым int , но бессмысленным в этом контексте?





Answer 1 Kevin


Самый простой способ сделать это - поместить метод input в цикл while. Используйте « continue когда вы получаете неправильный ввод, и выходите из цикла, когда вы удовлетворены.

Когда твой вход может увеличить исключение.

Используйте 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.")

Encapsulating it All in a Function

Если вам нужно попросить у пользователя много разных значений,то,возможно,было бы полезно поместить этот код в функцию,чтобы вам не приходилось каждый раз перепечатывать его заново.

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:
                    print(template.format(" or ".join((", ".join(map(str,
                                                                     range_[:-1])),
                                                       str(range_[-1])))))
        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 ошибок подряд», но вы недооцениваете изобретательность дураков!




Answer 2 Steven Stip


Зачем вам делать некоторое 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.

это сработает,так как возраст никогда не будет иметь значения,которое не будет иметь смысла,и код следует логике вашего "бизнес-процесса".




Answer 3 aaveg


Хотя принятый ответ поразителен.Я также хотел бы поделиться быстрым взломом для этой проблемы.(Это также решает проблему с отрицательным возрастом.)

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.Этот код для питона 3.x.




Answer 4 cat


Итак,недавно я подшучивал над чем-то подобным,и придумал следующее решение,использующее способ получения входных данных,который отвергает мусор,еще до того,как он будет проверен каким-либо логическим способом.

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()

Вы можете найти полный модуль здесь .

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!
$ _

Обратите внимание, что природа этой реализации заключается в том, что она закрывает стандартный ввод, как только что-то, что не является цифрой, читается. Я не нажимал ввод после a , но мне нужно было после чисел.

Вы можете объединить это с thismany() в том же модуле, чтобы разрешить, скажем, только три цифры.




Answer 5 Georgy


Функциональный подход или " смотри мама без петель! "

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

Как это работает?

  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 on
  2. replies = 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...
  3. Мы используем 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 .

Другие правила проверки:

  1. Строковые методы: Конечно, вы можете использовать другие строковые методы, такие как str.isalpha , чтобы получить только буквенные строки, или str.isupper , чтобы получить только заглавные буквы. Смотрите документы для полного списка.

  2. Тестирование членства:
    Есть несколько разных способов сделать это. Одним из них является использование метода __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. Сравнение чисел:
    Есть полезные методы сравнения, которые мы можем использовать здесь. Например, для __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 = двойное подчеркивание), вы всегда можете определить свои собственные функции или использовать функции из модуля operator .

  4. Путь существования:
    Здесь можно использовать библиотеку 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!

Предварительная обработка входных данных:

Иногда мы не хотим отклонять ввод, если пользователь случайно предоставил его IN CAPS или с пробелом в начале или конце строки. Чтобы учесть эти простые ошибки, мы можем предварительно обработать входные данные, применяя 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

К сожалению, если кому-то нужно индивидуальное сообщение для каждого неудачного случая, то, боюсь, не существует достаточно функционального способа. Или, по крайней мере, я не мог найти один.