Home > Enterprise >  Setup automatic inheritance of parent class' subclass in Python
Setup automatic inheritance of parent class' subclass in Python

Time:01-23

When running this code, the print displays the message "no number found in class A", although in fact it was not found in an object of class B. The aim is to change only the Base class in such a way that, when inheriting from it, descendants create their own NotFoundError exception inherited from Base.

class Base:
    class NotFoundError(Exception):
        pass

    def __new__(cls, *args, **kwargs):
        instance = super().__new__(cls)
        return instance

    def __init__(self, numbers: list[int]):
        self.numbers = numbers

    def pop_by_val(self, number: int):
        try:
            self.numbers.remove(number)
        except ValueError:
            raise self.NotFoundError()


class A(Base):
    pass


class B(Base):
    pass


a = A([1, 2])
b = B([1, 2])

try:
    a.pop_by_val(1)
    b.pop_by_val(3)
except A.NotFoundError:
    print("no number found in class A")
except B.NotFoundError:
    print("no number found in class B")

I guess it can be fixed by some kind of init/new dunders' customization, but I haven't succeeded in my tries

CodePudding user response:

A.NotFoundError and B.NotFoundError point to the same class object, as can be verified by running:

print(id(A.NotFoundError) == id(B.NotFoundError))

When you run

try:
    a.pop_by_val(1)
    b.pop_by_val(3)
except A.NotFoundError:
    print("no number found in class A")
except B.NotFoundError:
    print("no number found in class B")

The first pop_by_val(1) succeeds, so the program goes on. On the second b.pop_by_val(3), B.NotFoundError is raised, but since B.NotFoundError is identical to A.NotFoundError, the exception is caught by the first except clause and therefore prints no number found in class A.

CodePudding user response:

Note that in your snippet that A.NotFoundError and B.NotFoundError are the same exception class, so it will always pick the first except clause.

What you can do is to dynamically create a new exception class for each class which derives from Base.

The code below uses a meta class to create a new exception type for each class:

class Base_Meta(type):
    def __new__(cls, classname, supers, cls_dict):
        t = type.__new__(cls, classname, supers, cls_dict)
        cls.make_exception(t)
        return t
    
    @staticmethod
    def make_exception(t):
        class NotFoundError(Exception):
            pass
        t.NotFoundError = NotFoundError

class Base(metaclass=Base_Meta):

    def __init__(self, numbers: list[int]):
        self.numbers = numbers
       
    def pop_by_val(self, number: int):
        try:
            self.numbers.remove(number)
        except ValueError:
            raise self.NotFoundError()

The above change will cause the output: no number found in class B in your code.

CodePudding user response:

One solution to this is to write a metaclass that creates a new exception type for each subclass.

class NotFoundError(Exception):
     pass

class NotFoundErrorMeta(type):

    def __init__(cls, name, bases, attrs):
        super().__init__(name, bases, attrs)
        cls.NotFoundError = type("NotFoundError", (NotFoundError,), {
            "__qualname__": cls.__qualname__   ".NotFoundError" })

class Base(metaclass=NotFoundErrorMeta):

    def __init__(self, numbers: list[int]):
        self.numbers = numbers

    def pop_by_val(self, number: int):
        try:
            self.numbers.remove(number)
        except ValueError:
            raise self.NotFoundError()

Now each class raises its own NotFoundError type. These are subclasses of the NotFoundError class, so you can catch any NotFoundError via the base class, or catch a specific class's exception.

CodePudding user response:

There's no automatic definition of new, inherited class attributes, but you can define __init_subclass__ to define them for you.

class Base:
    class NotFoundError(Exception):
        pass

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        class NotFoundError(Base.NotFoundError):
            pass
        cls.NotFoundError = NotFoundError

    def __init__(self, numbers: list[int]):
        self.numbers = numbers

    def pop_by_val(self, number: int):
        try:
            self.numbers.remove(number)
        except ValueError:
            raise self.NotFoundError()

(This is basically doing the same thing as @kindall's answer, but avoids using a metaclass. Prefer __init_subclass__ where possible to make it easier for your class to interact with more classes, as metaclasses don't compose well.)

  • Related