161

Is there a way to trigger code when my class is subclassed?

class SuperClass: def triggered_routine(subclass): print("was subclassed by " + subclass.__name__) magically_register_triggered_routine() print("foo") class SubClass0(SuperClass): pass print("bar") class SubClass1(SuperClass): print("test") 

Should output

foo was subclassed by SubClass0 bar test was subclassed by SubClass1 
3
  • 1
    Use a metaclass; metaclasses are called when classes are created, just as classes are called when instances are created. Commented Aug 8, 2013 at 12:53
  • 7
    Can't add an answer, but today python3.6 have the __init_subclass__ - check it out! Commented Jun 14, 2017 at 14:35
  • 1
    @OrDuan: thanks, sounds useful. Might even be reason enough to un-mark this question a duplicate, since there is now a dedicated solution for my problem instead of the "use a metaclass" one. Commented Jun 19, 2017 at 6:13

2 Answers 2

84

Classes (by default) are instances of type. Just as an instance of a class Foo is created by foo = Foo(...), an instance of type (i.e. a class) is created by myclass = type(name, bases, clsdict).

If you want something special to happen at the moment of class-creation, then you have to modify the thing creating the class -- i.e. type. The way to do that is to define a subclass of type -- i.e. a metaclass.

A metaclass is to its class as a class is to its instance.

In Python2 you would define the metaclass of a class with

class SuperClass: __metaclass__ = Watcher 

where Watcher is a subclass of type.

In Python3 the syntax has been changed to

class SuperClass(metaclass=Watcher) 

Both are equivalent to

Superclass = Watcher(name, bases, clsdict) 

where in this case, name equals the string 'Superclass', and bases is the tuple (object, ). The clsdict is a dictionary of the class attributes defined in the body of the class definition.

Note the similarity to myclass = type(name, bases, clsdict).

So, just as you would use a class's __init__ to control events at the moment of a instance's creation, you can control events at the moment of a class's creation with a metaclass's __init__:


class Watcher(type): def __init__(cls, name, bases, clsdict): if len(cls.mro()) > 2: print("was subclassed by " + name) super(Watcher, cls).__init__(name, bases, clsdict) class SuperClass: __metaclass__ = Watcher print("foo") class SubClass0(SuperClass): pass print("bar") class SubClass1(SuperClass): print("test") 

prints

foo was subclassed by SubClass0 bar test was subclassed by SubClass1 
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks. However, your code only works in python2. In python3, SuperClass would need to be class SuperClass(metaclass = Watcher): pass
Thanks. I've edited the post to address the difference in Python2 and Python3 syntax.
Two key things that I missed when I tried this on my own: 1 - Watcher subclasses type, not object. 2 - SuperClass has no superclass at all, not object. I'm just so used to writing down object as a superclass for every class I defined that I somehow missed it that this code was using something else. Although there doesn't seem to be any harm in making SuperClass inherit from object... it's definitely necessary for Watcher to inherit from type, not object.
To update the solution in python3 and add usage of abstract classes (as i see that proper when we require such registry): ``` import abc class Watcher(abc.ABCMeta): def __init__(cls, name, bases, clsdict): if len(cls.mro()) > 2: print("was subclassed by " + name) super().__init__( name, bases, clsdict) class SuperClass( metaclass=Watcher): @abc.abstractmethod def method(): pass class SubClass0(SuperClass): def method(): print("S0 ok") class SubClass1(SuperClass): pass s1 = SubClass0() s1 = SubClass1() ```
13

Edit: My old post actually didn't work. Subclassing from classmethod doesn't work as expected.

First, we would like to have some way to tell the metaclass that this particular method is supposed to have the special called on subclass behavior, we'll just set an attribute on the function we'd like to call. As a convenience, we'll even turn the function into a classmethod so that the real baseclass it was found in can be discovered, too. We'll return the classmethod so that it can be used as a decorator, which is most convenient.

import types import inspect def subclass_hook(func): func.is_subclass_hook = True return classmethod(func) 

We're also going to want a convenient way to see that the subclass_hook decorator was used. We know that classmethod has been used, so we'll check for that, and only then look for the is_subclass_hook attribute.

def test_subclass_hook(thing): x = (isinstance(thing, types.MethodType) and getattr(thing.im_func, 'is_subclass_hook', False)) return x 

Finally, we need a metaclass that acts on the information: For most cases, the most interesting thing to do here is just check each of the supplied bases for hooks. In that way, super works in the least surprising way.

class MyMetaclass(type): def __init__(cls, name, bases, attrs): super(MyMetaclass, cls).__init__(name, bases, attrs) for base in bases: if base is object: continue for name, hook in inspect.getmembers(base, test_subclass_hook): hook(cls) 

and that should do it.

>>> class SuperClass: ... __metaclass__ = MyMetaclass ... @subclass_hook ... def triggered_routine(cls, subclass): ... print(cls.__name__ + " was subclassed by " + subclass.__name__) >>> class SubClass0(SuperClass): ... pass SuperClass was subclassed by SubClass0 >>> class SubClass1(SuperClass): ... print("test") test SuperClass was subclassed by SubClass1 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.