Python: запуск кода при подклассе класса

Есть ли способ запуска кода, когда мой класс подклассифицирован?

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")

Должен выводить

foo
was subclassed by SubClass0
bar
test
was subclassed by SubClass1

Ответ 1

Классы (по умолчанию) являются экземплярами type. Так же, как экземпляр класса Foo создается foo = Foo(...), экземпляр type (т.е. класс) создается myclass = type(name, bases, clsdict).

Если вы хотите, чтобы что-то особенное произошло в момент создания класса, тогда вам нужно изменить вещь, создающую класс - т.е. type. Способ сделать это - определить подкласс type - т.е. Метакласс.

Метакласс относится к его классу, поскольку класс относится к его экземпляру.

В Python2 вы должны определить метакласс класса с

class SuperClass:
    __metaclass__ = Watcher

где Watcher является подклассом type.

В Python3 синтаксис был изменен на

class SuperClass(metaclass=Watcher)

Оба эквивалентны

Superclass = Watcher(name, bases, clsdict)

где в этом случае name равно строке 'Superclass', а bases - это набор (object, ). clsdict - это словарь атрибутов класса, определенных в теле определения класса.

Обратите внимание на сходство с myclass = type(name, bases, clsdict).

Итак, так же, как вы бы использовали класс __init__ для управления событиями в момент создания экземпляра, вы можете управлять событиями в момент создания класса с помощью метакласса __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")

печатает

foo
was subclassed by SubClass0
bar
test
was subclassed by SubClass1

Ответ 2

Изменить: мой старый пост на самом деле не работал. Подкласс из classmethod не работает должным образом.

Во-первых, мы хотели бы иметь некоторый способ сообщить метаклассу, что этот конкретный метод должен иметь специальный вызов для поведения подкласса, мы просто установим атрибут функции, которую хотим вызвать. Для удобства мы даже превратим функцию в classmethod, чтобы можно было обнаружить и реальный базовый класс, который был найден. Мы вернем classmethod, чтобы его можно было использовать в качестве декоратора, что наиболее удобно.

import types
import inspect

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

Нам также понадобится удобный способ увидеть, что был использован декоратор subclass_hook. Мы знаем, что используется classmethod, поэтому мы проверим это, и только затем найдите атрибут is_subclass_hook.

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

Наконец, нам нужен метакласс, который воздействует на информацию: для большинства случаев самое интересное, что нужно сделать здесь, - это просто проверить каждую из предоставленных баз для перехватов. Таким образом, супер работает наименее удивительным образом.

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)

и это должно быть сделано.

>>> 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