不断请求用户输入直到他们提供有效响应

758 投票
22 回答
1044342 浏览
提问于 2025-04-18 04:14

我正在写一个程序,让用户可以输入信息。

#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)时,重新请求有效输入,而不是崩溃或接受无效值呢?

22 个回答

23

使用 Click

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

成员资格测试:

使用 click.Choice 类型。默认情况下,这个检查是区分大小写的。

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

处理路径和文件:

使用 click.Path 类型,我们可以检查路径是否存在,并且可以解析这些路径:

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

读取和写入文件可以通过 click.File 来完成:

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
30

虽然大家都觉得那个被采纳的答案很棒,但我也想分享一个简单的小技巧来解决这个问题。(这个方法也能解决负年龄的问题。)

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的。

42

函数式编程方法或“看,妈妈,没有循环!”:

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.chainitertools.repeat,会创建一个迭代器,它会一次性输出字符串"请输入一个数字: ",然后无限次输出"不是数字!请再试一次: "
    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. 我们使用filterstr.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!

预处理输入数据:

有时候我们不想拒绝用户的输入,比如用户不小心输入了全大写字母,或者字符串前后有空格。为了考虑这些简单的错误,我们可以通过应用str.lowerstr.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

不幸的是,如果有人需要为每个失败的情况提供自定义消息,那么,我恐怕没有找到一个“好看”的函数式方法。或者至少,我找不到。

63

为什么要写一个 while True 的循环,然后再在里面用 break 跳出这个循环呢?其实你可以直接把条件放在 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.

这样是可行的,因为年龄的值永远是合理的,而且代码也符合你的“业务流程”的逻辑。

995

最简单的方法就是把 input 这个输入方法放在一个循环里。如果用户输入了不好的数据,就用 continue 跳过这次输入,等到输入满意了就用 break 跳出循环。

当你的输入可能引发异常时

可以使用 tryexcept 来检测用户输入的数据是否无法解析。

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 次错误”,但你低估了傻瓜的创造力!

撰写回答