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!
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 


Version control on notebooks using pre-commit and Jupytext <-- Latest

New feature! Added comments to this *static* website <-- Next

Sorting threads in NeoMutt <-- Previous

On Bayesian likelihood <-- Random

Webmentions


Do you want to link a webmention to this page?
Provide the URL of your response for it to show up here.

Comments

alex on Monday, Dec 31, 2018:

Neat! Small notes:

If you were to apply all these changes the code would look something like this:

def error(incorrect_input):
    print("""
    '%s' was an example of an incorrect command
    """ % incorrect_input)
    help_message()


def read_cmd(user_input):
    inputs = user_input.split()
    cmd = inputs[0]
    args = inputs[1:]

    commands = {
        "help": help_message,
        "poem": lambda muse: say_poem_for(muse[0]),
        "say": lambda words: " ".join(words),
        "exit": lambda _: banner_say("Bye cruel world!"),
    }

    command = commands.get(cmd, lambda _: error(user_input))
    command(args)

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 😉.

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 written help_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

Hmm, maybe something went wrong with the markdown 🤔, the bullets also got mangled. As a test, here’s the link using markdown and here it is in plaintext: https://github.com/alextes/haskell-notepad/blob/master/PoemWriter.hs .

Edwin on Wednesday, Jan 2, 2019
In reply to alex

Looks like the markdown works fine, I think you just forgot it the first time around :) Although special internet night vision goggles are needed to see the link, I have to admit. In order for lists to compile correctly, you need a blank line before the list. But kudos for the Haskell script!

alex on Wednesday, Jan 2, 2019
In reply to alex

blank line before lists? No such requirement in the markdown spec. I think your parser is simply misbehaving or one of the tools in your magic comment toolchain is mangling the text 😛.

Edwin on Wednesday, Jan 2, 2019
In reply to alex

Haha, now we finally get to the important stuff: squabbling about markdown syntax! There is no such thing as “the” ultimate specification of Markdown, as there are a lot of flavours, so I’m not sure what specification you are referring to. Perhaps something John Gruber wrote, since your Haskell scripting betrays you are pretty hardcore, but otherwise my guess would be you are mostly using GitHub flavoured markdown. I pretty much daily use pandoc-markdown myself. But notice that a complete blank line is needed to separate paragraphs, and that therefore a ‘*’ without a preceding blank line is by most markdown flavours recognized as being part of the preceding paragaph, i.e. regular text. Check this link for an example of the difference, or consider this standard example on Wikipedia. But you don’t have to believe me, try it out for yourself ;-)