Selecting user commands in style (Python)
This post is part of the { Programming } series.
If you write an interactive program that takes user commands as an input, you can select the appropriate action for a given command by going through a sequence of if... else if
statements.
But if you write in Python, there’s always a cooler way to do things. A method I like personally in this situation is defining a dictionary where the keys are the command strings, and the corresponding values are lambda expressions.
In the following definition, all commands are called with command[cmd](args)
. When we want to deal with faulty commands immediately in a single line we can write command.get(cmd, lambda x: (error(input), help_message()))(args)
.
By passing a command string to the dictionary as a key, the lambda expression corresponding to that command key is selected.
But in order to be fully applied, the lambda function still required an argument, which we can simply pass behind the call to the dictionary.
Although this method is maybe not more efficient… I would say it wins when scored on style.
def read_cmd(input):
inputs = input.split()
cmd = inputs[0]
args = inputs[1:]
commands = {
"help": lambda x: help_message(),
"poem": lambda x: say_poem_for(x[0]),
"say": lambda x: banner_say(" ".join(x)),
"exit": lambda x: banner_say("Bye cruel world!")
}
commands.get(cmd, lambda x: (error(input), help_message()))(args)
# Fabricate some fake user inputs for testing
user_inputs = ["Incorrect command", "say Welcome to the mean poem machine", "poem reader", "exit"]
for user_input in user_inputs:
read_cmd(user_input)
The get
function of a dictionary deals with wrong commands by returning a default value, which in our case also has to be a function, as we pass args
to it.
The one-liner command.get(cmd, lambda x: (error(input), help_message()))(args)
therefore does the same as:
try:
command[cmd](args)
except:
error(input)
help_message()
To run the code for yourself, you could use these silly functions. Run them in Python 3.
def help_message():
print("""
help see this menu, obviously
say say some text placed in a ascii banner
poem say a little poem for your muse
exit say bye!
""")
def banner_say(message):
print("""
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
%s
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
""" % message)
def say_poem_for(muse):
print("""
Dear %s,
Roses are not blue
Violets are not red
Whatever you do
It trumps being dead
""" % muse)
def error(incorrect_input):
print("""
'%s' was an example of an incorrect command
""" % incorrect_input)
Which together produces the following output:
'Incorrect command' was an example of an incorrect command
help see this menu, obviously
say say some text placed in a ascii banner
poem say a little poem for your muse
exit say bye!
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Welcome to the mean poem machine
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dear reader,
Roses are not blue
Violets are not red
Whatever you do
It trumps being dead
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Bye cruel world!
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Comments
alex on Monday, Dec 31, 2018:
Neat! Small notes:
lambda _: func()
orlambda x: func(x)
is pointless. It’s the coders equivalent of dividing by 1 in languages where functions can be passed 😄.If you were to apply all these changes the code would look something like this:
Part is about how to interpret a command, part about how to interpret its arguments, and part about how to continue execution given a certain command. The application is trivially simple so it’s no problem and actually more beneficial to keep it this short I’d argue. Nevertheless, healthy to keep in mind this is an undesirable property in most programs.
For fun, here’s a haskell version. It makes little use of types, and overloads the
read_cmd
step to stay concise and close to the python version. Neither of which I’d normally recommend 😉.Reply to alex
Edwin on Wednesday, Jan 2, 2019
In reply to alex
Thanks for your reply! I really enjoy throwing this online and getting your two cents on it. First of all, your variable naming is obviously better, ‘x’ is not informative at all (I just happen to like the look of it, personally…).
Secondly, I like how you pulled apart this unreadable one liner
commands.get(cmd, lambda x: (error(input), help_message()))(args)
. As you maybe know, when I started this post this expression was way simpler, but along the way it became somewhat of a challenge to put as much stuff as possible in this one line… I didn’t have the side-effect in there initially, but put it in later for fun, because I actually did not know before writing this post that you could pass a tuple of functions into a lambda like that. But again, no debate here that your version is a definite improvement!Thirdly, your remark about dividing by one made me laugh (and cringe about myself a little bit). You actually made me go back to my code to think about why I define a lambda expression for a simple function call. I now know why: to prevent
help_message()
from always being executed immediately. But of course, like your code shows, instead of using a lambda, I should have just removed the parentheses and writtenhelp_message
instead. Learned something here!Fourthly, you are right that I don’t care in this case that
read_cmd
has multiple purposes right now because this is a small silly program anyways. After your remarks about multiple purposes and side-effects, it totally makes sense that you replied with a Haskell script! Unfortunately, it looks like you forgot to include the link though …Thanks again! I learned something from your reply, which motivates me to keep making mistakes in the public eye ;-)
alex on Wednesday, Jan 2, 2019
In reply to alex
Edwin on Wednesday, Jan 2, 2019
In reply to alex
alex on Wednesday, Jan 2, 2019
In reply to alex
Edwin on Wednesday, Jan 2, 2019
In reply to alex