2
\$\begingroup\$

I created a very simple interpreter in Python. It's capable of doing basic math (unfortunately with only two numbers), creating quines and printing text.

import re

def prompt():
    userInput = input("> ")
    return userInput

def parse(command):
    if command.startswith("+"):
        numbers = re.findall(r'\d+', command)
        nums = ''.join(numbers)
        nums.replace(" ", "")
        firstNumber = int(nums[0])
        secondNumber = int(nums[1])
        print(firstNumber + secondNumber)

    elif command.startswith("-"):
        numbers = re.findall(r'\d+', command)
        nums = ''.join(numbers)
        nums.replace(" ", "")
        firstNumber = int(nums[0])
        secondNumber = int(nums[1])
        print(firstNumber - secondNumber)

    elif command.startswith("*"):
        numbers = re.findall(r'\d+', command)
        nums = ''.join(numbers)
        nums.replace(" ", "")
        firstNumber = int(nums[0])
        secondNumber = int(nums[1])
        print(firstNumber * secondNumber)

    elif command.startswith("/"):
        numbers = re.findall(r'\d+', command)
        nums = ''.join(numbers)
        nums.replace(" ", "")
        firstNumber = int(nums[0])
        secondNumber = int(nums[1])
        print(firstNumber / secondNumber)

    elif command.startswith("q"):
        print(command)

    elif command.startswith("p"):
        print(command.strip("p "))

while True:
    userInput = prompt()
    parse(userInput)

Some examples:


Input: +65

Output: 11


Input: q This is a quine

Output: q This is a quine

It's working good for me, but there's a few things I'd like to add, like multiple statements on one line and math with 2+ numbers.

Is there anything I could do to improve this?

Please remember I'm not a professional programmer and that this is one of the first interpreters I've ever written.

\$\endgroup\$
  • \$\begingroup\$ Do you want to remain with the +65 style notation instead of 6+5? \$\endgroup\$ – SuperBiasedMan Oct 7 '15 at 13:46
  • \$\begingroup\$ @SuperBiasedMan The +65 notation was just to make it easier to code. (I don't know how to implement the 6+5 style notation...) No, I don't want to remain with the +65 notation. \$\endgroup\$ – m654 Oct 7 '15 at 13:50
1
\$\begingroup\$

First off, great that you have a separate prompt function. Too often people don't do this and it's much better separated into it's own function. However, you keep it very light on purpose right now. Much better would be to perform input validation here, ensure that the user has provided something you can actually work with. Even at a basic level, you could test for an empty string:

def prompt():
    while True:
        userInput = input("> ")
        if len(userInput) > 0:
            return userInput
        print("Please enter a command.")

You can see I used a while loop to keep asking for input until the user enters something valid, and then the function returns whatever their command is. This is only a small amount of validation, but it should give you an idea about how to expand on the idea.

You should have more small functions like prompt. For example, your number extracting process is the same in each arithmetic function but you copy and paste it anyway. Instead, have one function to handle that in all cases.

def parse_numbers(user_input):
    numbers = re.findall(r'\d+', user_input)
    nums = ''.join(numbers)
    return nums.replace(" ", "")

One thing that's odd about this function is that it only allows for single digit numbers. You should use the actual operator to split up separate numbers and that way you can have multiple digits.

>>> "12+251".split('+')
["12", "251"]

With this, I'd modify parse_numbers to instead split(+):

def parse_numbers(user_input, operator):
    numbers = user_input.split(operator)

Note that we nowhave to pass the operator parameter, this way we can call parse numbers on any arithmetic command, whether it has a +, -, * or \.

We could simply return numbers now, but then we'd have no input validation. So instead lets try turning all the string into numbers before we return them. A good way to do this is using map, which will apply a function to every item in a list. So we can apply int to each number and then return it. We can also use try except to make sure that if anything can't be converted to a number we tell the user what happened.

def parse_numbers(user_input, operator):
    numbers = user_input.split(operator)
    try:
        return map(int, numbers)
    except ValueError as error:
        print (error.message)

except ValueError means that if an invalid value is passed to int (like "hello") Python will catch the error. This prevents the whole program from ending and gives you an opportunity to instead inform the user why their input is invalid. That's what print (error.message) does, as that will tell the user something useful like:

invalid literal for int() with base 10: 'hello'

To continue with all these functions, I think you should have a function for each command. They can be quite short now that you defined parse_numbers but they still help organisation:

def add(command):
    numbers = parse_numbers(command)
    return numbers[0] + numbers[1]

Easy, right? And you could do this with all of them. Note that I'm using return instead of print because it's best to have functions actually return values in case you ever need them for more than just displaying the result. Also, it's not much harder to account for more than two numbers, ie. "23+3+1". It's very easy for add, you can use a built in function called sum that gets the total sum of all elements in a list.

def add(command):
    numbers = parse_numbers(command, '+')
    return sum(numbers)

For other functions it's not so easy, but you can use something called reduce. reduce basically takes each element of the list and combines them through some function until all the elements have been combined. This does exactly what you need for the other cases. For reduce you need to pass a list as well as a function, in this case we can use a special function called a lambda, they're basically small one line functions. They're useful in cases like this where you don't need to bother defining a whole function to do the job. They're quite compact but still look like functions just without names. Here's how subtract would look:

def subtract(command):
    numbers = parse_numbers(command, '-')
    return reduce(lambda x, y: x - y, numbers)

The same principle can operate on * and / to give you a set of four arithmetic functions. In Python 3 this function was moved to the functools module, so you'll need add an import for it.

from functools import reduce

Of course your other two are even simpler, they're just basic print calls.

def quine(command):
    print(command)

def print_command(command):
    print(command.strip("p "))

Two points on these though. First, I have no idea what a quine is. I had to google it and I still don't see why it's implemented here? You could explain it with a comment, but now that it's a function you could put on a docstring to explain what it does. Docstrings are programmatically accessible comments that explain a function or class. Good practice to put them on your functions. Even if add is quite clear in its use and function, it's good to note that it operates on a list of any length of integers and returns a single integer.

Second, your print command will accidentally remove any 'p's from the end of a string. strip actually removes all the characters that it's passed, not just a specific string. So see what happens here:

>>> "p ppp p p p ppppp pp p ".strip("p ")
""

Instead, just using slicing. Slicing allows you to use indices to get a certain range from a string or list. In your case, you just want to get from the 3rd element on. Indexing starts at 0, so the third element is index 2. You can slice command with [2:]. The lack of a second parameter after : means that it will go to the end of the string.

Now, since we have a bunch of handy functions, this will make the parse function even neater. Here's how it could look:

def prompt():
    while True:
        userInput = input("> ")
        if len(userInput) > 0:
            return userInput
        print("Please enter a command.")

def parse_numbers(user_input, operator):
    numbers = user_input.split(operator)
    try:
        return map(int, numbers)
    except ValueError as error:
        print (error.message)

def add(command):
    numbers = parse_numbers(command, '+')
    return sum(numbers)

def subtract(command):
    numbers = parse_numbers(command, '-')
    return reduce(lambda x, y: x - y, numbers)

def multiply(command):
    numbers = parse_numbers(command, '*')
    return reduce(lambda x, y: x * y, numbers)

def divide(command):
    numbers = parse_numbers(command, '/')
    return reduce(lambda x, y: x / y, numbers)

def parse(command):
    if command.startswith("q"):
        quine(command)
    elif command.startswith("p "):
        print_command(command)
    elif '+' in command:
        print (add(command))
    elif '-' in command:
        print (subtract(command))
    elif '*' in command:
        print (multiply(command))
    elif '/' in command:
        print (divide(command))
    else:
        print("Invalid command")

You'll note I added a print to inform the user when they've passed a totally invalid command too. I'm also not keen on the long chain of elifs, you could remove them partially with dictionaries since they're all basically calling a function with the same parameter. The difficulty is figuring out how to extract the command from your string without clashes. This is more of a design than programming problem, because if you're going to extend this you need to find a safe way to define your syntax that's both computer and human readable.

\$\endgroup\$
  • 2
    \$\begingroup\$ Wow, you must have put a lot of effort into this comment! I fixed my code and it's working perfectly except for the subtract(), multiply() and divide() functions. The reduce() doesn't seem to be defined... Is the reduce function only in Python 2.x? (I'm currently using Python 3.4.3) \$\endgroup\$ – m654 Oct 7 '15 at 15:39
  • \$\begingroup\$ Yep, reduce is only in Python 2.x. But if I try to run my code in Python 2.7, none of the functions work. (And I'm not really in the mood for porting the code...) <strike>Is there an alternative to reduce() for Python 3.4?</strike> I checked the Python 3 changelog and it said that reduce() was removed but functools.reduce() can be used instead. I'll try that. \$\endgroup\$ – m654 Oct 7 '15 at 15:46
  • \$\begingroup\$ @m654 Oh, yes it turns out they changed it in Python 3 because of readability. If you add from functools import reduce to your imports it should be fine. \$\endgroup\$ – SuperBiasedMan Oct 7 '15 at 15:50
  • \$\begingroup\$ I tried it out now and it worked perfectly, thanks! (Oh and btw, I added the q command because I was trying to make a golfing language. Lots of code golf challenges require you to create a quine.) \$\endgroup\$ – m654 Oct 7 '15 at 15:56
  • \$\begingroup\$ @m654 Ah, that's definitely where a docstring comes in handy then. Glad to help. :) \$\endgroup\$ – SuperBiasedMan Oct 7 '15 at 15:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.