6
\$\begingroup\$

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.
asked Sep 19, 2017 at 16:00
\$\endgroup\$
2
  • \$\begingroup\$ 20.5 is a number. If you want to force an int, use a different function name. \$\endgroup\$ Commented Sep 19, 2017 at 20:16
  • \$\begingroup\$ @glennjackman Yeah, I should probably re-name it, or use float rather than int. \$\endgroup\$ Commented Sep 19, 2017 at 20:23

2 Answers 2

5
\$\begingroup\$

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 KeyboardInterrupts and EOFErrors (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.
answered Sep 20, 2017 at 9:58
\$\endgroup\$
0
1
\$\begingroup\$

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.

answered Sep 19, 2017 at 22:25
\$\endgroup\$
5
  • \$\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\$ Commented 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 think build_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\$ Commented 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\$ Commented 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\$ Commented 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\$ Commented Sep 20, 2017 at 8:12

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.