Skip to main content
GameDev.net gamedev.net
🔒 Locked

python: paring down the number of ifs

Started by monkey_32606 Apr 17, 2008 at 8:58 AM 7 replies 1.7k views
Original Post
monkey_32606
monkey_32606
So I wrote this dead simple rpn calculator in python last night. It works just fine but there has to be a better way to map to the various operations instead of all of these elifs.

import math
stack = []
print "Type \'halp\' for directions. (\'q\' for quit)"
quitflag = False
while 1:
    input = str(raw_input(">>> ")).split()
    print input
    for token in input:
        if token.isdigit():
            stack.append(token)
        elif token == "pi":
            stack.append(math.pi)
        elif token == "pop":
            stack.pop()
        elif token == "p":
            print str(stack.pop())
        elif token == "+":
            stack.append(float(stack.pop())+float(stack.pop()))
        elif token == "-":
            stack.append(float(stack.pop())-float(stack.pop()))
        elif token == "*":
            stack.append(float(stack.pop())*float(stack.pop()))
        elif token == "/":
            stack.append(float(stack.pop())/float(stack.pop()))
        elif token == "^":
            stack.append(float(stack.pop())**float(stack.pop()))
        elif token == "halp":
            print "This is a reverse polish notaion (RPN) calculator."
            print "Operations are evaluated left to right and results are pushed onto the stack."
            print "Example >>> 9 1 2 +  ---->  [9, 3]"
            print "Cont'd  >>> +        ---->  [12]"
            print "Cont'd  >>> 36 /     ---->  [3]  (division may seem counter intuitive)"
            print "Operands: +-*/^  "
            print "Commands: pi (push pi); pop (pop stack); p (print)"
        elif token == "q":
            print "Bai!"
            quitflag = True
        else:
            stack.append(token)
            
    if quitflag == True:        # dirty stupid hack
      break
    print "Stack is:",stack

Anybody got any great ideas? I'd love to hear them.
Quote: Michael TanczosCut that shit out. You shouldn't be spying on other people.. especially your parents. If your dad wanted to look at horses having sex with transexual eskimo midgets, that's his business and not yours.
ToohrVyk
ToohrVyk
I would probably suggest creating a dictionary of lambdas. I believe this would appear as something along the lines of this, but my Python syntax is notoriously incorrect:

data = {  'pi' : (lambda x: x.append(math.pi)),  'pop' : (lambda x: x.pop()),  'p' : (lambda x: print str(x.pop()),  '+' : (lambda x: x.append( float(x.pop()) +  float(x.pop() )),  '-' : (lambda x: x.append( float(x.pop()) -  float(x.pop() )),  '*' : (lambda x: x.append( float(x.pop()) *  float(x.pop() )),  '/' : (lambda x: x.append( float(x.pop()) /  float(x.pop() )),  '^' : (lambda x: x.append( float(x.pop()) ** float(x.pop() )),}  for token in input:  if token in data:    data[token](stack)  else:    stack.append(token)

thedustbustr
thedustbustr
pop and p and pi dont work, but they would probably be better as special cases in the token reader, not tacked onto the doer
tokens = {  '+' : (lambda lhs,rhs: lhs+rhs)  '-' : (lambda lhs,rhs: lhs-rhs)  '*' : (lambda lhs,rhs: lhs*rhs)  '/' : (lambda lhs,rhs: lhs/rhs)  '^' : (lambda lhs,rhs: lhs**rhs)}  for token in input:  if token in tokens:    lhs, rhs=float(stack.pop()), float(stack.pop())    stack.append(tokens[token](lhs,rhs))  else:    stack.append(token)tokens['+'](4,18)
Zahlman
Zahlman
Quote:
Original post by ToohrVyk
I would probably suggest creating a dictionary of lambdas. I believe this would appear as something along the lines of this, but my Python syntax is notoriously incorrect:

*** Source Snippet Removed ***


Python lambdas need to be expressions, not statements. That rules out 'print'. (Meanwhile, there's no need to stringify what you print.)

Also, you can handle the 'default' case of dict lookup by using the .get() member of the dict.

def display(value): print valuedata = {  'pi' : (lambda x: x.append(math.pi)),  'pop' : (lambda x: x.pop()),  'p' : (lambda x: display(x.pop())),  '+' : (lambda x: x.append(float(x.pop()) +  float(x.pop()))),  '-' : (lambda x: x.append(float(x.pop()) -  float(x.pop()))),  '*' : (lambda x: x.append(float(x.pop()) *  float(x.pop()))),  '/' : (lambda x: x.append(float(x.pop()) /  float(x.pop()))),  '^' : (lambda x: x.append(float(x.pop()) ** float(x.pop()))),}for token in input: data.get(token, lambda x: x.append(token))(stack)
monkey_32606
monkey_32606
Thanks for the help. This seems like one of the situations where it may actually be more readable to use the elifs :( Anyways, I've expanded and updated the scope of the calculator since I posted this to include much of the python math library. I think right now it could be used as a general purpose calculator; as long as you ignore the lack of error checking (for instance if you try to add with no values on the stack, python has a kitten).

If anyone wants it here's the code for the rpnCalc module which does not yet use a dictionary; a read eval print loop would be easy to implement in Tk or Wx or just about anything.

rpnCalc Module
import math#(C) 2008 Travis Fickett released under the terms of The MIT Licenseclass rpnCalc:    stack = []    quitflag = False    def evalinput(self, instring = ""):        tokenlist = instring.split()        for token in tokenlist:            if token.isdigit():                self.stack.append(token)            elif token == "+":                self.stack.append(float(self.stack.pop())+float(self.stack.pop()))            elif token == "-":                self.stack.append(float(self.stack.pop())-float(self.stack.pop()))            elif token == "*":                self.stack.append(float(self.stack.pop())*float(self.stack.pop()))            elif token == "/":                self.stack.append(float(self.stack.pop())/float(self.stack.pop()))            elif token == "^":                self.stack.append(float(self.stack.pop())**float(self.stack.pop()))            elif token == "pi":                self.stack.append(math.pi)            elif token == "e":                self.stack.append(math.e)            elif token == "pop":                self.stack.pop()            elif token == "swap":                a = self.stack.pop()                b = self.stack.pop()                self.stack.append(a)                self.stack.append(b)            elif token == "drop":       # drop all values from the stack                self.stack = []            elif token == "ln":                self.stack.append(math.log(float(self.stack.pop())))            elif token == "log":                self.stack.append(math.log10(float(self.stack.pop())))            elif token == "sin":                self.stack.append(math.sin(float(self.stack.pop())))            elif token == "cos":                self.stack.append(math.cos(float(self.stack.pop())))            elif token == "tan":                self.stack.append(math.tan(float(self.stack.pop())))            elif token == "p":                print str(self.stack.pop())            elif token == "i":                self.stack.append(str(raw_input("......>")))            elif token == "load":                #not yet implimented                pass            elif token == "halp" or token == "help":                print "This is a reverse polish notaion (RPN) calculator."                print "Operations are evaluated left to right and results are pushed onto the stack."                print "Example >>> 9 1 2 +  ---->  [9, 3]"                print "Cont'd  >>> +        ---->  [12]"                print "Cont'd  >>> 36 /     ---->  [3]  (division may seem counter intuitive)"                print "Operands: +-*/^  "                print "Commands: pi e pop(erase top) p(print) swap(top two) drop(entire stack)"                print "Trig Functions: sin cos tan (operate in radians)."            elif token == "q":                print "Bai!"                self.quitflag = True            else:                self.stack.append(token)


#Released into the public domain 2008 by Travis Fickettimport rpnCalcprint "Type \'halp\' for directions. (\'q\' for quit)"rpn = rpnCalc.rpnCalc()while rpn.quitflag == False:    #READ EVAL PRINT LOOP    instring = str(raw_input(">>> "))    print instring    rpn.evalinput(instring)    if rpn.quitflag == True:        # dirty stupid hack        break    print "Stack:",rpn.stack
Quote: Michael TanczosCut that shit out. You shouldn't be spying on other people.. especially your parents. If your dad wanted to look at horses having sex with transexual eskimo midgets, that's his business and not yours.
Anon Mike
Anon Mike
Quote:
Original post by monkey_32606
This seems like one of the situations where it may actually be more readable to use the elifs :(

Readability is in the eye of the reader. In this case I find the lambda version to be *far* easier to read than the elif version. And I don't even know python.
-Mike
Zahlman
Zahlman
Oh oh oh. Idea.

Putting things into a class is redundant when you're just "namespacing" things; having rpnCalc be a module already gets you all the benefit there.

But.

We can make a class that represents the stack, by extending the list class. Yes, Python lists are objects, and being objects, they are instances of a class, and we can extend that class.

Then, we can use the dict-lookup stuff to select a member function of the class, which tells us what to do.

import mathclass rpn_stack(list):  def plus(self):    x, y = self.pop(), self.pop()    self.append(x + y)  def minus(self):    x, y = self.pop(), self.pop()    self.append(x - y)  # etc.  def pi(self):    self.append(math.pi)  def e(self):    self.append(math.e)  # the definition of 'pop' would just... pop, so we don't need that at all.  def swap(self):    x, y = self.pop(), self.pop()    self += [y, x] # a slicker way to do it :)  def drop(self):    self[:] = [] # notice how we use a slice here. You can't assign to 'self'...  def p(self):    print self.pop()  def i(self):    self.append(str(raw_input("......>")))  # Help ought to be implemented separately. Why allow for 'help' in the middle  # of a random calculation expression :)  # Now here's where we handle a general token.  def handle(self, token):    # First off, if it's a named unary function from 'math', we can look it up there.    if token in ('ln', 'log', 'sin', 'cos', 'tan'):      # transform the names to the math module's names      if token == 'log': token = 'log10'      elif token == 'ln': token = 'log'      # Here's how we do the lookup:      self.append(getattr(math, token)(self.pop()))      # Instead of doing the cast *there*, I'm going to ensure that only       # floats ever appear on the stack.      return    if token in ('pi', 'e', 'pop', 'swap', 'drop', 'p', 'i'):      # Do the lookup in ourself.      getattr(self, token)()      return    # Next, check the mapping for binary arithmetic operators. Which one? This one:    ops = {'+': self.plus, '-': self.minus, '*': self.times, '/': self.div, '^': self.exp}    # And we'll do it in a way that provides the catch-all default:    ops.get(token, lambda: self.push(float(token)))()# Now, the loop to evaluate stuff is external:def evalinput(self, instring = ""):  mystack = rpn_stack()  # Look closely how the loop termination logic is handled. No flag needed.  # In cases where a flag is used to break a loop, you should try to use it for  # the loop condition. Otherwise, you might as well break directly.  while True:    data = str(raw_input(">>> ")).split()    if data[0] in ('help', 'halp'):      # show the help      continue    if data[0] == 'q':      break    for token in data:      mystack.handle(token)  print "Bai!"  print "Stack is:", stack
monkey_32606
monkey_32606
Cool, this is why I come here. People like Zahlman explain neat stuff.

I had no idea you could do that with getattr. I like making help and quit a special case (which fixes some problems). The reason it has that stupid quitflag is because break would only leave the first for loop (DURRRR)

E:
It was pretty trivial to add user defined variables too. I made them as a dictionary. The form is something like this:
>>> a 42 setUservars: {'a': 42}>>> a 1 +             Defined variables are expanded.Stack: [43]


In that example 'a' and 42 are pushed onto the stack. 'Set' pops them and adds a key value pair to the uservars dictionary. The parser looks for keys matching the token in the uservars dictionary and pushes the value of that onto the stack. The only weird thing that can happen is that you can define a key in a python dictionary to be almost anything.

E.G.:
>>> pi 42 setUservars: {'3.14159.....':42}    This particular pair is inaccessible (and crashes the current implementation)




[Edited by - monkey_32606 on April 18, 2008 3:06:27 PM]
Quote: Michael TanczosCut that shit out. You shouldn't be spying on other people.. especially your parents. If your dad wanted to look at horses having sex with transexual eskimo midgets, that's his business and not yours.
Zahlman
Zahlman
Here's a syntax which may help you avoid problems:

>>> 42 :aUservars: {'a': 42} # 42 is still on the stack; it could be popped>>> 42 :pi# variable is assigned, but inaccessible; shouldn't crash anything.# Or you could make variables have higher priority in your parsing.>>> a42>>> *aUservars: {} # 'a' removed from variable bindings.


Implementation is up to you. :)

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.