Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

make operators overloading less redundant in python?

I'm writing a class overloading the list type. I just wrote this and I'm wondering if there exists any other way less redundant to do it :

class Vector:
    def __mul__(self, other):
        #Vector([1, 2, 3]) * 5 => Vector([5, 10, 15])
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(i * other)
            return Vector(tmp)
        raise VectorException("We can only mul a Vector by a scalar")

    def __truediv__(self, other):
        #Vector([1, 2, 3]) / 5 => Vector([0.2, 0.4, 0.6])
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(i / other)
            return Vector(tmp)
        raise VectorException("We can only div a Vector by a Scalar")

    def __floordiv__(self, other):
        #Vector([1, 2, 3]) // 2 => Vector([0, 1, 1])
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(i // other)
            return Vector(tmp)
        raise VectorException("We can only div a Vector by a Scalar")

As you can see, every overloaded method is a copy/paste of the previous with just small changes.

like image 482
politinsa Avatar asked Mar 06 '18 23:03

politinsa


People also ask

How does a Python class handle operator overloading?

The operator overloading in Python means provide extended meaning beyond their predefined operational meaning. Such as, we use the "+" operator for adding two integers as well as joining two strings or merging two lists. We can achieve this as the "+" operator is overloaded by the "int" class and "str" class.

Which function overloads == operator in Python?

Operator Overloading means giving extended meaning beyond their predefined operational meaning. For example operator + is used to add two integers as well as join two strings and merge two lists. It is achievable because '+' operator is overloaded by int class and str class.


2 Answers

Factorize code using decorator design pattern and lambda function:

class Vector:
    def __do_it(self, other, func):
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(func(i, other))
            return Vector(tmp)
        raise ValueError("We can only operate a Vector by a scalar")

    def __mul__(self, other):
        return self.__do_it(other, lambda i, o: i * o)

    def __truediv__(self, other):
        return self.__do_it(other, lambda i, o: i / o)

    def __floordiv__(self, other):
        return self.__do_it(other, lambda i, o: i // o)
like image 51
Ramazan Polat Avatar answered Sep 22 '22 08:09

Ramazan Polat


What you want to do here is dynamically generate the methods. There are multiple ways to do this, from going super-dynamic and creating them on the fly in a metaclass's __getattribute__ (although that doesn't work for some special methods—see the docs) to generating source text to save in a .py file that you can then import. But the simplest solution is to create them in the class definition, something like this:

class MyVector:
    # ...

    def _make_op_method(op):
        def _op(self, other):
            if isinstance(other, int) or isinstance(other, float):
                tmp = list()
                for i in self.l:
                    tmp.append(op(i. other))
                return Vector(tmp)
            raise VectorException("We can only {} a Vector by a scalar".format(
                op.__name__.strip('_'))
        _.op.__name__ = op.__name__
        return _op

    __mul__ = _make_op(operator.__mul__)
    __truediv__ = _make_op(operator.__truediv__)
    # and so on

You can get fancier and set _op.__doc__ to an appropriate docstring that you generate (see functools.wraps in the stdlib for some relevant code), and build __rmul__ and __imul__ the same way you build __mul__, and so on. And you can write a metaclass, class decorator, or function generator that wraps up some of the details if you're going to be doing many variations of the same thing. But this is the basic idea.

In fact, moving it outside the class makes it easier to eliminate even more repetition. Just define that _op(self, other, op) method in the class instead of locally inside _make_op and decorate the class with @numeric_ops, which you can define like this:

def numeric_ops(cls):
    for op in "mul truediv floordiv".split():  # "mul truediv floordiv ... ".split():
        def _op(self, other):
            return self._op(other, getattr(operator, op)
        _op.__name__ = f"__{op}__"
        setattr(cls, f"__{op}__", _op)

If you look at, e.g., functions.total_ordering, it does something similar to generate any missing ordering ops out of the ones that are there.

The operator.mul, etc., come from the operator module in the stdlib—they're just trivial functions where operator.__mul__(x, y) basically just calls x * y, and so on, made for when you need to pass around an operator expression as a function.

There are some examples of this kind of code in the stdlib—although far more examples of the related but much simpler __rmul__ = __mul__.

The key here is that there's no difference between names you create with def and names you create by assigning with =. Either way, __mul__ becomes an attribute of the class, and its value is a function that does what you want. (And, similarly, there’s almost no difference between names you create during class definition and names you inject afterward.)


So, should you be doing this?

Well, DRY is important. If you copy-paste-edit a dozen times it’s not unlikely that you’ll screw up one of the edits and end up with a mod method that actually multiples and that (and a unit test that doesn’t catch it). And then, if you later discover a flaw in the implementation you copied and pasted a dozen times (as between the original and edited version of the question), you have to fix the same flaw in a dozen places, which is yet another potential bug magnet.

On the other hand, readability counts. If you don't understand how that works, you probably shouldn't be doing this, and should settle for Ramazan Polat's answer. (It's not quite as compact, or as efficient, but it's surely easier to understand.) After all, if the code isn’t obvious to you, the fact that you only have to fix a bug once instead of a dozen times is swamped by the fact that you don’t know how to fix it. And even if you do understand it, the cost of cleverness can often outweigh the benefits of DRY.

I think total_ordering shows about where you’d want to draw the line. If you’re doing this one time, you’re better off with the repetition, but if you’re doing it for multiple classes or in multiple projects, you’re probably better off abstracting the cleverness into a library that you can write once, exhaustive test with a variety of different classes, and then use over and over.

like image 38
abarnert Avatar answered Sep 19 '22 08:09

abarnert