11

I'm trying to write a generic metaclass to track subclasses

Since I want this to be generic, I didn't want to hardcode any class name within this metaclass, therefore I came up with a function that generates the proper metaclass, something like:

def make_subtracker(root):
    class SubclassTracker(type):
        def __init__(cls, name, bases, dct):
            print('registering %s' % (name,))
            root._registry.append(cls)
            super(SubclassTracker, cls).__init__(name, bases, dct)
    return SubclassTracker

This way I could invoke it to generate a metaclass for a specific root class with:

__metaclass__ = make_subtracker(Root)

Here is where I bump into a problem. I cannot do this:

class Root(object):
   _registry = []
   __metaclass__ = make_subtracker(Root)

...because Root is not defined yet when I use make_subtracker(Root). I tried adding the __metaclass__ attribute later, so that at least it can be applied in subclasses:

class Root(object):
   _registry = []

Root.__metaclass__ = make_subtracker(Root)

...but this doesn't work. __metaclass__ has a special processing when the class definition is read, as defined in Customizing class creation.

I'm looking for suggestions in order to do this (either change a class' metaclass at runtime in a way that it is applied to its subclasses, or any other alternative).

Carles Barrobés
  • 11,608
  • 5
  • 46
  • 60
  • 1
    Please don't do this. Folks who come after you will rip it out because it's too complex. Please use a factory function that creates objects of the appropriate subclass. – S.Lott Oct 12 '10 at 14:54

3 Answers3

11

I think you want something like this (untested):

class SubclassTracker(type):
    def __init__(cls, name, bases, dct):
        if not hasattr(cls, '_registry'):
            cls._registry = []
        print('registering %s' % (name,))
        cls._registry.append(cls)
        super(SubclassTracker, cls).__init__(name, bases, dct)

Then, for Python 2, you can invoke it like:

class Root(object):
    __metaclass__ = SubclassTracker

for Python 3

class Root(object, metaclass=SubclassTracker):

Note that you don't need to stick the _registry attribute on there because stuff like that is what metaclasses are for. Since you already happen to have one laying around... ;)

Note also that you might want to move the registration code into an else clause so that the class doesn't register itself as a subclass.

Frozen Flame
  • 3,135
  • 2
  • 23
  • 35
aaronasterling
  • 68,820
  • 20
  • 127
  • 125
  • In fact, since you SubclassTracker does not include any hardcoded reference to the *root* type, there is no need for a function to create it, therefore I directly defined a SubclassTracker class and forgot about the make_subtracker altogether. Works fine, thanks! – Carles Barrobés Oct 12 '10 at 14:26
  • @Carles, the version you had when you made your last comment will break with multiple classes if they don't define `_registry`. I fixed it up though so have a look at the new one. It's cleaner. It actually hadn't even occurred to me to do away with the factory function. – aaronasterling Oct 12 '10 at 14:28
  • FWIW, as currently written, `Root` will add itself to its `_registry` list even though technically it's not a subclass of itself. This may or may not be desirable. – martineau Feb 05 '13 at 23:01
  • Furthermore, if you derive a class from `Root` it will have not only have itself in its `_registry` list, but will also `Root` in it, even though neither are subclasses -- which seems very likely to be unwanted. – martineau Feb 05 '13 at 23:26
11

Python does this automatically for new-style classes, as mentioned in this answer to the similar queston How to find all the subclasses of a class given its name? here.

martineau
  • 119,623
  • 25
  • 170
  • 301
  • 1
    Although this does technically keep track, depending on your usage, it might not be very efficient. It returns a list, so if you're looking for a class with specific attributes, you'll have to iterate over the entire list. Using a meta class allows you to efficiently index sublcasses however you want. – Cerin Feb 05 '13 at 20:03
  • @Cerin: Perhaps...if high efficiency is a concern or issue -- which cannot be determined from the question which doesn't specify how the information will to be used. Regardless, I'd be interested in seeing a concrete implementation of your idea(s) since all the other answers are currently also list-based -- so feel free to add one of your own. – martineau Feb 05 '13 at 20:30
  • @martinaeu, My implementation is the same as aaronasterling's, except the registry is a dictionary instead of a list, and the key is the whatever hashed representation you'd want to use for each class. I'm currently using this as an alternative to the register() pattern in Django, to associate a ModelForm class with a unique slug, for quick URL dispatching. – Cerin Feb 05 '13 at 22:11
  • @Cerin: aaronasterling's answer has some issues and references to Django mean little to many of us, so again, post some code or edit an existing answer. – martineau Feb 05 '13 at 23:31
  • You're being overly defensive. I meant no criticism. I only mentioned a point I thought others might want to consider. – Cerin Feb 06 '13 at 15:58
  • @Cerin: Someone concerned about the inefficiency of having to search a list might want to take a look at my own [answer](http://stackoverflow.com/a/3863845/355230) to the the linked question which returns a dictionary which mapping all the subclass names to the subclasses -- which could be modified to return a different type of mapping since it has access to each subclass when it's defined. – martineau Feb 20 '13 at 08:21
2

Here's something I've been playing around with (that works):

def sublass_registry():
    ''' Create a metaclass to register subclasses '''

    class SublassRegistryMeta(type):
        def __init__(cls, name, bases, classdict):
            if classdict.get('__metaclass__') is SublassRegistryMeta:
                SublassRegistryMeta.lineage = [cls] # put root class at head of a list
            else:
                # sublclasses won't have __metaclass__ explicitly set to this class
                # we know they're subclassees because this ctor is being called for them
                SublassRegistryMeta.lineage.append(cls) # add subclass to list
            type.__init__(cls, name, bases, classdict)

    return SublassRegistryMeta

def subclasses(cls):
    ''' Return a list containing base and subclasses '''

    try:
        if cls.__metaclass__.lineage[0] is cls: # only valid for a root class
            return cls.__metaclass__.lineage
    except AttributeError:
        pass
    return None

class Car(object): # root class
    __metaclass__ = sublass_registry()

class Audi(Car): # inherits __metaclass__
    pass

class Ford(Car): # inherits __metaclass__
    pass

class Audi2(Audi): # sub-subclass also inherits __metaclass__
    pass

print subclasses(Car)
# [<class '__main__.Car'>, <class '__main__.Audi'>, <class '__main__.Ford'>, <class '__main__.Audi2'>]
print subclasses(Audi)
# None
martineau
  • 119,623
  • 25
  • 170
  • 301