Earlier today I answered Timetable viewer, where one of my recommendations was to make a get_input
function.
However, this hasn't been the first time I've recommended making a bespoke get_input
function.
This function should get user input, until it is valid. This may be any input, or any number in a specific range. The arguments customize how it works, such as working with strings or integers. The function should repeatedly prompt the user for specific input, until a valid input is entered. In the case case of valid input we should return that data, so the end user can use it.
Since it's quite a common function, with a lot of uses, I decided to make a generic version of the function, that should cover most use cases. I decided to make two functions, as strings don't need to be converted, however when changing to numbers, you'd have to duplicate a lot of arguments.
def build_input(
prompt=None,
*,
errors=None,
values=None,
process=None,
wanted=None,
end=None
):
prompt = prompt or ''
process = process or (lambda v: v)
errors = errors or tuple()
if values is None:
wanted = wanted or (lambda v: True)
else:
values = list(values)
prompt += '[{}] '.format('/'.join(str(i) for i in values))
wanted = wanted or (lambda v: v in values)
values = set(values)
if end is not None:
prompt += end
elif values:
prompt += '\n> '
def ask():
while True:
value = input(prompt)
try:
value = process(value)
except errors:
continue
if wanted(value):
return value
return ask
def build_number_input(
prompt=None,
*,
errors=ValueError,
values=None,
process=int,
wanted=None,
end=None
):
return build_input(
prompt,
errors=errors,
values=values,
process=process,
wanted=wanted,
end=end
)
I have a couple of concerns:
build_number_input
spans 17 lines, for a function definition and a function call. Is there a better way to define this?- Are there a better way to style my function definitions? I think they may not be PEP8 compliant, but couldn't think of anything nicer to read.
- Are there any common options that you think should be added?
Below are a couple of ways to use the above functions, and a sample run:
name_input = build_input('What is your name? ')
age_input = build_number_input('What is your age? ')
day_input = build_number_input('Day: ', values=[1, 2, 3, 4, 5])
again_input = build_input('Again? ', values='yn', process=lambda v:v.lower())
print('Hello, {}! You are {}.'.format(name_input(), age_input()))
print('You want day {}'.format(day_input()))
print('You {}want to go again.'.format('' if again_input() == 'y' else "don't "))
What is your name? Peilonrayz
What is your age? 20.5
What is your age? 20a
What is your age? 20
Hello, Peilonrayz! You are 20.
Day: [1/2/3/4/5]
> 6
Day: [1/2/3/4/5]
> 0
Day: [1/2/3/4/5]
> 55
Day: [1/2/3/4/5]
> five
Day: [1/2/3/4/5]
> 5
You want day 5
Again? [y/n]
> yeah
Again? [y/n]
> Nope?
Again? [y/n]
> N
You don't want to go again.
2 Answers 2
I do not like the functional approach of this, especially since you are "inheriting" the user_input
by number_input
.
Here's an object-oriented version:
#! /usr/bin/env python3
class UserInput:
def __init__(self, prompt=None, *, errors=None, values=None, process=None,
wanted=None, default=None, end=None):
self.prompt = prompt
self.errors = errors
self.values = values
self.process = process
self.wanted = wanted
self.default = default
self.end = end
def __call__(self):
return self.get()
@property
def prompt(self):
"""Returns the prompt."""
prompt = self._prompt or ''
if self.values:
prompt += '[{}] '.format('/'.join(str(i) for i in self.values))
if self.end is not None:
prompt += self.end
elif self.values:
prompt += '\n> '
return prompt
@prompt.setter
def prompt(self, prompt):
self._prompt = prompt
@property
def errors(self):
return self._errors or tuple()
@errors.setter
def errors(self, errors):
self._errors = errors
@property
def values(self):
return self._values
@values.setter
def values(self, values):
self._values = set(values) if values is not None else None
@property
def process(self):
return self._process or (lambda v: v)
@process.setter
def process(self, process):
self._process = process
@property
def wanted(self):
"""Returns the wanted callback."""
if self._wanted:
return self._wanted
elif self.values is None:
return lambda _: True
return lambda value: value in self.values
@wanted.setter
def wanted(self, wanted):
self._wanted = wanted
def get(self):
"""Retrieves the user input."""
while True:
try:
value = input(self.prompt)
except (EOFError, KeyboardInterrupt):
if self.default is not None:
return self.default
raise
try:
value = self.process(value)
except self.errors:
continue
if self.wanted(value):
return value
class NumberInput(UserInput):
def __init__(self, prompt=None, *, errors=ValueError, values=None,
process=int, wanted=None, end=None):
super().__init__(
prompt, errors=errors, values=values, process=process,
wanted=wanted, end=end)
def main():
name_input = UserInput('What is your name? ')
age_input = NumberInput('What is your age? ')
day_input = NumberInput('Day: ', values=[1, 2, 3, 4, 5])
again_input = UserInput('Again? ', values='yn', process=lambda v:v.lower())
print('Hello, {}! You are {}.'.format(name_input(), age_input()))
print('You want day {}'.format(day_input()))
print('You {}want to go again.'.format('' if again_input() == 'y' else "don't "))
if __name__ == '__main__':
main()
I also added an additional keyword argument default
the value of which will be returned on KeyboardInterrupt
s and EOFError
s (which occur on pressing [Ctrl]+[D]) iff specified.
Regarding the function of the code itself, I think that it is trying to be a jack-of-all-trades-device and doing a bad job at it while violating the YAGNI principle.
User input is highly variable and dynamic. If you think of a simple yes/no question where yes
should be default (if one just hits enter) and the prompt should highlight the default value like in the pacman prompt below, this is really hard to implement using your functions (or classes).
$ LANG=en_US sudo pacman -S kate
resolving dependencies...
:: There are 2 providers available for phonon-qt5-backend:
:: Repository extra
1) phonon-qt5-gstreamer 2) phonon-qt5-vlc
Enter a number (default=1): # Just hit [Return] here.
looking for conflicting packages...
warning: dependency cycle detected:
warning: phonon-qt5-gstreamer will be installed before its phonon-qt5 dependency
Packages (43) attica-qt5-5.38.0-1 editorconfig-core-c-0.12.1-2 kactivities-5.38.0-1 karchive-5.38.0-1 kauth-5.38.0-1 kbookmarks-5.38.0-1 kcodecs-5.38.0-1 kcompletion-5.38.0-1 kconfig-5.38.0-1
kconfigwidgets-5.38.0-1 kcoreaddons-5.38.0-1 kcrash-5.38.0-1 kdbusaddons-5.38.0-1 kglobalaccel-5.38.1-1 kguiaddons-5.38.0-1 ki18n-5.38.0-1 kiconthemes-5.38.0-1 kio-5.38.0-1
kitemmodels-5.38.0-1 kitemviews-5.38.0-1 kjobwidgets-5.38.0-1 knewstuff-5.38.0-1 knotifications-5.38.0-1 kparts-5.38.0-1 kservice-5.38.0-1 ktexteditor-5.38.0-1 ktextwidgets-5.38.0-1
kwallet-5.38.0-1 kwidgetsaddons-5.38.0-1 kwindowsystem-5.38.0-1 kxmlgui-5.38.0-1 libdbusmenu-qt5-0.9.3+16.04.20160218-1 media-player-info-22-2 phonon-qt5-4.9.1-4
phonon-qt5-gstreamer-4.9.0-3 polkit-qt5-0.112.0+git20160226-1 qt5-multimedia-5.9.1-2 qt5-speech-5.9.1-2 solid-5.38.0-1 sonnet-5.38.0-1 syntax-highlighting-5.38.0-1 threadweaver-5.38.0-1
kate-17.08.1-1
Total Download Size: 37.95 MiB
Total Installed Size: 146.67 MiB
:: Proceed with installation? [Y/n] # Default option is capitalized.
A few things that can save you in LOC (Lines of Code)
Recursion: keep trying to ask for input, recursive each time. that gets rid of a whole loop
Function Alias: build_input accepts values and saves them. When the callable is called, it will call the appropriate function with your settings saved.
Default args: can be set in the function definition.
#returns a callable
def build_input(*args,**kwargs):
def a():
return _build_input(*args,**kwargs)
return a
def _build_input(prompt = None, values = None, process = lambda a : a, end = ''):
try:
text = process(input(prompt + end))
if(values != None):
return text if text in values else _build_input(prompt,values,process,end)
return text
except:
return _build_input(prompt,values,process,end)
#this is also a better way to build_number_input, by using kwargs
## ** is the packing/unpacking directive, so you can take the settings, set some defaults, and pass to the next funciton
def build_number_input(*args,**kwargs):
kwargs.update({'process':int})
return build_input(*args,**kwargs)
name_input = build_input('what is your name',process = str)
age_input = build_input('what is your age',process = int,values = [12,2,4])
day_input = build_input('Day: ',process = int,values = [1,2,3,4,5])
again_input = build_input('Again?',process = lambda v:v.lower(),values = 'yn')
num_input = build_number_input(prompt='what number?')
num_input()
age_input()
day_input()
again_input()
not sure what the errors directive was, I don't really think you care what the error is, if there is an error you just ask again.
-
\$\begingroup\$ Whilst I want to reduce LOC, that's not the main aim. Your code is more annoying to use, as you have to specify the same stuff over and over. The
errors
variable is very important, I don't want to silence ctrl-c. And recursion uses \$O(n)\$ memory, as you build a call stack, and you can exceed the maximum stack limit, causing errors with your code. In all, I don't see how your code is better than the original. Explaining how your changes are better would help sway me to your side. \$\endgroup\$2017年09月19日 23:17:17 +00:00Commented Sep 19, 2017 at 23:17 -
\$\begingroup\$ I'm just a fan of reduced LOC. I guess the best changes to your code would be
prompt = prompt or '' process = process or (lambda v: v) errors = errors or tuple()
defined in the function definition. Also, I thinkbuild_number_input
is cleaner and if you change kwargs later its adaptable. I think that there are aspects of my code that make improvements on yours, I think that there is likely things you can take from my code to make yours better -- I might have just got a little carried away on LOC. \$\endgroup\$Joshua Klein– Joshua Klein2017年09月19日 23:58:56 +00:00Commented Sep 19, 2017 at 23:58 -
\$\begingroup\$ also you convert
values
to a list, then a set. Why did you translate it to a set? You do not need to convert it to a list, a python string is already an iterate. \$\endgroup\$Joshua Klein– Joshua Klein2017年09月20日 00:02:20 +00:00Commented Sep 20, 2017 at 0:02 -
\$\begingroup\$ It'd be clearer if you could add your first comment to your answer, as I don't really understand you. \$\endgroup\$2017年09月20日 08:10:50 +00:00Commented Sep 20, 2017 at 8:10
-
\$\begingroup\$ If
values
is an iterator, rather than an iterable, then it will be consumed when building the prompt. Converting it to a list, means that we can loop through it twice, once to make the prompt - which needs ordered data too, the other to make the set. The set is used for \$O(1)\,ドル rather than \$O(n)\,ドル lookup. \$\endgroup\$2017年09月20日 08:12:53 +00:00Commented Sep 20, 2017 at 8:12
float
rather thanint
. \$\endgroup\$