5

I am experimenting with ways to implement a simplified Term Rewriting System (TRS)/Symbolic Algebra System in Python. For this I would really like to be able to be able to intercept and modify the operands in particular cases during the class instance instantiation process. The solution I came up with, was to create a metaclass that modifies the typical call behavior of a class object (of type 'type').

class Preprocess(type):
    """
    Operation argument preprocessing Metaclass.
    Classes using this Metaclass must implement the 
        _preprocess_(*operands, **kwargs)
    classmethod.
    """

    def __call__(cls, *operands, **kwargs):
        pops, pargs = cls._preprocess_(*operands, **kwargs)
        return super(Preprocess, cls).__call__(*pops, **pargs)

An example case would be to expand out nested operations F(F(a,b),c)-->F(a,b,c)

class Flat(object):
    """
    Use for associative Operations to expand nested 
    expressions of same Head: F(F(x,y),z) => F(x,y,z)
    """
    __metaclass__ = Preprocess
    @classmethod
    def _preprocess_(cls, *operands, **kwargs):
        head = []
        for o in operands:
            if isinstance(o, cls):
                head += list(o.operands)
            else:
                head.append(o)
        return tuple(head), kwargs

So, now this behavior can be realized through inheritance:

class Operation(object):
    def __init__(self, *operands):
        self.operands = operands

class F(Flat, Operation):
    pass

This leads to the desired behavior:

print F(F(1,2,3),4,5).operands
(1,2,3,4,5)

However, I would like to combine several such preprocessing classes and have them process the operands sequentially according to the natural class mro.

class Orderless(object):
    """
    Use for commutative Operations to bring into ordered, equivalent 
    form: F(*operands) => F(*sorted(operands))
    """
    __metaclass__ = Preprocess

    @classmethod
    def _preprocess_(cls, *operands, **kwargs):

        return sorted(operands), kwargs

And this does not seem to work as wanted. Defining a Flat and Orderless Operation type

class G(Flat, Orderless, Expression):
    pass

results in only the first Preprocessing superclass being 'active'.

print G(G(3,2,1),-1,-3).operands
(3,2,1,-1,-3)

How can I ensure that all Preprocessing classes' preprocess methods are called before class instantiation?

Update:

I can't seem to formally answer my question yet due to my status as new stackoverflow user. So, I believe this is probably the best solution I can come up with:

class Preprocess(type):
    """
    Abstract operation argument preprocessing class.
    Subclasses must implement the 
        _preprocess_(*operands, **kwargs)
    classmethod.
    """

    def __call__(cls, *operands, **kwargs):
        for cc in cls.__mro__:
            if hasattr(cc, "_preprocess_"):
                operands, kwargs = cc._preprocess_(*operands, **kwargs)

        return super(Preprocess, cls).__call__(*operands, **kwargs)

I guess the problem is that super(Preprocess, cls).__call__(*operands, **kwargs) does not traverse the mro of cls as expected.

7
  • Override the allocator and have it examine the bases. Commented May 9, 2012 at 19:09
  • Hm, how would that work exactly? Printing out the bases of the cls object in the Preprocess.__call__ method yields (as expected) (<class 'main.Flat'>, <class 'main.Orderless'>,<class 'main.Operation'>) Commented May 9, 2012 at 19:14
  • You need to return an object of a dynamic type. Which is why you do it in the allocator. Commented May 9, 2012 at 19:23
  • By allocator, do you mean the the "__new__()" method? I was doing this before, but the problem with that is, if I change the operands in the new method, but return an object of the same type, the constructor (init) receives the original unprocessed operands as its arguments... Commented May 9, 2012 at 20:40
  • 1
    The entire purpose of the allocator is to produce a whole new type. Commented May 9, 2012 at 21:38

1 Answer 1

1

I think you're going about this the wrong way; drop the metaclass, use class and method decorators instead.

For example, define your flat as:

@init_args_preprocessor
def flat(operands, kwargs): # No need for asterisks
    head = []
    for o in operands:
        if isinstance(o, cls):
            head += list(o.operands)
        else:
            head.append(o)
    return tuple(head), kwargs

With the init_args_preprocessor decorator turning that function into a class decorator:

def init_args_preprocessor(preprocessor):
    def class_decorator(cls):
        orig_init = cls.__init__
        def new_init(self, *args, **kwargs):
            args, kwargs = preprocessor(args, kwargs)
            orig_init(self, *args, **kwargs)
        cls.__init__ = new_init
        return cls
   return class_decorator

And now, instead of mixins use decorators:

class Operation(object):
    def __init__(self, *operands):
        self.operands = operands

@flat
class F(Operation):
    pass

And you should have no problem combining the class modifiers cleanly:

@init_args_preprocessor
def orderless(args, kwargs):
    return sorted(args), kwargs

@orderless
@flat
class G(Expression):
   pass

Caveat Emptor: All code above strictly untested.

Sign up to request clarification or add additional context in comments.

3 Comments

Hi, yes that looks like a very nice solution and it turns out I am actually now doing something along those lines. One issue with this is that the object 'G' will not be a class anymore but rather a function, so isinstance() checks will fail without further tweaking.
Why is G a function? It is only modified by class decorators. @init_args_preprocessor turns flat into a function that takes a class and returns a class.
My bad, you are correct. This is, in fact, exactly the way I ended up doing it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.