views:

80

answers:

4

I'm trying to writing a generic metaclass for tracking 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 http://docs.python.org/reference/datamodel.html#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).

+4  A: 

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 you can invoke it like:

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.

aaronasterling
There is a syntax error in type(metaclass.root._registry.append(cls) ... I changed it to: `cls._registry.append(cls)` which does work
Carles Barrobés
@Carles That was an ugly syntax error. Like complete and total garbage. I edited a few times so it was a few different ideas for a few different lines that all should have been deleted. I'm sorry about that. It looks good now. Thanks for pointing it out.
aaronasterling
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
@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
A: 

have you look to ABCMeta : http://docs.python.org/library/abc.html

singularity
A: 

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
            return 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
+2  A: 

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

martineau