5

I have python class trees, each made up of an abstract base class and many deriving concrete classes. I want all concrete classes to be accessible through a base-class method, and I do not want to specify anything during child-class creation.

This is what my imagined solution looks like:

class BaseClassA(object):
    # <some magic code around here>
    @classmethod
    def getConcreteClasses(cls):
        # <some magic related code here>

class ConcreteClassA1(BaseClassA):
    # no magic-related code here

class ConcreteClassA2(BaseClassA):
    # no magic-related code here

As much as possible, I'd prefer to write the "magic" once as a sort of design pattern. I want to be able to apply it to different class trees in different scenarios (i.e. add a similar tree with "BaseClassB" and its concrete classes).

Thanks Internet!

Yonatan
  • 1,187
  • 15
  • 33
  • 3
    That's... not really a solution... – Ignacio Vazquez-Abrams Jul 18 '11 at 08:07
  • 1
    Will all of the concrete classes be in the same source file? – Kenan Banks Jul 18 '11 at 08:08
  • @Ignacio: indeed that's not a solution, it's how I would like to use a solution in my scenario. @Triptych: No, not necessarily. – Yonatan Jul 18 '11 at 08:38
  • Does this answer your question? [How to auto register a class when it's defined](https://stackoverflow.com/questions/5189232/how-to-auto-register-a-class-when-its-defined) – Aziz Alto May 01 '20 at 01:38
  • While on a similar realm, the other question's core issue is with instantiating directly-decorated classes, not with indirectly triggering registration of the classes themselves. Thanks for the reference though! – Yonatan May 02 '20 at 09:27

4 Answers4

8

Here is a simple solution using modern python's (3.6+) __init__subclass__ defined in PEP 487. It allows you to avoid using a meta-class.

class BaseClassA(object):
    _subclasses = [] 

    @classmethod
    def get_concrete_classes(cls):
        return list(cls._subclasses)

    def __init_subclass__(cls):
        BaseClassA._subclasses.append(cls)


class ConcreteClassA1(BaseClassA):
    pass  # no magic-related code here


class ConcreteClassA2(BaseClassA):
    pass  # no magic-related code here


print(BaseClassA.get_concrete_classes())
derchambers
  • 904
  • 13
  • 19
7

you can use meta classes for that:

class AutoRegister(type):
    def __new__(mcs, name, bases, classdict):
        new_cls = type.__new__(mcs, name, bases, classdict)
        #print mcs, name, bases, classdict
        for b in bases:
            if hasattr(b, 'register_subclass'):
                b.register_subclass(new_cls)
        return new_cls


class AbstractClassA(object):
    __metaclass__ = AutoRegister
    _subclasses = []

    @classmethod
    def register_subclass(klass, cls):
        klass._subclasses.append(cls)

    @classmethod
    def get_concrete_classes(klass):
        return klass._subclasses


class ConcreteClassA1(AbstractClassA):
    pass

class ConcreteClassA2(AbstractClassA):
    pass

class ConcreteClassA3(ConcreteClassA2):
    pass


print AbstractClassA.get_concrete_classes()

I'm personnaly very wary of this kind of magic. Don't put too much of this in your code.

gurney alex
  • 13,247
  • 4
  • 43
  • 57
  • 1
    Very elegant! I've seen a similar solution with meta-classes, but less flexible. Thanks Alex! – Yonatan Jul 18 '11 at 08:45
4

You should know that part of the answer you're looking for is built-in. New-style classes automatically keep a weak reference to all of their child classes which can be accessed with the __subclasses__ method:

@classmethod
def getConcreteClasses(cls):
    return cls.__subclasses__()

This won't return sub-sub-classes. If you need those, you can create a recursive generator to get them all:

@classmethod
def getConcreteClasses(cls):
    for c in cls.__subclasses__():
        yield c
        for c2 in c.getConcreteClasses():
            yield c2
Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
  • Brilliant, I had no idea this mechanism exists. This is great when no "callback" behavior of registration is required, but instead on-demand iteration of subclass tree. It's also nice to generate the subclass tree starting from python's built-in "object" class. – Yonatan Jul 18 '11 at 14:28
1

Another way to do this, with a decorator, if your subclasses are either not defining __init__ or are calling their parent's __init__:

def lister(cls):
    cls.classes = list()
    cls._init = cls.__init__
    def init(self, *args, **kwargs):
        cls = self.__class__
        if cls not in cls.classes:
            cls.classes.append(cls)
        cls._init(self, *args, **kwargs)
    cls.__init__ = init
    @classmethod
    def getclasses(cls):
        return cls.classes
    cls.getclasses = getclasses
    return cls

@lister
class A(object): pass

class B(A): pass

class C(A):
    def __init__(self):
        super(C, self).__init__()

b = B()
c = C()
c2 = C()
print 'Classes:', c.getclasses()

It will work whether or not the base class defines __init__.

agf
  • 171,228
  • 44
  • 289
  • 238
  • The '__init__' limitation is totally acceptable; however, note that concrete classes will not be 'registered' until the first instance has been created. This does not fit my scenario where I want to choose which class to instantiate based on which concrete classes are available. However, it's very good for when you want to register only classes which have *actually* been instantiated. – Yonatan Jul 18 '11 at 08:56
  • Didn't think about what your use case was. Also, came back to add this is probably better done with `__new__` rather than `__init__` as it's perhaps less likely to be overridden without calling the parent class' `__new__`. – agf Jul 18 '11 at 09:25