pythoninheritancesubclass

How to collect all leaf subclasses of a parent class in Python?


Given a base class named ParentClass, how can I find all leaf subclasses of it? For example, if I have:

class ParentClass:
    pass

class SubClass1(ParentClass):
    pass

class SubClass2(ParentClass):
    pass

class SubClass3(ParentClass):
    pass

class SubClass4(SubClass2):
    pass

class SubClass5(SubClass2):
    pass

class SubClass6(SubClass2, SubClass3):
    pass

I want a list containing, [SubClass1, SubClass4, SubClass5, SubClass6], and not SubClass2 or SubClass3 as they are not leaves in the inheritance hierarchy.


Solution

  • Since the problem is that you have an arbitrary hierarchy, and just want the subclassmost to be "concrete" classes, and having a registry for them, maybe requiring, in one of the base classes, that these concrete classes set a class attribute that all concrete implementations should have, but that would not make sense for intermediate classes (I can imagine a card .name attribute, for example).

    The usual mechanism of Abstract Base Classes for Python can work for this, with the required attribute overriding a method in the baseclass marked as @abstractmethod and an __init_subclass__ method in the same base can then add that class to a registry with your desired leave classes (And even check if a class marked as "leave" by setting name had been further subclassed, raising an error)

    class Base(ABC):
        # All concrete classes must set a "name" attribute
        name = abstractmethod(lambda:None)
        _card_registry = set()
        def __init_subclass__(cls, *args, **kw):
            if cls.name is not __class__.name:  # subclass overwrote ".name"
                # optional - check if no other superclass of cls, other than this one,  set name:
                for supercls in cls.__mro__[1:]:
                    if supercls is __class__:
                        break
                    if getattr(supercls, "name") is not __class__.name:
                        raise TypeError(f"{supercls.__name__} class setting ´.name´ was further subclassed")
                __class__._card_registry.add(cls)
            super().__init_subclass__(*args, **kw)
    

    And after importing all your modules, Base._card_registry would contain all classes that interest you.


    Now, the literal answer for getting all the leafmost subclasses, although not the best pattern in this specific case: Python will allow one to retrieve class' direct subclasses with the .__subclasses__ method. A simple code to retrieve all current leave classes could be:

    def getleaves(cls, leaves=None):
        if leaves is None: leaves=set()
        if subclasses:=cls.__subclasses__():
            for subclass in subclasses:
                getleaves(subclass, leaves)
        else:
            leaves.add(cls)
        return leaves