1

I have a Python library which will be used by other people:

class BaseClassA:
   

class BaseClassB:
    def func0(self):
        this.class_a_obj = BaseClassA()

BaseClassB creates a BaseClassA object and stores a pointer. This is an issue because I want to allow the user to extend my library classes:

class ExtendClassA(BaseClassA):

And my library should choose the extended class (ExtendClassA) instead of the base class (BaseClassA) in the func0 method.

Above is a very simple example my problem statement. In reality I have 10ish classes where extending/creation happens. I want to avoid the user having to rewrite func0 in an extended BaseClassB to support the new ExtendClassA class they created.

I'm reaching out to the stack overflow community to see what solutions other people have implemented for issues like this. My initial thought is to have a global dict which 'registers' class types/constructors and classes would get the class constructors from the global dict. When a user wants to extend a class they would replace the class in the dict with the new class.

Library code:

global lib_class_dict
lib_class_dict['ClassA'] = BaseClassA()
lib_class_dict['ClassB'] = BaseClassB()

class BaseClassA:
   

class BaseClassB:
    def func0(self):
        this.class_a_obj = lib_class_dict['ClassB']

User code:

lib_class_dict['ClassA'] = ExtendClassA():


class ExtendClassA:

EDIT: Adding more details regarding the complexities I'm dealing with.

I have scenarios where method calls are buried deep within the library, which makes it hard to pass a class from the user entry point -> function:

(user would call BaseClassB.func0() in below example)

class BaseClassA:
   

class BaseClassB:
    def func0(self):
        this.class_c_obj = BaseClassC()

class BaseClassC:
    def __init__(self):
        this.class_d_obj = BaseClassD()

class BaseClassD:
    def __init__(self):
        this.class_a_obj = BaseClassA()

Multiple classes can create one type of object:

class BaseClassA:
   

class BaseClassB:
    def func0(self):
        this.class_a_obj = BaseClassA()

class BaseClassC:
    def __init__(self):
        this.class_a_obj = BaseClassA()

class BaseClassD:
    def __init__(self):
        this.class_a_obj = BaseClassA()

For these reasons I'm hoping to have a global or central location all classes can grab the correct class.

user1334858
  • 1,885
  • 5
  • 30
  • 39
  • consider including a stripped down version of the actual class. Some architecture decisions can be informed by how you expect your classes to interact (which is in turn informed by their function). That's missing with names e.g. `ClassA`, `ClassB` – anon01 Oct 22 '21 at 23:43
  • A very dumb idea from not a professional: would that be possible to use *__init_subclass__* method, and redefine a function there somehow? A long shot, but maybe will be playable... https://stackoverflow.com/questions/45400284/understanding-init-subclass – Alex Oct 22 '21 at 23:47
  • In your first example, each `BaseClassB` has its own `BaseClassA` object while in the library code, all share the same instance referenced in `lib_class_dict`. What is the correct expected behaviour? – qouify Oct 23 '21 at 04:35

2 Answers2

2

Allow them to specify the class to use as an optional parameter to func0

def BaseClassB:
    def func0(self, objclass=BaseClassA):
        self.class_a_obj = objclass()

obj1 = BlassClassB()
obj1.func0()
obj2 = BassClassB()
obj2.func0(objclass = ExtendClassA)
Barmar
  • 741,623
  • 53
  • 500
  • 612
  • Although this could work I don't think this is reasonable for my particular library. Some of the methods are "private," so there is no easy way for the user to pass a class to certain functions. Let me update the description showing these complexities. – user1334858 Oct 19 '21 at 19:41
1

So, I've tried a PoC that, if I understand correctly, might do the trick. Give it a look.

By the way, whether it does work or not, I have a strong feeling this is actually a bad practice in almost all scenarios, as it changes class behavior in a obscure, unexpected way that would be very difficult to debug.

For example, in the below PoC if you inherit the same BaseClassA multiple times - only the latter inheritance shall be written in the class library, which would be a huge pain for the programmer trying to understand what on earth is happening with his code and why. But of course, there are some use cases when shooting ourselves in a leg is less painful than designing & using a proper architecture :)

So, the first example where we have inheritance (I specified multiple inherited classes, just to show that only the last inherited one would be saved in a library):

#################################
# 1. We define all base classes

class BaseClassA:
    def whoami(self):
        print(type(self))
    def __init_subclass__(cls):
       omfg_that_feels_like_a_reeeeally_bad_practise['ClassA'] = cls
       print('Class Dict Updated:')
       print('New Class A: ' +  str(cls))

#################################
# 2. We define a class library

global omfg_that_feels_like_a_reeeeally_bad_practise
omfg_that_feels_like_a_reeeeally_bad_practise = {}
omfg_that_feels_like_a_reeeeally_bad_practise['ClassA'] = BaseClassA

#################################
# 3. We define a first class that refer our base class (before inheriting from base class)

class UserClassA:
    def __init__(self):
        self.class_a_obj = omfg_that_feels_like_a_reeeeally_bad_practise['ClassA']()

#################################
# 4. We inherit from the base class several times

class FirstExtendedClassA(BaseClassA):
    pass


class SecondExtendedClassA(BaseClassA):
    pass


class SuperExtendedClassA(FirstExtendedClassA):
    pass

#################################
# 5. We define a second class that refer our base class (after inheriting from base class)

class UserClassB:
    def __init__(self):
        self.class_a_obj = omfg_that_feels_like_a_reeeeally_bad_practise['ClassA']()

#################################
## 6. Now we try to refer both user classes

insane_class_test = UserClassA()
print(str(insane_class_test.class_a_obj))
### LOOK - A LAST INHERITED CHILD CLASS OBJECT IS USED!
# <__main__.SuperExtendedClassA object at 0x00000DEADBEEF>

insane_class_test = UserClassB()
print(str(insane_class_test.class_a_obj))
### LOOK - A LAST INHERITED CHILD CLASS OBJECT IS USED!
# <__main__.SuperExtendedClassA object at 0x00000DEADBEEF>

And if we remove inheritance, the base class will be used:

#################################
# 1. We define all base classes

class BaseClassA:
    def whoami(self):
        print(type(self))
    def __init_subclass__(cls):
       omfg_that_feels_like_a_reeeeally_bad_practise['ClassA'] = cls
       print('Class Dict Updated:')
       print('New Class A: ' +  str(cls))

#################################
# 2. We define a class library

global omfg_that_feels_like_a_reeeeally_bad_practise
omfg_that_feels_like_a_reeeeally_bad_practise = {}
omfg_that_feels_like_a_reeeeally_bad_practise['ClassA'] = BaseClassA

#################################
# 3. We define a first class that refer our base class

class UserClassA:
    def __init__(self):
        self.class_a_obj = omfg_that_feels_like_a_reeeeally_bad_practise['ClassA']()

#################################
# 5. We define a second class that refer our base class

class UserClassB:
    def __init__(self):
        self.class_a_obj = omfg_that_feels_like_a_reeeeally_bad_practise['ClassA']()

#################################
## 6. Now we try to refer both user classes

insane_class_test = UserClassA()
print(str(insane_class_test.class_a_obj))
### LOOK - A DEFAULT CLASS OBJECT IS USED!
# <__main__.BaseClassA object at 0x00000DEADBEEF>

insane_class_test = UserClassB()
print(str(insane_class_test.class_a_obj))
### LOOK - A DEFAULT CLASS OBJECT IS USED!
# <__main__.BaseClassA object at 0x00000DEADBEEF>
Alex
  • 815
  • 9
  • 19
  • 1
    Yeah this might be bad practice, but one of the only solutions I could come up with.. This is in a very controlled environment, so even though it isn't ideal I think this is the best solution for me. – user1334858 Oct 25 '21 at 23:44
  • Happy to hear that. Good luck! – Alex Oct 26 '21 at 05:44