PythonProgrammationDéveloppeur Python senior

Quel méthode d'instance spécifique définie sur une **metaclasse** **Python** intercepte les appels `isinstance()` visant ses instances, et quel risque de récursion infinie surgit si cette méthode accède à des attributs arbitraires sur l'objet candidat ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse à la question.

Lorsque isinstance(obj, cls) est invoqué, Python vérifie si type(cls) définit __instancecheck__(self, instance). Si présent, cette méthode de metaclasse détermine l'appartenance, permettant le "sous-typage virtuel" sans héritage. Le danger survient lorsque l'implémentation utilise hasattr() ou l'accès par attribut sur obj ; si obj implémente __getattr__ ou des descripteurs qui déclenchent des vérifications isinstance(), la méthode de metaclasse y revient, causant une récursion sans fin.

Situation de la vie réelle

Nous avons conçu un cadre de validation où des plugins devaient satisfaire des interfaces sans héritage explicite. Nous avons défini un ABC Processor et voulu que isinstance(plugin, Processor) réussisse si plugin exposait une méthode process, soutenant le duck typing pour des bibliothèques externes.

La première approche exigeait que tous les plugins héritent de Processor ou appellent Processor.register(). Ceci était sûr pour les types mais impraticable pour notre cas d'utilisation ; cela interdisait les classes générées à l'exécution et nécessitait de modifier un code tiers que nous ne pouvions pas changer. Par conséquent, cela échouait à soutenir la découverte de plugins dynamiques provenant de sources non fiables.

La deuxième approche a implémenté __instancecheck__ sur la metaclasse Processor en utilisant hasattr(candidate, 'process'). Bien que flexible, cela a échoué avec RecursionError lorsque un plugin utilisait un décorateur de propriété qui validait son type de retour via isinstance, invoquant de nouveau la méthode de metaclasse avant le retour du premier appel.

Nous avons adopté une troisième solution : implémenter __instancecheck__ en utilisant object.__getattribute__ pour contourner la logique des descripteurs. En vérifiant type(candidate).__dict__.get('process') et en s'assurant qu'il est appelable, nous avons évité de déclencher les effets secondaires de __getattr__ ou de propriétés définies par l'utilisateur. Cela a éliminé le risque de récursion tout en préservant le duck typing dynamique, permettant au cadre d'intégrer en toute sécurité des milliers de plugins hétérogènes sans modification de source.

class MetaProcessor(type): def __instancecheck__(cls, candidate): # Sûr : contourne __getattr__ et descripteurs try: attr = type(candidate).__dict__.get('process') return callable(attr) except Exception: return False class Processor(metaclass=MetaProcessor): pass

Ce que les candidats manquent souvent


Pourquoi isinstance() consulte-t-il la metaclasse du deuxième argument plutôt que du premier ?

Le protocole attribue l'autorité au type auquel on compare (cls), et non à l'objet candidat (obj). En plaçant __instancecheck__ sur type(cls), Python garantit que seule la définition de classe contrôle sa sémantique d'appartenance. Cela empêche les objets de falsifier les vérifications d'instance ; le type définit unilatéralement ce qui constitue son instance, maintenant l'intégrité dans les vérifications de type sensibles à la sécurité.


Quelle est la relation entre __instancecheck__ et __subclasscheck__, et pourquoi doivent-ils rester cohérents ?

__instancecheck__ valide des objets pour isinstance(), tandis que __subclasscheck__ valide des types pour issubclass(). Si __instancecheck__ accepte des instances virtuelles (objets n'héritant pas de la classe), __subclasscheck__ doit généralement accepter les sous-classes virtuelles correspondantes pour préserver l'invariant que isinstance(obj, cls) implique issubclass(type(obj), cls). Violer cela fait échouer le code de conteneur générique lorsqu'il vérifie des relations de sous-classe après que des vérifications d'instance aient réussi.


Comment l'utilisation de getattr() à l'intérieur de __instancecheck__ déclenche-t-elle spécifiquement une récursion infinie par rapport à object.__getattribute__ ?

getattr(obj, 'name') invoque le protocole complet des descripteurs et peut appeler obj.__getattr__('name') si l'attribut est manquant. Si obj.__getattr__ implémente le chargement paresseux, la journalisation, ou la coercition de type qui appelle en interne isinstance(obj, OurClass), la metaclasse __instancecheck__ y revient avant le retour. En revanche, object.__getattribute__(obj, 'name') (ou inspecter type(obj).__dict__) contourne complètement __getattr__ et les descripteurs personnalisés, accédant aux détails d'implémentation bruts sans déclencher du code utilisateur qui pourrait récursivement revenir.