0

Consider the following sample code:

from abc import ABC, abstractmethod, abstractproperty

class Base(ABC):

    @abstractmethod
    def foo(self) -> str:
        print("abstract")

    @property
    @abstractmethod
    def __name__(self) -> str:
        return "abstract"

    @abstractmethod
    def __str__(self) -> str:
        return "abstract"

    @property
    @abstractmethod
    def __add__(self, other) -> str:
        return "abstract"


class Sub(Base):

    def foo(self):
        print("concrete")

    def __str__(self):
        return "concrete"

    def __add__(self, other) -> str:
        return "concrete"


sub = Sub()
sub.foo()
sub.__name__
print(str(sub))

Note that the subclass does not implement the abstract property __name__, and indeed when __name__ is referenced, it prints as "abstract" from its parent:

>>> sub.foo()
concrete
>>> sub.__name__
'abstract'
>>> print(str(sub))
concrete

However, it is not because __name__ is a dunder method, nor because of some issue with @property and @abstractmethod decorators not working well together, because if I remove the implementation of __add__ from Sub, it does not let me instantiate it. (I know __add__ is not normally a property, but I wanted to use a 'real' dunder method) The same expected behavior occurs if I remove the implementation of __str__ and foo. Only __name__ behaves this way.

What is it about __name__ that causes this behavior? Is there some way around this, or do I need to have the parent (abstract) implementation manually raise the TypeError for me?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Keozon
  • 998
  • 10
  • 25

1 Answers1

1

Classes have a __name__ attribute by way of a data descriptor on type:

>>> Sub.__name__
'Sub'
>>> '__name__' in Sub.__dict__
False

It's a data descriptor because it also intercepts assignments to ensure that the value is a string. The actual values are stored in a slot on the C structure, the descriptor is a proxy for that value (so setting a new value on the class doesn't add a new entry to the __dict__ either):

>>> Sub.__name__ = None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only assign string to NewName.__name__, not 'NoneType'
>>> Sub.__name__ = 'NewName'
>>> Sub.__name__
'NewName'
>>> '__name__' in Sub.__dict__
False

(actually accessing that descriptor without triggering it's __get__ is not really possible as type itself has no __dict__ and has itself a __name__).

This causes the test for the attribute when creating instances of Sub to succeed, the class has that attribute after all:

>>> hasattr(Sub, '__name__')
True

On instances of Sub, the Base.__name__ implementation is then found because instance descriptor rules only consider the class and base classes, not the metatype.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343