How do you do a python 'eval' only within

2019-03-27 05:42发布

问题:

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'))    

回答1:

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('a', 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).



回答2:

So, I advice you to do something like this:

>>> class S(object):
...     def add(self, a, b):
...         return a + b
... 
>>> filter(lambda (k,v): not k.startswith("__"), S.__dict__.items())
[('add', <function add at 0x109cec500>)]
>>> target = S()
>>> map(lambda (k, f): (k, f.__get__(target, S)), filter(lambda (k,v): not k.startswith("__"), S.__dict__.items()))
[('add', <bound method S.add of <__main__.S object at 0x109ce4ed0>>)]
>>> dict(_)
{'add': <bound method S.add of <__main__.S object at 0x109ce4ed0>>}
>>> eval("add(45, 10) + add(10, 1)", _, {})
66

Seems like that you need. Let me explain how this works.

  1. eval accepts locals and globals as parameters.
  2. So we need to define special global context which will be "representation" of your class.
  3. To do this, we need to provide as globals dictionary of all "valuable" bounded methods.
  4. Starting from simples part. We have S class definition. How to get all "valuable" methods? Simple filter names from S.__dict__ in order to check whether method name starts from __ or not (you see, that as result we get list with 1 item - add function).
  5. Create target = instance of S class which will be "eval context".
  6. Next step is most "tricky". We need to create "bound method" from each function. To do this, we use those fact, that class __dict__ stores functions, each function is non-data descriptor and bounded method can be fetched simply with func.__get__(obj, type(obj)). This operation is performed in map.
  7. Take result from previous step, create dict from it.
  8. Pass as globals to eval function.

I hope, this will help.



回答3:

The solution proposed above to populate locals works well in most cases, but may be problematic in case of properties (data descriptors). These are evaluated once when the dictionary is populated. This means that multiple references to the same variable name will always return the exact same instance, which may not be expected behavior in case of properties.

This problem can be solved by noting that eval expects a locals argument that behaves like a dict (as opposed to globals, which must be a dict). In other words, we can override __getitem__ in your instance to resolve variable names on the fly in the context of the instance and pass it directly as locals attribute to eval. Your example can thus be implemented as:

class Formula(object):
    def __getitem__(self, key):
        if key not in dir(self) or key.startswith('__'):
            raise KeyError(key)
        return getattr(self, key)

    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))"
    print eval(evalString, {}, Formula())

if __name__ == "__main__":
    main()

This trick should work with inheritance, static methods, class methods and properties. Finally, using dir and getattr avoids the need to interact directly with __dict__ or __mro__, though the results of dir may not always be complete.



回答4:

You might have a look at the accepted answer to this question: "Getting the block of commands that are to be executed in the with-statement".

This has been a helpful way for me to create my own contexts in which math operations on rectangular arrays, such as Python Pandas data frames, "just work" without needing to bother with the ugly extra Pandas syntax. For example, when I write "a = x*y" inside of the context, it automatically assigns a as an attribute to the context object, and it knows to perform vectorial operations with the context object's x and y attributes.

I've found this context stuff to be very very helpful, despite the fact that whenever I ask on StackOverflow, I often get trollish responses that it must not be what I really want to do.

You could probably get this to work for the context in which eval looks for functions too.