158 votes

Comment exécuter du code lorsqu'une classe est sous-classée ?

Existe-t-il un moyen de déclencher du code lorsque ma classe est sous-classée ?

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

Devrait sortir

foo
was subclassed by SubClass0
bar
test
was subclassed by SubClass1

83voto

unutbu Points 222216

Les classes (par défaut) sont des instances de type . Tout comme une instance d'une classe Foo est créé par foo = Foo(...) , une instance de type (c'est-à-dire une classe) est créé par myclass = type(name, bases, clsdict) .

Si vous voulez que quelque chose de spécial se produise au moment de la création de la classe, alors vous devez modifier la chose qui crée la classe -- c'est à dire type . La façon de procéder est de définir une sous-classe de type -- c'est-à-dire une métaclasse.

Une métaclasse est à sa classe ce qu'une classe est à son instance.

En Python2, vous définiriez la métaclasse d'une classe avec

class SuperClass:
    __metaclass__ = Watcher

donde Watcher est une sous-classe de type .

Dans Python3, la syntaxe a été modifiée comme suit

class SuperClass(metaclass=Watcher)

Les deux sont équivalents à

Superclass = Watcher(name, bases, clsdict)

où dans ce cas, name est égal à la chaîne de caractères 'Superclass' y bases est le tuple (object, ) . Le site clsdict est un dictionnaire des attributs de classe définis dans le corps de la définition de la classe.

Notez la similitude avec myclass = type(name, bases, clsdict) .

Ainsi, de la même manière que vous utiliseriez la classe __init__ pour contrôler les événements au moment de la création d'une instance, vous pouvez contrôler les événements au moment de la création d'une classe à l'aide d'une métaclasse __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")

imprime

foo
was subclassed by SubClass0
bar
test
was subclassed by SubClass1

12voto

IfLoop Points 59461

Edit : Mon ancien post n'a en fait pas fonctionné. Sous-classement de classmethod ne fonctionne pas comme prévu.

Tout d'abord, nous aimerions avoir un moyen d'indiquer à la métaclasse que cette méthode particulière est censée avoir le comportement spécial d'appel sur la sous-classe, nous allons simplement définir un attribut sur la fonction que nous aimerions appeler. Pour des raisons de commodité, nous allons même transformer la fonction en un objet classmethod afin que la vraie classe de base dans laquelle il a été trouvé puisse également être découverte. Nous retournons la classmethod pour qu'elle puisse être utilisée comme décorateur, ce qui est très pratique.

import types
import inspect

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

Nous allons également vouloir un moyen pratique de voir que les subclass_hook Le décorateur a été utilisé. Nous savons que classmethod a été utilisé, nous allons donc vérifier cela, et seulement ensuite chercher l'élément is_subclass_hook attribut.

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

Enfin, nous avons besoin d'une métaclasse qui agit sur les informations : Dans la plupart des cas, la chose la plus intéressante à faire ici est simplement de vérifier chacune des bases fournies pour les hooks. De cette façon, super fonctionne de la manière la moins surprenante.

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)

et ça devrait le faire.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X