不断请求用户输入直到他们提供有效响应
我正在写一个程序,让用户可以输入信息。
#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 个回答
使用 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
虽然大家都觉得那个被采纳的答案很棒,但我也想分享一个简单的小技巧来解决这个问题。(这个方法也能解决负年龄的问题。)
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的。
函数式编程方法或“看,妈妈,没有循环!”:
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
,会创建一个迭代器,它会一次性输出字符串"请输入一个数字: "
,然后无限次输出"不是数字!请再试一次: "
: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
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...
- 我们使用
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 = 双下划线),你可以随时定义自己的函数,或者使用
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
不幸的是,如果有人需要为每个失败的情况提供自定义消息,那么,我恐怕没有找到一个“好看”的函数式方法。或者至少,我找不到。
为什么要写一个 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.
这样是可行的,因为年龄的值永远是合理的,而且代码也符合你的“业务流程”的逻辑。
最简单的方法就是把 input
这个输入方法放在一个循环里。如果用户输入了不好的数据,就用 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 次错误”,但你低估了傻瓜的创造力!