Programmatically create arithmetic special methods

2019-04-29 23:48发布

My idea is to create particular function objects that can be summed/subtracted/... together, returning a new function object that has the same properties. This example code, hopefully, demonstrates the idea:

from FuncObj import Func

# create some functions
quad = Func(lambda x: x**2)
cube = Func(lambda x: x**3)

# now combine functions as you like
plus = quad + cube
minus = quad - cube
other = quad * quad / cube

# and these can be called
plus(1) + minus(32) * other(5)

I have written the following code, which is hopefully commented and documented enough to explain what i want to achieve.

import operator

class GenericFunction(object):
    """ Base class providing arithmetic special methods. 
        Use derived class which must implement the 
        __call__ method.
    """

    # this way of defining special methods works well
    def __add__(self, operand):
        """ This is an example of a special method i want to implement. """
        obj = GenericFunction()
        # this is a trick from Alex Martelli at
        # http://stackoverflow.com/questions/1705928/problem-with-making-object-callable-in-python
        # to allow per-instance __call__ methods
        obj.__class__ = type(obj.__class__.__name__, (obj.__class__,), {})
        obj.__class__.__call__ = lambda s, ti: self(ti) + operand(ti)
        return obj

    # on the other hand this factory function seems buggy 
    def _method_factory(operation, name):
        """ Method factory.
        Parameters
        ----------
        op : callable
            an arithmetic operator from the operator module
        name : str
            the name of the special method that will be created
        Returns
        -------
        method : callable
            the __***__ special method
        """
        def method(s, operand):
            obj = GenericFunction()
            obj.__class__ = type(obj.__class__.__name__, (obj.__class__,), {})
            obj.__class__.__call__ = lambda s, ti: operation(s(ti), operand(ti))
            return obj
        return method

    __sub__ = _method_factory(operator.__sub__, '__sub__')
    __mul__ = _method_factory(operator.__mul__, '__mul__')
    __truediv__ = _method_factory(operator.__truediv__, '__div__')


class Func(GenericFunction):
    """ A customizable callable object. 
        Parameters
        ----------
        func : callable
    """
    def __init__(self, func):
        self.func = func

    def __call__(self, *args):
        return self.func(*args)


if __name__ == '__main__':

    # create some functions
    quad = Func(lambda x: x**2)
    cube = Func(lambda x: x**3)

    # now combine functions
    poly_plus = quad + cube
    poly_minus = quad - cube

    # this is the expected behaviour, and it works well
    # since the __add__ method is defined correctly.
    assert quad(1) + cube(1) == poly_plus(1)

    # this, and the others with * and / result in a "maximum recursion depth exceeded"
    assert quad(1) - cube(1) == poly_minus(1)

I think I'm missing something important but I can't see it.

EDIT

After Dietrich answer i forgot to mention a corner case. Suppose i want to subclass GenericInput and i need to customize the call method__, without passing a callable to the constructor. I have to examples, (actually this is the code for which i originally posted this question).

class NoiseInput(GenericInput):
    def __init__(self, sigma, a, b, t):
        """ A band-pass noisy input. """
        self._noise = lfilter(b, a, np.random.normal(0, 1, len(t)))
        self._noise *= sigma/self._noise.std()
        self._spline = InterpolatedUnivariateSpline(t, self._noise, k=2)

    def __call__(self, ti):
        """ Compute value of the input at a given time. """
        return self._spline(ti)

class SineInput(GenericInput):
    def __init__(self, A, fc):
        self.A = A
        self.fc = fc

    def __call__(self, ti):
        return self.A*np.sin(2*np.pi*ti*self.fc)

In this case there is some more work to do.

2条回答
Juvenile、少年°
2楼-- · 2019-04-30 00:31

There's a lot of code here which doesn't need to exist, and it's more complicated than it needs to be.

For example, the __class__ attribute is one of the so-called "magic" attributes. Magic attributes are special and only need to be used in special cases, like when you are using metaprogramming. There is no need to create a class by code here.

Another example is the Func class in your code, which doesn't actually do anything. You can safely replace it with:

def Func(x):
    return x

So you have the opposite problem: you are not "missing" anything, you have way too much.

class Func(object):
    def __init__(self, func):
        self._func = func
    def __call__(self, x):
        return self._func(x)
    def __mul__(self, other):
        return Func(lambda x: self(x) * other(x))
    def __add__(self, other):
        return Func(lambda x: self(x) + other(x))
    def __sub__(self, other):
        return Func(lambda x: self(x) - other(x))

Note that this is not the traditional way of accomplishing such a problem. Traditionally, one avoids lambdas and uses an expression tree here. The advantage of using an expression tree is that the resulting expressions can be manipulated algebraically. For example, you can solve them, compute exact derivatives, or print them out as equations.

查看更多
仙女界的扛把子
3楼-- · 2019-04-30 00:32

I'm assuming that you'd want f(x ** 2) + f(x ** 3) to return a function x**2 + x**3? You could try this:

class Func:
    def __init__(self, func):
        self._func = func

    def __call__(self, *args):
        return self._func(*args)

    def __add__(self, other):
        def result(*args):
            return self._func(*args) + other(*args)
        return Func(result)
    __radd__ = __add__

    def __mul__(self, other):
        def result(*args):
            return self._func(*args) * other(*args)
        return Func(result)
    __rmul__ = __mul__

    # etc...

which works for me and is much simpler than what you have.

EDIT:

You could probably not even bother with the self._func calls in the arithmetic methods and just call self directly.

查看更多
登录 后发表回答