158

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
martineau
  • 119,623
  • 25
  • 170
  • 301
mic_e
  • 5,594
  • 4
  • 34
  • 49
  • 1
    Use a metaclass; metaclasses are called when classes are created, just as classes are called when instances are created. – Martijn Pieters Aug 08 '13 at 12:53
  • 7
    Can't add an answer, but today python3.6 have the `__init_subclass__` - check it out! – Or Duan Jun 14 '17 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. – mic_e Jun 19 '17 at 06:13

2 Answers2

83

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
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • 4
    Thanks. However, your code only works in python2. In python3, SuperClass would need to be `class SuperClass(metaclass = Watcher): pass` – mic_e Aug 08 '13 at 13:19
  • Thanks. I've edited the post to address the difference in Python2 and Python3 syntax. – unutbu Aug 08 '13 at 14:09
  • 1
    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`. – ArtOfWarfare Sep 19 '15 at 14:36
  • 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() ``` – susundberg Oct 14 '20 at 06:12
12

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
SingleNegationElimination
  • 151,563
  • 33
  • 264
  • 304