Home > Back-end >  __str__ wrapper in custom Exception
__str__ wrapper in custom Exception

Time:11-26

Why does the below code print error msg instead of ABC\nerror msg?

class CustomException(Exception):
    """ABC"""
    def __init__(self, *args):
        super().__init__(*args)
        self.__str__ = self._wrapper(self.__str__)
    def _wrapper(self, f):
        def _inner(*args, **kwargs):
            return self.__doc__   '\n'   f(*args, **kwargs)
        return _inner

print(CustomException('error msg'))

CodePudding user response:

Forget the Exception class for now. Consider this:

class A:
    def __str__(self):
        return 'A'

obj = A()
print(obj)
obj.__str__ = lambda x: 'B'
print(obj)
A.__str__ = lambda x: 'B'
print(obj)

output:

A
A   # Not B !
B

Dunder methods' lookup is different in Python. From docs:

For custom classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not in the object’s instance dictionary.

What you want to achieve is:

class CustomException(Exception):
    """ABC"""

    def __init__(self, *args):
        super().__init__(*args)

    def __str__(self):
        return self.__doc__   '\n'   super().__str__()


print(CustomException('error msg'))

output:

ABC
error msg

CodePudding user response:

Operations backed by special methods usually explicitly look up the special method as a proper method not just as a callable attribute. Concretely, instead of self.__str__ the interpreter roughly looks at type(self).__str__.__get__(self, type(self)) – i.e. a descriptor __str__ on the class to be bound with the instance. To override a special method, it is thus necessary to override the class' descriptor instead of the instance' attribute.

This can be done by a) declaring the special method as a slot, which handles the type(self).__str__ part, and b) assigning a function, which handles the __get__(self, type(self)) part.

class CustomException(Exception):
    """ABC"""
    __slots__ = ("__str__",)  # <<< magic

    def __init__(self, *args):
        super().__init__(*args)
        # vvv self.__str__ is the class' slot
        self.__str__ = self._wrapper(super().__str__)
        #                            AAA real __str__ lives on the super class
    def _wrapper(self, f):
        def _inner(*args, **kwargs):
            return self.__doc__   '\n'   f(*args, **kwargs)
        return _inner

print(CustomException('error msg'))

Note that since every instance behaves the same in this case, it is advisable to just define a new __str__ method in practice.

CodePudding user response:

As per @khelwood's reply in the comments this code works as intended:

class CustomException(Exception):
    """ABC"""
    def __init__(self, *args):
        super().__init__(*args)
        self.__class__.__str__ = self._wrapper(self.__str__)
    def _wrapper(self, f):
        def _inner(*args, **kwargs):
            return self.__doc__   '\n'   f()
        return _inner

but I ended up with this equivalent (@MisterMiyagi):

class CustomException(Exception):
    """ABC"""
    def __str__(self):
        return self.__doc__   '\n'   super().__str__()
  • Related