Search code examples
pythonevalabstract-syntax-tree

How do you do a python 'eval' only within an object context?


Is it possible to do something like

c = MyObj()
c.eval("func1(42)+func2(24)")

in Python..i.e. have func1() and func2() be evaluated within the scope of the object 'c' (if they were member functions within that class definition)? I can't do a simple parsing, since for my application the eval strings can become arbitrarily complicated. I guess doing some magic with the ast module might do the trick, but due to the dirth of literature on ast, I'm not sure where to look:

import ast

class MyTransformer(ast.NodeTransformer):
    def visit_Name(self, node):
        # do a generic_visit so that child nodes are processed
        ast.NodeVisitor.generic_visit(self, node)
        return ast.copy_location(
            # do something magical with names that are functions, so that they become 
            # method calls to a Formula object
            newnode,
            node
        )

class Formula(object):

    def LEFT(self, s, n):
        return s[:n]

    def RIGHT(self, s, n):
        return s[0-n:]

    def CONCAT(self, *args, **kwargs):
        return ''.join([arg for arg in args])

def main():

    evalString = "CONCAT(LEFT('Hello', 2), RIGHT('World', 3))"

    # we want to emulate something like Formula().eval(evalString)
    node = ast.parse(evalString, mode='eval')
    MyTransformer().visit(node)

    ast.fix_missing_locations(node)
    print eval(compile(node, '<string>', mode='eval'))    

Solution

  • You almost certainly don't want to do this, but you can.

    The context for eval is the globals and locals dictionaries that you want to evaluate your code in. The most common cases are probably eval(expr, globals(), mycontext) and eval(expr, mycontext), which replace the default local and global contexts, respectively, leaving the other alone. Replacing the local context with an object's dictionary is similar to running "within" (a method of) that object—although keep in mind that "being a member function" doesn't do as much good as you might expect if you don't have a self to call other member functions on…

    Anyway, here's a quick example:

    >>> class Foo(object):
    ...     def __init__(self):
    ...         self.bar = 3
    >>> foo = Foo()
    >>> eval('bar', globals(), foo.__dict__)
    3
    

    Keep in mind that __dict__ may not be exactly what you want here. For example:

    >>> class Foo(object):
    ...     @staticmethod
    ...     def bar():
    ...         return 3
    >>> foo = Foo()
    >>> eval('bar()', globals(), foo.__dict__)
    NameError: name 'bar' is not defined
    >>> eval('bar()', globals(), {k: getattr(foo, k) for k in dir(foo)}
    3
    

    To make this work the way you want, you have to know exactly how to define you want, in Python terms—which requires knowing a bit about how objects works under the covers (MRO, maybe descriptors, etc.).

    If you really need eval, and you really need to provide arbitrary contexts, you're probably better building those contexts explicitly (as dictionaries) rather than trying to force objects into that role:

    >>> foo = {
    ...     'bar': lambda: 3
    ... }
    >>> eval('bar()', globals(), foo)
    

    This use is much closer to the Javascript style you're trying to emulate in Python anyway.

    Of course, unlike JS, Python doesn't let you put multi-line definitions inside an expression, so for complex cases you have to do this:

    >>> def bar():
    ...     return 3
    >>> foo = {
    ...     'bar': bar
    ... }
    >>> eval('bar()', globals(), foo)
    

    But arguably that's almost always more readable (which is basically the argument behind Python not allowing multi-line definitions in expressions).