2

I'm asking for suggestions on how to get "close" to overloading the and operator. A code example to hint at what I'm doing:

import operator


OP = { operator.ge: ">=", operator.le: "<=" }


class Expression(object):
    def __init__(self, left, right, op):
        self.left = left
        self.right = right
        self.op = op

    def __le__(self, other):
        return Expression(self, other, operator.le)

    def __ge__(self, other):
        return Expression(self, other, operator.ge)

    def __str__(self):
        return "(%s %s %s)" % (self.left, OP[self.op], self.right)

    def __repr__(self):
        return "<Expression: %s %s %s>" % (self.left, self.op, self.right)


class Variable(Expression):
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

    def __repr__(self):
        return "<Variable: %s>" % self.name


def _and(left, right):
    return (left, right)

print 1 <= Variable("x") <= 3
print _and(1 <= Variable("x"), Variable("x") <= 3)

The above example prints:

(x <= 3)
(<Expression: x <built-in function ge> 1>, <Expression: x <built-in function le> 3>)

According to the Python docs an expression 1 <= a <= 2 translates to 1 <= a and a <= 2, so I'm guessing that in my example 1 <= Variable("x") evaluates to True so the expression returns the value of Variable("x") <= 3. I need a way to get both expressions (1 <= a, and a <= 2) separately, and preferably without writing out the middle expression twice (like I did with the _and function), since the middle expression could be quite complex and bothersome to have twice (I realize I could save it to a variable and use it, but this is also less readable). According to this SE post the and operator can't be overloaded, so what I'm asking is whether this can be done, and then how, or if I have to resort to splitting the comparison in two (specifying the middle expression twice). Any suggestions in the right direction are appreciated! Thank you.

Possibly related:

Edit: So what I'm essentially doing is writing out constraints for a linear program, so relevant parts of my code currently look something like this:

model.add_constr(a <= b)
model.add_constr(b <= c)
# now i could alter add_constr to support either a tuple or two arguments enabling:
model.add_constr(a <= b, b <= c)
model.add_constr((a <= b, b <= c))
# but most pleasing would be if I could do 
model.add_constr(a <= b <= c)
# that would return for example a two tuple that could be interpreted appropriately by 
# add_constr()
Community
  • 1
  • 1
karihre
  • 437
  • 5
  • 9

2 Answers2

2

It looks like you want to delay evaluation of or statically analyze a Python-like expression. Have you considered using abstract syntax trees? Python can parse and not evaluate an expression using tools from the ast standard library module. See http://docs.python.org/2/library/ast.html and http://eli.thegreenplace.net/2009/11/28/python-internals-working-with-python-asts/ .

Instead of trying to oeverload every operator, you'd have a Python expression represented as a data structure, and you can do whatever you want with it. You can make modifications to the tree and then execute the result, or convert the tree into your own Expression and Variable classes.

For instance, ast represents chains of comparisons as an arbitrarily long list of expressions and comparison operators (see the Compare expression type). This is how the and-chaining is implemented.

Edit: Here's an example usage:

>>> import ast
>>> x = ast.parse("2 + 2")
>>> print ast.dump(x)
Module(body=[Expr(value=BinOp(left=Num(n=2), op=Add(), right=Num(n=2)))])
>>> eval(compile(ast.Interactive(body=[x.body[0]]), "fakefile", "single"))
4

In your case, you probably wouldn't follow through to actually letting Python evaluate the expression, since you just want to use Python as an expression parser, not an interpreter. (That's what your Expression and Variable symbolic trees are effectively doing.) You'd interpret a subset of the possible expression trees as directives to pass to model. That is, you'd handle the subset of Python that you're interested in: symbols (variables), comparison operators, logical operators. The rest can raise a SyntaxError or NotImplementedError or whatever you want to use to inform your users that they're trying to do something outside of the implemented subset.

Jim Pivarski
  • 5,568
  • 2
  • 35
  • 47
  • This sounds very promising. I'll check in a bit later :). – karihre Jul 16 '13 at 02:33
  • Did a fair bit of reading but a bit confused. How would I "delay" the evaluation of these expressions? From my understanding I'd basically have to run my python script through a different one (like `$ python transform.py original.py`), that looks at the script source and changes the AST's before evaluating the code? Or pass the expressions as strings to functions who would "mis"-interpret them? Am I overlooking something? – karihre Jul 16 '13 at 03:32
  • I hope I'm answering what you're asking: the expressions that you interpret via ASTs would have to be quoted, either by literal quotes as in `evaluate("my expression")` or by transforming a whole external file. This is essentially like `(quote (stuff...))` in Lisp. "Mis"-interpreted Python code can't be freely mixed with other Python code. I'll add an example to my answer. – Jim Pivarski Jul 16 '13 at 04:30
  • Thanks, your answer was spot on. – karihre Jul 16 '13 at 05:20
0

For a purely syntactic solution, you could simply put parentheses around one of the comparisons, to break Python's operator chaining: (a <= b) <= c.

I'm not sure that it makes a lot of sense that way though. That is, if you did that with normal values you'd often get a nonsensical result (or an error in Python 3, perhaps). It might confuse other people reading your code (or yourself, months from now).

I suspect a better solution is to do as you were considering, and stick with simple comparisons When you need to express a double-ended constraint, use two single ended ones. If you need a complicated value in the middle, try saving it in a variable rather than recomputing it in each expression:

b_expr = b**4 + 3*b**2 - 4*x - 100
model.add_constr(a <= b_expr, b_expr <= c)
Blckknght
  • 100,903
  • 11
  • 120
  • 169