pafcu pafcu - 7 days ago 6
Python Question

Forward declaration of classes?

I have some classes looking like this:

class Base:
subs = [Sub3,Sub1]
# Note that this is NOT a list of all subclasses!
# Order is also important

class Sub1(Base): pass
class Sub2(Base): pass
class Sub3(Base): pass
...


Now, this fails because Sub1 and Sub3 are not defined when Base.subs is. But obviously I can't put the subclasses before Base either. Is there a way to forward-declare classes in Python? I want to work with
isinstance
so the types in subs actually have to be the same as the later declared subclasses, it's not enough that they have the same name and other properties.

One workaround is to do:
Base.subs = [Sub3,Sub1]
after the subclasses have been defined, but I don't like having to split my class in that way.

Edit: Added information about order

Answer

This is a hybrid version of @Ignacio Vazquez-Abrams and @aaronasterling's answers which preserves the order of the subclasses in the list. Initially the subclass names are manually placed in the subs list in the desired order. Then as each subclass is defined, a class decorator causes the corresponding string to be replaced with the actual subtype.

class Base:
  @classmethod
  def registersub(cls, subcls):
    """ Decorator to register subclass types. """
    # replace occurrence(s) of subclass name in 'subs' with the subclass itself
    while subcls.__name__ in cls.subs:
        cls.subs[cls.subs.index(subcls.__name__)] = subcls
    return cls

  subs = ['Sub3','Sub1']  # some subclass names in some special order

@Base.registersub
class Sub1(Base): pass
@Base.registersub
class Sub2(Base): pass
@Base.registersub
class Sub3(Base): pass

print Base.subs
# [<class __main__.Sub3>, <class __main__.Sub1>]

It important to note that there's a perhaps subtle difference between my original answer above and the update which follows below -- namely which is that the while the former will work with any class name that is registered with @Base.registersub, whether or not its a subclass of Base.

I'm pointing this out for two reasons. First because in your comments you said that subs was a "bunch of classes in a list, some of which might be its subclasses", and more importantly because that is not the case with the code in my update, which only works for Base subclasses since they are effectively "registered" automatically via the metaclass -- but leave anything other in the list alone.

Update
Exactly the same thing can also be done using a metaclass -- which has the advantage that it eliminates the need to explicitly decorate each subclass as shown in the accepted answer above and instead makes it happen automagically. Note that even though the metaclass's __init__() is called for the creation of every subclass, it only updates the subs list if that subclass's name appears in it -- so the initial Base class definition of the contents of subs still controls what gets put in it and in what order.

class BaseMeta(type):
    def __init__(cls, name, bases, classdict):
        if classdict.get('__metaclass__') is not BaseMeta:  # subclass?
            # replace any occurrences of this subclass name
            # in Base class 'subs' list with the subclass itself
            while name in cls.subs:
                cls.subs[cls.subs.index(name)] = cls
        type.__init__(cls, name, bases, classdict)

class Base:
    __metaclass__ = BaseMeta
    subs = ['Sub3','Sub1']  # some subclass names in some special order

class Sub1(Base): pass
class Sub2(Base): pass
class Sub3(Base): pass

print Base.subs
# [<class '__main__.Sub3'>, <class '__main__.Sub1'>]