Introspection: expression evaluation order - precedence

Christos TZOTZIOY Georgiou tzot at sil-tec.gr
Fri Oct 24 08:58:32 EDT 2003


Hi all,

this post contains at the end a handy module that I've used quite often
when I wanted to analyse the occasional complex expression and how it
was to be evaluated.

The function analyse_expression is called with a single string argument
containing an expression.  Names are allowed (actually, preferred over
numbers ;-), since the function eval's in a protected dictionary, where
names are generated as needed.
The output is a string containing many lines, where each line is of the
format:

[<operand1><space>]<operator><space><operand2>

<operand1> and 1st <space> are missing for unary operators.

There are only a few exception checks, since basically the function is
to be called with expressions pasted from actual syntactically correct
code.

Hope this helps other people, esp. newcomers in the python world.
Next step will be an tree-structured expression editor, allowing easy
editing (eg exchange the first argument of a function with the second
onei, even if these are complex expressions themselves, for people who
don't break down complex expressions as much as the Python spirit would
suggest), which could find a use inside Idle if done OK; but that will
be RSN :)
In case you find any flaws in this module, I would be glad to know and
correct them.  Improvements are accepted without any arguments!

Examples:

>>> print analyse_expression('x+y-sqrt(5/-z.real*6)')
x + y
z . real
- z.real
5 / -z.real
5/-z.real * 6
sqrt ( 5/-z.real*6 )
x+y - sqrt(5/-z.real*6)

Why names are preferred over numbers (a bug, actually ;-):

>>> print analyse_expression('5+sin(angle=.6*x)')

Traceback (most recent call last):
  File "<pyshell#16>", line 1, in -toplevel-
    print analyse_expression('5+sin(angle=.6)')
  File "analexpr.py", line 68, in analyse_expression
    eval(code_object, namespace)
  File "<evaluator>", line 0, in -toplevel-
TypeError: unsupported operand type(s) for +: 'int' and 'str'

but, substituting z for 5

>>> print analyse_expression('z+sin(angle=.6*x)')
0.6 * x
sin ( angle=0.6*x )
z + sin(angle=0.6*x)

Don't use expressions without any names in it:

>>> analyse_expression('6+7-8*4')
''

cause it doesn't work... use at least one name:

>>> print analyse_expression('6+7-z*4')
z * 4
13 - z*4

Using 'and', 'or' keywords will always behave as if their first operand
was True:

>>> print analexpr.analyse_expression('z+7 and x+1 or y')
z + 7
x + 1


The module (no copyrights, public domain):

class EvaluationObject(object):
    """A helper class for analysing expressions"""
    __slots__ = "_datum",
    def __init__(self, datum):
        self._datum = datum
    def __str__(self):
        return self._datum
    def __call__(self, *args, **kwargs):
        reply= []
        reply.append(self._datum)
        reply.append("(")
        if args:
            out_arg_list1= []
            for arg in args:
                out_arg_list1.append(str(arg).replace(' ', ''))
            reply.append(','.join(out_arg_list1))
        if kwargs:
            out_arg_list2= []
            for arg, value in kwargs.iteritems():
                out_arg_list2.append("%s=%s" % (arg, value))
            reply.append(','.join(out_arg_list2).replace(' ', ''))
        reply.append(")")
        rc = " ".join(reply)
        EvaluationObject.order.append(rc)
        return rc

# create all the (EvaluationObject.__method__)s
def _make_binary_method(operator, reverse=False):
    "Binary arithmetic operator factory function for EvaluationObject"
    def _dummy(self, other):
        if reverse: self, other = other, self
        rc = "%s %s %s" % (str(self).replace(' ',''), operator,
str(other).replace(' ',''))
        EvaluationObject.order.append(rc)
        return EvaluationObject(rc)
    return _dummy
# mass-make the arithmetic methods
for function in "add,+ sub,- mul,* floordiv,// mod,%" \
        " pow,** lshift,<< rshift,>>" \
        " and,& xor,^ or,| div,/ truediv,/" \
        " getattr,.".split():
    name, operator= function.split(",")
    setattr(EvaluationObject, "__%s__" % name,
_make_binary_method(operator))
    setattr(EvaluationObject, "__r%s__" % name,
_make_binary_method(operator, reverse=True))

def _make_unary_method(operator):
    "Unary arithmetic operator factory function for EvaluationObject"
    def _dummy(self):
        rc = "%s %s" % (operator, str(self).replace(' ', ''))
        EvaluationObject.order.append(rc)
        return EvaluationObject(rc)
    return _dummy
for function in "neg,- pos,+ invert,~".split():
    name, operator = function.split(",")
    setattr(EvaluationObject, "__%s__" % name,
_make_unary_method(operator))

# cleanup
del _make_binary_method, _make_unary_method, function, name, operator

def analyse_expression(expr):
    '''Return as string a list of the steps taken to evaluate expr'''
    code_object = compile(expr, "<evaluator>", "eval")
    namespace = {'__builtins__': {}}
    # namespace should be a dict subclass that creates items
    # on demand.
    # exec and eval assume that the namespaces are dict objects
    # and bypass any __getitem__ methods of the subclass
    # to overcome this limitation, keep trying to eval the expression
    # until no more name errors occur.
    while True:
        try:
            EvaluationObject.order = []
            eval(code_object, namespace)
        except NameError, exc:
            # exc.args[0] is of the form:
            # name 'x' is not defined
            # use hardcoded slice to get the missing name
            name = exc.args[0][6:-16]
            namespace[name] = EvaluationObject(name)
        else:
            break
    result = '\n'.join(EvaluationObject.order)
    del EvaluationObject.order
    return result

-- 
TZOTZIOY, I speak England very best,
Ils sont fous ces Redmontains! --Harddix




More information about the Python-list mailing list