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