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