On Thu, 3 Nov 2005, Paul Cochrane wrote:

> On Wed, 02 Nov 2005 06:33:28 +0000, Bengt Richter wrote:
>
>> On Wed, 2 Nov 2005 06:08:22 +0000 (UTC), Paul Cochrane <[EMAIL PROTECTED]> 
>> wrote:
>>
>>> I've got an application that I'm writing that autogenerates python 
>>> code which I then execute with exec().  I know that this is not the 
>>> best way to run things, and I'm not 100% sure as to what I really 
>>> should do.  I've had a look through Programming Python and the Python 
>>> Cookbook, which have given me ideas, but nothing has gelled yet, so I 
>>> thought I'd put the question to the community.  But first, let me be a 
>>> little more detailed in what I want to do:

Paul, this is a rather interesting problem. There are two aspects to it, 
which i believe are probably separable: getting instructions from the 
client to the server, and getting data back from the server to the client. 
The former is more complex, i think, and what's attracted the attention so 
far.

The first thing i'd say is that, while eval/exec is definitely a code 
smell, that doesn't mean it's never the right solution. If you need to be 
able to express complex things, python code might well be the best way to 
do it, and the best way to evaluate python code is eval/exec.

>> It's a little hard to tell without knowing more about your user input 
>> (command language?) syntax that is translated to or feeds the process 
>> that "autogenerates python code".
>
> It's basically just a command language I guess.

Hang on - the stuff that the user writes is what you're calling "pyvisi 
code", is that right? That doesn't look like 'just a command language', 
that looks like python, using a library you've written. Or is there 
another language, the "just a command language", on top of that?

And what you call "vtk-python code" - this is python again, but using the 
renderer's native library, right?

And you generate the vtk-python from the pyvisi-python by executing the 
pyvisi-python, there being (pluggable renderer-specific) logic in the guts 
of your pyvisi classes to emit the vtk-python code, right? You're not 
parsing anything?

>> There are lots of easy things you could do without generating and exec-ing
>> python code per se.
>
> I'd love to know of other options.  I like the idea of generating the 
> code one would have to write for a particular renderer so that if the 
> user wanted to, they could use the autogenerated code to form the basis 
> of a specific visualisation script they could then hack themselves.

If you want vtk-python code as an intermediate, i think you're stuck with 
eval/exec [1].

> One of the main ideas of the module is to distill the common visualisation
> tasks down to a simple set of commands, and then let the interface work out
> how to actually implement that.

Okay. There's a classic design pattern called Interpreter that applies 
here. This is one of the more complex patterns, and one that's rather 
poorly explained in the Gang of Four book, so it's not well-known.

Basically, the idea is that you provide classes which make it possible for 
a program to build structures encoding a series of instructions - 
essentially, you define a language whose concrete syntax is objects, not 
text - then you write code which takes such structures and carries out the 
instructions encoded in them - an interpreter, in other words.

For example, here's a very simple example for doing basic arithmetic:

# the language

class expression(object):
        pass

class constant(expression):
        def __init__(self, value):
                self.value = value

class unary(expression):
        def __init__(self, op, arg):
                self.op = op
                self.arg = arg

class binary(expression):
        def __init__(self, op, arg_l, arg_r):
                self.op = op
                self.arg_l = arg_l
                self.arg_r = arg_r

# the interpreter

UNARY_OPS = {
        "-": lambda x: -x,
        "|": lambda x: abs(x) # apologies for abnormal syntax
}

BINARY_OPS = {
        "+": lambda l, r: l + r,
        "-": lambda l, r: l - r,
        "*": lambda l, r: l * r,
        "/": lambda l, r: l / r,
}

def evaluate(expr):
        if isinstance(expr, constant):
                return expr.value
        elif isinstance(expr, unary):
                op = UNARY_OPS[expr.op]
                arg = evaluate(expr.arg)
                return op(arg)
        elif isinstance(expr, binary):
                op = BINARY_OPS[expr.op]
                arg_l = evaluate(expr.arg_l)
                arg_r = evaluate(expr.arg_r)
                return op(arg_l, arg_r)
        else:
                raise Exception, "unknown expression type: " + str(type(expr))

# a quick demo

expr = binary("-",
        binary("*",
                constant(2.0),
                constant(3.0)),
        unary("-",
                binary("/",
                        constant(4.0),
                        constant(5.0))))

print evaluate(expr)

This is by no means a useful or well-designed bit of code, and there are 
several things that could have been done differently (bare vs wrapped 
constants, operations defined by a symbol vs expression subtypes for each 
operation, etc), but i hope it gets the idea across - representing a 
language using an object graph, which lets you write programs that can 
speak that language.

Your code is already doing something a bit like this - you build scene 
graphs, then call render on them to get them to do something. Instead of 
that, you'd pass the whole scene to a renderer object, which would do the 
rendering (directly, rather than by generating code). The point is that 
the renderer could be in another process, provided you have a way to move 
the scene graph from one process to another - the pickle module, for 
example, or a custom serialisation format if you feel like reinventing the 
wheel.

An approach like this has a natural solution to your second problem, too - 
the evaluator function can return objects, which again can just be pickled 
and sent over the network.

tom

[1] Okay, so there is a way to do this without ever actually creating 
python code. You're not going to like this.

You need to apply the interpreter pattern to python itself. Well, a 
simplified subset of it. Looking at your generated vtk-python code, you 
basically do the following things:

- call methods with variables and literals as arguments
        - throwing away the result
        - or keeping it in a variable
- do for loops over ranges of integers

To make things a bit simpler, i'm going to add:

- getting attributes
- subscripting arrays
- return a value

You also need to do some arithmetic, i think; i leave that as an exercise 
for the reader.

So we need a language like:

class statement(object):
        pass

class invoke(statement):
        def __init__(self, var, target, method, args):
                self.var = var # name of variable for result; None to throw away
                self.target = target
                self.method = method
                self.args = args

class get(statement):
        def __init__(self, var, target, field):
                self.var = var
                self.target = target
                self.field = field

class subscript(statement):
        def __init__(self, var, target, index):
                self.var = var
                self.target = target
                self.index = index

class forloop(statement):
        def __init__(self, var, limit, body):
                self.var = var
                self.limit = limit
                self.body = body # tuple of statements

def return_(statement):
        def __init__(self, value):
                self.value = value

With which we can write a script like:

vtk_script = [
        invoke("_plot", "vtk", "vtkXYPlotActor", ()),
        invoke("_renderer", "vtk", "vtkRenderer", ()),
        invoke("_renderWindow", "vtk", "vtkRenderWindow", ()),
        invoke(None, "_renderWindow", "AddRenderer", ("_renderer")),
        invoke(None, "_renderWindow", "SetSize", (640, 480)),
        invoke(None, "_renderer", "SetBackground", (1, 1, 1)),
        invoke("_x", None, "array", ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 
8.0, 9.0],)),
        get("vtk_vtkDataArray", "vtk", "vtkDataArray"),
        get("vtk_VTK_FLOAT", "vtk", "VTK_FLOAT"),
        invoke("_xData", "vtk_vtkDataArray", "CreateDataArray", 
("vtk_VTK_FLOAT",)),
        invoke("_x_len", None, "len", ("_x",)),
        invoke(None, "_xData", "SetNumberOfTuples", ("_x_len",)),
        invoke("_y0", None, "array", ([0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 
49.0, 64.0, 81.0],)),
        invoke("_y0Data", "vtk_vtkDataArray", "CreateDataArray", 
("vtk_VTK_FLOAT",)),
        invoke("_y0_len", None, "len", ("_y0",)),
        invoke(None, "_y0Data", "SetNumberOfTuples", ("_y0_len",)),
        forloop("i", "_x_len", (
                subscript("_x_i", "_x", "i"),
                invoke(None, "_xData", "SetTuple1", ("i", "_x_i")),)),
        # etc
]

Or rather, your pyvisi classes can generate structures like this, exactly 
as they currently generate code.

Code to convert this into python source is trivial, so i'll gloss over 
that. The interpreter looks like this:

def isiterable(x):
        return hasattr(x, "__iter__")

def execute(script, vars=None):
        if (vars == None):
                vars = {}
                # initialise variables with 'vtk' and anything else you need
        def decode_arg(arg):
                if (isinstance(arg, str)):
                        return vars[arg]
                elif (isiterable(arg)):
                        return map(decode_arg, arg)
                else:
                        return arg
        for stmt in script:
                if (isinstance(stmt, invoke)):
                        target = vars[stmt.target]
                        method = getattr(target, stmt.method)
                        args = decode_arg(stmt.args)
                        result = method(*args)
                        if (stmt.var != None):
                                vars[stmt.var] = result
                elif (isinstance(stmt, get)):
                        target = vars[stmt.target]
                        vars[stmt.var] = getattr(target, stmt.field)
                elif (isinstance(stmt, subscript)):
                        target = vars[stmt.target]
                        vars[stmt.var] = getattr(target, decode_arg(stmt.index))
                elif (isinstance(stmt, forloop)):
                        var = stmt.var
                        limit = decode_arg(stmt.limit)
                        body = stmt.body
                        for i in range(limit):
                                vars[var] = i
                                execute(body, vars)
                elif (isinstance(stmt, return_)):
                        return vars[stmt.value]
                        # nb won't work from inside a for loop!
                        # you can use an exception to handle returns properly

Note that i haven't tested this, i've just written it off the top of my 
head, so it probably won't work, but maybe you get the idea.

To be honest, i'd go with exec.

-- 
Fitter, Happier, More Productive.
-- 
http://mail.python.org/mailman/listinfo/python-list

Reply via email to