pythonpython-3.xinstantiationabc

Python abstract methods for Children, but such that don't prohibit instances of the Base?


I'm currently using ABC's to ensure that my child classes implement a specific method (or property, in this particular case). I want to make it impossible to create children of Entity without implementing similar_entities:

class Entity(ABC):
    @property
    @abstractmethod
    def similar_entities(self) -> list[str]:
        return []

class SubEntity1(Entity):
    @property
    def similar_entities(self):
        return ["a", "b", "c"]

SubEntity1() # Can be instantiated

class SubEntity2(Entity):
    pass

SubEntity2() # TypeError

However, I don't actually want to prohibit users from creating instances of the base class Entity itself; creating a "generic" entity is meaningful in this case, and I would like to use the implementation of similar_entities defined on the Entity class itself. I'm only using @abstractmethod to "idiot-check" myself so that I don't accidentally forget to implement this property on my subclasses.

I know full well that the "correct" way to do this would be to create a stub implementation of Entity and use that for generic instances instead:

class GenericEntity(Entity):
    @property
    def similar_entities(self) -> list[str]:
        return super().similar_entities

GenericEntity() # ...

But to me this essentially violates DRY, and since I'm working with such a simple and limited context I'm wondering if there's a way to have my cake and eat it too. Is there any way to massage @abstractmethod to only complain on child-instances? If not, is there some better, more flexible way to establish this contract between my classes? I'm also not opposed to "rolling-my-own" decorator if this is more appropriate in this case.


Solution

  • Here is my code that fits:

    1. Implementing similar_entities on subclasses is allowed.
    2. Entity can be instantiated.
    3. Subclasses defined without similar_entities property raise TypeError.
    class MyMeta(type):
        def __init__(self, classname, superclasses, attributedict):
            super().__init__(classname, superclasses, attributedict)
            if 'similar_entities' not in self.__dict__:
                raise TypeError
    
    
    class Entity(metaclass=MyMeta):
        @property
        def similar_entities(self):
            return []
    

    Example 1:

    class SubEntity1(Entity):
        @property
        def similar_entities(self):
            return ["a", "b", "c"]
    
    print(SubEntity1().similar_entities)  # ['a', 'b', 'c']
    

    Example 2:

    class SubEntity2(Entity):
        pass
    
    print(SubEntity2().similar_entities)  # TypeError
    

    Example 3:

    print(Entity().similar_entities)  # []