Do I have to override all math operators in a subclass?

Question:

I want to make a simple Point2d class in a Python 3.7 program that implements just a few features. I saw in an SO answer (that I can’t find now) that one way to create a Point class was to override complex so I wrote this:

import math

class Point2d(complex):

    def distanceTo(self, otherPoint):
        return math.sqrt((self.real - otherPoint.real)**2 + (self.imag - otherPoint.imag)**2)

    def x(self):
        return self.real

    def y(self):
        return self.imag

This works:

In [48]: p1 = Point2d(3, 3)

In [49]: p2 = Point2d(6, 7)

In [50]: p1.distanceTo(p2)
Out[50]: 5.0

But when I do this, p3 is instance of complex, not Point2d:

In [51]: p3 = p1 + p2

In [52]: p3.distanceTo(p1)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-52-37fbadb3015e> in <module>
----> 1 p3.distanceTo(p1)

AttributeError: 'complex' object has no attribute 'distanceTo'

Most of my background is in Objective-C and C# so I’m still trying to figure out the pythonic way of doing things like this. Do I need to override all the math operators I want to use on my Point2d class? Or am I going about this completely the wrong way?

Asked By: SSteve

||

Answers:

The problem, is that your class, when it uses any of the data model functions belonging to complex It returns a complex, so you’ll need to turn this in to your Point2d class

adding this method should do the trick

def __add__(self, b):
    return Point2d(super().__add__(b))

But still there should be a better way of doing it. But this is the way to dynamically wrap some Data Model (dunder) methods.

By the way, the distance function you can make it shorter something like this

def distanceTo(self, otherPoint):
    return abs(self - otherPoint)
Answered By: ekiim

In general, prefer composition to inheritance. You can implement all the desired operations in terms of complex numbers.

class Point2D:
    def __init__(self, x, y):
        self._p = complex(x,y)

    @classmethod
    def _from_complex(self, z):
        return Point2D(z.real, z.imag)

    def __add__(self, other):
        return Point2D._from_complex(self._p + other._p)

    # etc

Is it a lot of boilerplate? Yes. But it’s not really boilerplate you can avoid.

Answered By: chepner

In this case I suggest to implement your class Point2d from scratch.

If you’re lazy, take a look to some lib like sympy which includes a Point class and other geometry stuff https://docs.sympy.org/latest/modules/geometry/index.html

Answered By: Cartucho

I’m going to mention a way of overriding all the methods without manually writing each of them, but only because we are all consenting adults here. I don’t really recommend it, it is much clearer if you just override each and every operation. That said, you can write a class wrapper which inspects all the methods of the base class and converts the output to a point if it is a complex type.

import math
import inspect


def convert_to_base(cls):
    def decorate_func(name, method, base_cls):
        def method_wrapper(*args, **kwargs):
            obj = method(*args, **kwargs)
            return cls.convert(obj, base_cls) if isinstance(obj, base_cls) else obj
        return method_wrapper if name not in ('__init__', '__new__') else method
    for base_cls in cls.__bases__:
        for name, method in inspect.getmembers(base_cls, inspect.isroutine):  # Might want to change this filter
            setattr(cls, name, decorate_func(name, method, base_cls))
    return cls


@convert_to_base
class Point2d(complex):

    @classmethod
    def convert(cls, obj, base_cls):
        # Use base_cls if you need to know which base class to convert.
        return cls(obj.real, obj.imag)

    def distanceTo(self, otherPoint):
        return math.sqrt((self.real - otherPoint.real)**2 + (self.imag - otherPoint.imag)**2)

    def x(self):
        return self.real

    def y(self):
        return self.imag

p1 = Point2d(3, 3)
p2 = Point2d(6, 7)
p3 = p1 + p2
p4 = p3.distanceTo(p1)
print(p4)

# 9.219544457292887

What is happening here is that it just checks all the methods of the base class, and if what it returns is of the type of the base class, converts it to the child class, as defined by the special classmethod in the child class.

Answered By: André C. Andersen

Maybe you are interested in this. A while ago I started working in a module called odeanimate (For time reason the development is kind of stalled).

The idea was to create a module do to math in an explicit way, such that you have objects that respect the operators in a meaningful way.

I would encourage you to check this file in particular
odeanimate/vector.py, which in my opinion is a good example on how to override the operators.

But essentially the answer is to implement all the data model methods or dunder methods, which the other answers already explained.

Answered By: ekiim
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.