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

Viewed 158

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?

4 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)

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.

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.

Related