我正在编写一个接受用户输入的程序。

#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)?


当前回答

使用点击:

Click是一个命令行界面库,它提供了向用户请求有效响应的功能。

简单的例子:

import click

number = click.prompt('Please enter a number', type=float)
print(number)
Please enter a number: 
 a
Error: a is not a valid floating point value
Please enter a number: 
 10
10.0

注意它是如何自动将字符串值转换为浮点数的。

检查一个值是否在一个范围内:

提供了不同的自定义类型。为了得到一个特定范围内的数字,我们可以使用IntRange:

age = click.prompt("What's your age?", type=click.IntRange(1, 120))
print(age)
What's your age?: 
 a
Error: a is not a valid integer
What's your age?: 
 0
Error: 0 is not in the valid range of 1 to 120.
What's your age?: 
 5
5

我们也可以只指定一个极限,min或max:

age = click.prompt("What's your age?", type=click.IntRange(min=14))
print(age)
What's your age?: 
 0
Error: 0 is smaller than the minimum valid value 14.
What's your age?: 
 18
18

会员测试:

使用点击。选择类型。默认情况下,该检查是区分大小写的。

choices = {'apple', 'orange', 'peach'}
choice = click.prompt('Provide a fruit', type=click.Choice(choices, case_sensitive=False))
print(choice)
Provide a fruit (apple, peach, orange): 
 banana
Error: invalid choice: banana. (choose from apple, peach, orange)
Provide a fruit (apple, peach, orange): 
 OrAnGe
orange

使用路径和文件:

使用点击。路径类型,我们可以检查现有的路径并解析它们:

path = click.prompt('Provide path', type=click.Path(exists=True, resolve_path=True))
print(path)
Provide path: 
 nonexistent
Error: Path "nonexistent" does not exist.
Provide path: 
 existing_folder
'/path/to/existing_folder

文件的读写可以通过点击完成。文件:

file = click.prompt('In which file to write data?', type=click.File('w'))
with file.open():
    file.write('Hello!')
# More info about `lazy=True` at:
# https://click.palletsprojects.com/en/7.x/arguments/#file-opening-safety
file = click.prompt('Which file you wanna read?', type=click.File(lazy=True))
with file.open():
    print(file.read())
In which file to write data?: 
         # <-- provided an empty string, which is an illegal name for a file
In which file to write data?: 
 some_file.txt
Which file you wanna read?: 
 nonexistent.txt
Error: Could not open file: nonexistent.txt: No such file or directory
Which file you wanna read?: 
 some_file.txt
Hello!

其他的例子:

密码确认:

password = click.prompt('Enter password', hide_input=True, confirmation_prompt=True)
print(password)
Enter password: 
 ······
Repeat for confirmation: 
 ·
Error: the two entered values do not match
Enter password: 
 ······
Repeat for confirmation: 
 ······
qwerty

默认值:

在这种情况下,只需按Enter(或任何你使用的键)而不输入值,就会给你一个默认值:

number = click.prompt('Please enter a number', type=int, default=42)
print(number)
Please enter a number [42]: 
 a
Error: a is not a valid integer
Please enter a number [42]: 
 
42

其他回答

最简单的方法是将输入法放入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.")

把它封装在一个函数中

如果您需要向用户询问许多不同的值,那么将这些代码放在函数中可能会很有用,这样您就不必每次都重新键入。

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

常见的陷阱,以及为什么你应该避免它们

冗余输入语句的冗余使用

这种方法有效,但通常被认为是糟糕的风格:

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方法更短,但它违反了软件开发的“不要重复自己”原则。这增加了系统中出现错误的可能性。如果您想通过将输入更改为raw_input来向后移植到2.7,但不小心只更改了上面的第一个输入,该怎么办?这是一个等待发生的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终止:超出最大递归深度。你可能认为“没有傻瓜会连续犯1000个错误”,但你低估了傻瓜的聪明才智!

您总是可以应用简单的if-else逻辑,并在for循环的同时向代码中添加一个if逻辑。

while True:
     age = int(input("Please enter your age: "))
     if (age >= 18)  : 
         print("You are able to vote in the United States!")
     if (age < 18) & (age > 0):
         print("You are not able to vote in the United States.")
     else:
         print("Wrong characters, the input must be numeric")
         continue

这将是一个无限的厕所,你将被要求进入一个无限的时代。

所以,我最近在搞一些类似的事情,我想出了下面的解决方案,它使用一种方法来获取输入,拒绝垃圾,甚至在它以任何逻辑方式检查之前。

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

您可以在这里找到完整的模块。

例子:

$ ./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()函数合并,以只允许(比如说)三位数字。

使用try-except来处理错误并重复一次:

while True:
    try:
        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.")
    except Exception as e:
        print("please enter number")

基于Daniel Q和Patrick Artner的优秀建议, 这里有一个更普遍的解决方案。

# Assuming Python3
import sys

class ValidationError(ValueError):  # thanks Patrick Artner
    pass

def validate_input(prompt, cast=str, cond=(lambda x: True), onerror=None):
    if onerror==None: onerror = {}
    while True:
        try:
            data = cast(input(prompt))
            if not cond(data): raise ValidationError
            return data
        except tuple(onerror.keys()) as e:  # thanks Daniel Q
            print(onerror[type(e)], file=sys.stderr)

我选择了显式的if和raise语句,而不是assert, 因为断言检查可能被关闭, 而验证应始终开启以提供健壮性。

这可以用来获得不同种类的输入, 使用不同的验证条件。 例如:

# No validation, equivalent to simple input:
anystr = validate_input("Enter any string: ")

# Get a string containing only letters:
letters = validate_input("Enter letters: ",
    cond=str.isalpha,
    onerror={ValidationError: "Only letters, please!"})

# Get a float in [0, 100]:
percentage = validate_input("Percentage? ",
    cast=float, cond=lambda x: 0.0<=x<=100.0,
    onerror={ValidationError: "Must be between 0 and 100!",
             ValueError: "Not a number!"})

或者,回答最初的问题:

age = validate_input("Please enter your age: ",
        cast=int, cond=lambda a:0<=a<150,
        onerror={ValidationError: "Enter a plausible age, please!",
                 ValueError: "Enter an integer, please!"})
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.")