0

I have a large class definition, and for clarity would like to seperate out the arithmatic dunder methods in a seperate file. These methods, however, need to be able to return a new instance of the class, which causes a cyclical import, a name error, or an ugly unmaintainability.

My motivation for wanting to do this is at the end.

Here is a miminal working example. Put all in one file...

#classa.py
class A:
    def __init__(self, val):
        self.val = val
    def __add__(self, other):
        return A(self.val + other.val)

...it works:

from .classa import A
a1 = A(30)
a2 = A(21)
(a1 + a2).val # 51

Now, trying to put the __add__ method into a seperate file.

What does not work:

Attempt 1

#classa.py

from .classa_add import Aarithmatic

class A(Aarithmatic):
    def __init__(self, val):
        self.val = val


#classa_add.py

from .classa import A

class Aarithmatic:
    def __add__(self, other):
        return A(self.val + other.val)

Here we understandably get an error due to cyclic imports:

from .classa import A  # <-- ImportError
a1 = A(30)
a2 = A(21)
(a1 + a2).val

Attempt 2

Just in case people point me toward this answer :)

#classa.py

from .classa_add import Aarithmatic

class A(Aarithmatic):
    def __init__(self, val):
        self.val = val


#classa_add.py

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING: 
    from .classa import A

class Aarithmatic:
    def __add__(self, other):
        return A(self.val + other.val)

Here we have a nameerror when trying to add the instances, as A is not known. Which makes sense, the 'solution' used here is for when A is used in annotations only:

from .classa import A
a1 = A(30)
a2 = A(21)
(a1 + a2).val # <-- NameError: name 'A' is not defined # (in __add__)

Attempt 3

I could do it like this...

#classa.py

class A:
    def __init__(self, val):
        self.val = val

#classa_add.py

from .classa import A

def _a_add(self, other):
    return A(self.val + other.val)

A.__add__ = _a_add

# Or as oneliner: 
A.__add__ = lambda self, other: A(self.val + other.val)

but this is first of all quite ugly and unreadable, and second of all I have to import classa_add just to add the addition functionality, which is confusing:

from .classa import A
import .classa_add  # <- unclear to reader, why this needs to be here
a1 = A(30)
a2 = A(21)
(a1+a2).val # 51

If this were a package, I could add an __init__.py file to do this, but it's still ugly, and I'm sure there's a better solution.

Does anyone know, what that better solution is?

Many thanks!

I'm using python 3.8, btw.


Edit

Reasoning. Why do I even want to do this?

I have two classes (A and B) that are similar and each have quite a lot of methods. Think along the lines of pandas DataFrame and Series classes, just a bit smaller and not so similar that it is possible to have them inherit from a common parent. Currently, each class has a file in which it is defined (classa.py and classb.py).

Now, These files are firstly too long, and secondly, arithmatic on the instances is very similar.

This makes me want to create a seperate file that defines what it means to do a1 + a2, b1 + b2, a1 * a2, b1 * b2, but also a + b and a * b.

I hope this makes it clearer why I want to do this, and I hope you agree it is more maintainable to split the class definitions up this way.

ElRudi
  • 2,122
  • 2
  • 18
  • 33
  • 3
    I don't think I've ever seen anything like this before. It looks like an XY problem – roganjosh Nov 02 '21 at 00:00
  • 1
    You could write a mixin class maybe, or figure out some cleaner inheritance structure. But `A.__add__ = _a_add` feels outright bizarre and I think I'd be totally surprised by this. I don't understand why it's not in the main class definition – roganjosh Nov 02 '21 at 00:03
  • I agree with the XY problem, there's no way this is the best way to do whatever you want to do – CJR Nov 02 '21 at 00:13
  • I've added my reasoning to the end of the question – ElRudi Nov 02 '21 at 10:42

1 Answers1

4

Your Aarithmatic class can get a reference to A when its methods are called, via the self argument. It doesn't need to import A at all. If __add__ is being called on an A instance, then self.__class__ (or type(self)) should give us A.

So you could use:

def __add__(self, other):
    return self.__class__(self.val + other.val)

But I'd also like to challenge the premise of the question, that it's a good idea to split these classes up (and especially to put them in separate files). If you find that you're writing very tightly integrated code, that code should all be located together. Inheritance and modules are very nice tools, but if you find they are getting in your way, it may be a sign that they're not the right tool for your current problem.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Thanks Blckknght, that looks like it might work. But I think we have the same knee-jerk reaction to seeing dunder-methods being called like this - taht there must be a better way to do things. I've added my reasoning to the end of the question, that should give a better explanation of why I want to do this – ElRudi Nov 02 '21 at 10:46
  • Having the `__add__` method return an instance of the same class as one of its arguments is likely to be a lot more extensible than hard-coding the name `A` as the type that gets returned. If you don't like `self.__class__`, you can also write it `type(self)`. – Blckknght Nov 02 '21 at 20:17