有効な回答が得られるまでユーザーに入力を求める

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!

非センシカルなデータが入力されたときにクラッシュするのではなく、有効な入力を求めるようにするにはどうすればいいですか?

有効な int ですが、このコンテキストでは無意味な -1 などの値を拒否するにはどうすればよいですか?




Answer 1 Kevin


これを行う最も簡単な方法は、 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

例外処理とカスタムバリデーションの組み合わせ

上記のテクニックはどちらも1つのループにまとめることができます。

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:
                    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 メソッドよりも短いため、最初は魅力的に見えるかもしれませんが、ソフトウェア開発の原則を繰り返さないことに違反しています。これにより、システムにバグが発生する可能性が高くなります。 inputraw_input に変更して2.7にバックポートしたいが、誤って上の最初の 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: ")))

追伸:このコードは python 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!
$ _

この実装の性質は、数字ではない何かが読み取られるとすぐにstdinを閉じることに注意してください。私は後に入力してヒットしませんでしたが、私は数字の後に必要としていました。 a

これを同じモジュールの thismany() 関数とマージして、たとえば 3桁のみを許可することができます。




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. メンバーシップテスト:
    それを実行するには、いくつかの異なる方法があります。それらの1つは __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メソッド(dunder = double-underscore)を使用したくない場合は、いつでも独自の関数を定義するか、 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!

入力データの前処理を行います。

ユーザーが誤って提供されている場合時々 、入力を拒否したくないCAPS INまたは文字列の先頭または末尾にスペースを。これらの単純な間違いを考慮に入れる str.lower str.strip メソッドと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

残念ながら、失敗したケースごとにカスタムメッセージが必要な場合、恐らく、かなり機能的な方法はありません。または、少なくとも、私はそれを見つけることができませんでした。