Jason S
Jason S

Reputation: 189836

python: generalizing delegating a method

I have a class like this containing one or more numeric elements.

class Foo:
    # ... other methods ...
    def _update(self, f):
        # ... returns a new Foo() object based on transforming
        #     one or more data members with a function f()
    def __add__(self, k):
        return self._update(lambda x: x.__add__(k))
    def __radd__(self, k):
        return self._update(lambda x: x.__radd__(k))
    def __sub__(self, k):
        return self._update(lambda x: x.__sub__(k))
    def __rsub__(self, k):
        return self._update(lambda x: x.__rsub__(k))
    def __mul__(self, k):
        return self._update(lambda x: x.__mul__(k))
    def __rmul__(self, k):
        return self._update(lambda x: x.__rmul__(k))
    def __div__(self, k):
        return self._update(lambda x: x.__div__(k))
    def __rdiv__(self, k):
        return self._update(lambda x: x.__rdiv__(k))
    # I want to add other numeric methods also

Is there any way to generalize this for all the numeric methods, without having to do this for each and every one of them?

I just want to delegate for any method in the list of numeric methods.

Upvotes: 3

Views: 567

Answers (3)

Jon Gauthier
Jon Gauthier

Reputation: 25592

Use a class decorator:

def add_numerics(klass):
    for numeric_fn in ['add','radd','sub','rsub','mul','rmul','div','rdiv']:
        dunder_fn = '__{}__'.format(numeric_fn)
        setattr(klass, dunder_fn, lambda self, k: self._update(lambda x: getattr(x, dunder_fn)(k)))
    return klass

@add_numerics
class Foo:
    def _update(self, f):
        # ...
        return Foo()

Upvotes: 2

Martijn Pieters
Martijn Pieters

Reputation: 1124258

You want to use the operator module here, together with a (short) list of binary numeric operator names, without the underscores for compactness:

import operator 

numeric_ops = 'add div floordiv mod mul pow sub truediv'.split()

def delegated_arithmetic(handler):
    def add_op_method(op, cls):
        op_func = getattr(operator, op)
        def delegated_op(self, k):
            getattr(self, handler)(lambda x: op_func(x, k))
        setattr(cls, '__{}__'.format(op), delegated_op)

    def add_reflected_op_method(op, cls):
        op_func = getattr(operator, op)
        def delegated_op(self, k):
            getattr(self, handler)(lambda x: op_func(k, x))
        setattr(cls, '__r{}__'.format(op), delegated_op)

    def decorator(cls):
        for op in numeric_ops:
            add_op_method(op, cls)
            add_reflected_op_method(op, cls) # reverted operation
            add_op_method('i' + op, cls)     # in-place operation
        return cls

    return decorator

Now just decorate your class:

@delegated_arithmetic('_update')
class Foo:
    # ... other methods ...
    def _update(self, f):
        # ... returns a new Foo() object based on transforming
        #     one or more data members with a function f()

The decorator takes the name you wanted to delegate the call to to make this a little more generic.

Demo:

>>> @delegated_arithmetic('_update')
... class Foo(object):
...     def _update(self, f):
...         print 'Update called with {}'.format(f)
...         print f(10)
... 
>>> foo = Foo()
>>> foo + 10
Update called with <function <lambda> at 0x107086410>
20
>>> foo - 10
Update called with <function <lambda> at 0x107086410>
0
>>> 10 + foo
Update called with <function <lambda> at 0x107086410>
20
>>> 10 - foo
Update called with <function <lambda> at 0x107086410>
0

Upvotes: 3

chepner
chepner

Reputation: 532093

This might be too general, as it will dispatch any attribute not explicitly declared.

class Foo:
    def _update(self, f):
        ...

    def __getattr__(self, attr):
        def __(self, k):
            return self._update( lambda x: x.__getattr__(attr)(k) )
        return __

You can make it a little more selective:

    def __getattr__(self, attr):
        if attr in ['__add__', '__radd__', ... ]:
            def __(self, k):
                return self._update( lambda x: x.__getattr__(attr)(k) )
            return __
        else:
            raise AttributeError("Unknown attribute: {0}".format(attr))

Upvotes: 0

Related Questions