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
Here's essentially a hybrid version of @Ignacio Vazquez-Abrams' and @aaronasterling's answers which preserves the order of the subclasses in the list. Initially the desired subclass names (i.e. strings) 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 subclass:
class Base(object): # New-style class (i.e. explicitly derived from object).
@classmethod
def register_subclass(cls, subclass):
""" Class decorator for registering subclasses. """
# Replace any occurrences of the class name in the class' subs list.
# with the class itself.
# Assumes the classes in the list are all subclasses of this one.
# Works because class decorators are called *after* the decorated class'
# initial creation.
while subclass.__name__ in cls.subs:
cls.subs[cls.subs.index(subclass.__name__)] = subclass
return cls # Return modified class.
subs = ['Sub3', 'Sub1'] # Ordered list of subclass names.
@Base.register_subclass
class Sub1(Base): pass
@Base.register_subclass
class Sub2(Base): pass
@Base.register_subclass
class Sub3(Base): pass
print('Base.subs: {}'.format(Base.subs))
# Base.subs: [<class '__main__.Sub3'>, <class '__main__.Sub1'>]
Update: Metaclasses
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 my original answer above (which you accepted), however it makes it all happen automagically.
Note that even though the metaclass' __init__()
is called for the creation of every subclass, it only updates the subs
list if the subclass' name appears in it—so the initial Base class' definition of the contents of the subs
list still controls what gets replaced in it (maintaining its order).
class BaseMeta(type):
def __init__(cls, name, bases, classdict):
if classdict.get('__metaclass__') is not BaseMeta: # Metaclass instance?
# Replace any occurrences of a subclass' name in the class being
# created the class' sub list with the subclass itself.
# Names of classes which aren't direct subclasses will be ignored.
while name in cls.subs:
cls.subs[cls.subs.index(name)] = cls
# Chain to __init__() of the class instance being created after changes.
# Note class instance being defined must be new-style class.
super(BaseMeta, cls).__init__(name, bases, classdict)
# Python 2 metaclass syntax.
class Base(object): # New-style class (derived from built-in object class).
__metaclass__ = BaseMeta
subs = ['Sub3', 'Sub1'] # Ordered list of subclass names.
# Python 3 metaclass syntax.
#class Base(metaclass=BaseMeta):
# subs = ['Sub3', 'Sub1'] # Ordered list of subclass names.
# Note: No need to manually register the (direct) subclasses.
class Sub1(Base): pass
class Sub2(Base): pass
class Sub3(Base): pass
print('Base.subs: {}'.format(Base.subs))
Output:
Base.subs: [<class '__main__.Sub3'>, <class '__main__.Sub1'>]
It important to note that there's at least one subtle difference between these two answers—namely that the first will work with any class name that is registered via @Base.register_subclass()
, whether or not its actually a subclass of Base
(although that might be possible to change/fix.)
I'm pointing this out for a couple of 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's not the case with the code in my update, which only works for Base
subclasses since they effectively get "registered" automatically via the metaclass—but will leave anything else in the list alone. This could be considered a bug or a feature. ;¬)
Update: Python 3.6+
In Python 3.6 a new object
method was added named __init_subclass__()
which provides an even simpler way implement things that also doesn't require decorating all the subclasses or defining a metaclass:
#!/usr/bin/env python3.6
class Base:
subs = ['Sub3', 'Sub1'] # Ordered list of subclass names.
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
while cls.__name__ in cls.subs:
cls.subs[cls.subs.index(cls.__name__)] = cls
# Note: No need to manually register the subclasses.
class Sub1(Base): pass
class Sub2(Base): pass
class Sub3(Base): pass
print('Base.subs: {}'.format(Base.subs))
Write a decorator that adds it to the registry in Base
.
class Base(object):
subs = []
@classmethod
def addsub(cls, scls):
cls.subs.append(scls)
...
@Base.addsub
class Sub1(Base):
pass
class Sub2(Base):
pass
@Base.addsub
class Sub3(Base):
pass
Edit: Because of the added requirement of order I completely reworked my answer. I also make use of a class decorator, which was used here first by @Ignacio Vazquez-Abrams.
Edit2: code now tested and some stupidity slightly corrected
class Base(object):
subs = []
@classmethod
def addsub(cls, before=None):
def inner(subclass):
if before and before in cls.subs:
cls.subs.insert(cls.subs.index(before), subclass)
else:
cls.subs.append(subclass)
return subclass
return inner
@Base.addsub()
class Sub1(Base):
pass
class Sub2(Base):
pass
@Base.addsub(before=Sub1)
class Sub3(Base):
pass