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
Forward Declaration refers to the beforehand declaration of the syntax or signature of an identifier, variable, function, class, etc. prior to its usage (done later in the program). Example: // Forward Declaration of the sum() void sum(int, int); // Usage of the sum void sum(int a, int b) { // Body }
The main rule is that you can only forward-declare classes whose memory layout (and thus member functions and data members) do not need to be known in the file you forward-declare it. This would rule out base classes and anything but classes used via references and pointers. Almost.
In computer programming, a forward declaration is a declaration of an identifier (denoting an entity such as a type, a variable, a constant, or a function) for which the programmer has not yet given a complete definition.
Generally you would include forward declarations in a header file and then include that header file in the same way that iostream is included.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With