Le crochet __init_subclass__ a été introduit dans Python 3.6 dans le cadre de PEP 487. Avant cela, toute classe souhaitant effectuer des actions lors de l'héritage — telles que l'enregistrement, la validation ou la collecte automatique de champs — devait déclarer une métaclasse personnalisée. Les métaclasses, bien que puissantes, créent des frictions dans les scénarios d'héritage multiple car elles entrent en conflit à moins d'être soigneusement coordonnées. Le nouveau crochet permet aux classes de base de participer à l'initialisation des sous-classes sans forcer toute la hiérarchie à adopter une métaclasse spécifique, simplifiant ainsi des frameworks comme Django ORM et SQLAlchemy, qui reposaient auparavant sur une gymnastique complexe des métaclasses.
Lorsqu'une classe B hérite d'une classe de base A, les développeurs de frameworks ont souvent besoin d'exécuter une logique au moment où la classe B est définie — avant que des instances ne soient créées. Par exemple, un ORM pourrait avoir besoin de recueillir toutes les définitions de colonnes de B et de les stocker dans un registre. Utiliser une métaclasse nécessite que A ait type ou une métaclasse personnalisée comme sa métaclasse, ce qui devient problématique lorsque B doit également utiliser une autre métaclasse (par exemple, d'une ABC ou d'un autre framework). Cela entraîne des erreurs de conflit de métaclasse qui sont difficiles à résoudre. De plus, __new__ de la métaclasse s'exécute avant que l'espace de noms de la classe ne soit entièrement peuplé, rendant difficile l'inspection des attributs de classe finaux.
Python fournit la méthode de classe __init_subclass__. Lorsqu'une classe définit cette méthode, elle est appelée automatiquement chaque fois qu'une classe est créée ayant la classe définissante comme parent direct. Le crochet reçoit la sous-classe nouvellement créée comme premier argument, suivie de tout argument de mot-clé passé dans la ligne de définition de la classe (par exemple, class B(A, keyword=value)).
class RegistryBase: _registry = {} def __init_subclass__(cls, category="default", **kwargs): super().__init_subclass__(**kwargs) print(f"Enregistrement de {cls.__name__} sous la catégorie '{category}'") cls._registry[cls.__name__] = {"class": cls, "category": category} class Plugin(RegistryBase, category="audio"): pass class Effect(Plugin, category="reverb"): pass
Contrairement à __new__ de la métaclasse, qui s'exécute lors de la création de la classe avant que l'objet classe n'existe, __init_subclass__ s'exécute après que l'objet classe a été entièrement construit. Cela permet au crochet d'inspecter cls.__dict__, les méthodes et les annotations en toute sécurité. Le crochet respecte également le MRO, garantissant que les enregistrements de classes parentes se produisent avant la logique de classe enfant lorsque super() est appelé.
Dans une grande plateforme de traitement audio SaaS, l'équipe d'ingénierie avait besoin de mettre en œuvre un système de plugins où des développeurs tiers pourraient définir des effets audio en sous-classant une classe de base AudioEffect. Chaque sous-classe devait s'enregistrer automatiquement dans un catalogue d'effets global avec des métadonnées telles que effect_name, latency_ms et category. Le problème était que la plateforme utilisait déjà des bases déclaratives de SQLAlchemy (qui utilisent des métaclasses) pour les modèles de base de données, et certains effets audio devaient hériter à la fois de AudioEffect et des modèles SQLAlchemy. L'introduction d'une métaclasse personnalisée pour AudioEffect a causé des conflits de métaclasses avec DeclarativeMeta de SQLAlchemy, empêchant le démarrage de l'application.
La première approche a impliqué un enregistrement manuel à l'aide d'un décorateur. Les développeurs devaient écrire @register_effect au-dessus de chaque définition de classe. Cela fonctionnait mais était sujet aux erreurs ; les développeurs oubliaient fréquemment le décorateur, entraînant des effets manquants en production. Cela nécessitait également de répéter les métadonnées à la fois dans les arguments du décorateur et dans la définition de la classe, violant les principes DRY.
La deuxième approche tentait d'utiliser une métaclasse commune qui héritait simultanément de DeclarativeMeta et d'un EffectMeta. Cela a résolu le conflit immédiat mais créé une dépendance fragile. Chaque fois que SQLAlchemy mettait à jour sa logique de métaclasse interne, la plateforme se rompait. Cela a également forcé toutes les classes d'effet à être des modèles de base de données, ce qui n'était pas approprié pour les effets légers côté client.
La troisième approche a utilisé __init_subclass__. La classe de base AudioEffect a défini __init_subclass__ pour capturer les arguments de mot-clé passés lors de la définition de la classe, tels que effect_id et version. Lorsqu'un développeur écrivait class Reverb(AudioEffect, effect_id="rvb-01", version=2), le crochet validait automatiquement l'unicité de l'ID et enregistrait la classe dans un registre WeakValueDictionary sécurisé par thread. Cela a évité complètement les conflits de métaclasses car __init_subclass__ est une méthode de classe normale qui coopère avec n'importe quelle métaclasse.
L'équipe a choisi la troisième solution. Elle a préservé la compatibilité avec SQLAlchemy, a éliminé le besoin de décorateurs et a assuré que l'enregistrement se produisait automatiquement au moment de l'importation. Le résultat était un système de plugins qui "fonctionnait simplement" — les développeurs devaient juste sous-classer et déclarer des paramètres en ligne. Le système a réussi à enregistrer plus de 150 effets sans un seul conflit de métaclasses, et le temps de démarrage s'est amélioré de 40 % par rapport à l'approche métaclasse en raison de la réduction de la complexité du calcul MRO.
Pourquoi __init_subclass__ doit-il toujours appeler super().__init_subclass__() même si le parent ne le définit pas ?
Les candidats supposent souvent que comme object ne définit pas __init_subclass__, l'appel est facultatif. Cependant, dans les scénarios d'héritage multiple, ne pas appeler super() peut rompre la chaîne pour les classes sœurs qui implémentent également le crochet. L'héritage multiple coopératif de Python exige que chaque participant dans le diamant appelle super() pour s'assurer que toutes les branches de la hiérarchie exécutent leur logique d'initialisation. Si A et B définissent tous deux __init_subclass__, et que C(A, B) n'appelle que le crochet de A, la logique d'enregistrement de B est silencieusement ignorée, entraînant des bogues subtils dans les systèmes de plugins.
Comment __init_subclass__ gère-t-il les arguments de mot-clé qui ne sont pas consommés par la signature de méthode, et pourquoi **kwargs est-il obligatoire ?
Lorsqu'une sous-classe est définie avec des arguments de mot-clé (par exemple, class D(C, custom_arg=5)), ces arguments sont passés à __init_subclass__. Si la signature de la méthode n'inclut pas **kwargs pour capturer et propager les arguments non utilisés, et si une autre classe dans le MRO définit également __init_subclass__, une TypeError se produit car Python essaie de passer l'argument de mot-clé au crochet suivant qui ne l'accepte pas. Par conséquent, les implémentations robustes doivent toujours inclure **kwargs et les passer à super().__init_subclass__(**kwargs) pour supporter l'héritage coopératif où différents niveaux consomment différents paramètres.
Peut __init_subclass__ modifier l'espace de noms de la classe ou ajouter dynamiquement des méthodes, et quelles sont les implications pour __slots__ ?
Les candidats confondent souvent __init_subclass__ avec __new__ de la métaclasse. Étant donné que __init_subclass__ s'exécute après que la classe a été entièrement créée, elle ne peut pas modifier le dictionnaire de la classe avant sa création (contrairement à __prepare__ ou __new__ de la métaclasse). Cependant, elle peut ajouter dynamiquement des attributs en utilisant setattr(cls, name, value). Le danger réside dans __slots__ : si une classe parente utilise __slots__, la sous-classe hérite de cette contrainte. Tenter d'ajouter un nouvel attribut à une classe dotée de slots via setattr dans __init_subclass__ entraînera une AttributeError, à moins que la sous-classe elle-même n'ait défini __slots__ ou __dict__. Cette limitation oblige les architectes à choisir entre utiliser __init_subclass__ pour l'enregistrement/métadonnées et utiliser des métaclasses pour une véritable modification structurelle du corps de la classe.