PythonProgrammationDéveloppeur Python Senior

À travers quel mécanisme de substitution dynamique un objet non-classe **Python** spécifie-t-il ses classes participantes lorsqu'il apparaît dans une liste d'héritage de classe, influençant ainsi le calcul de l'ordre de résolution des méthodes ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse à la question

Historique de la question

Avec l'adoption de PEP 560 dans Python 3.7, le système de types nécessitait un moyen d'utiliser des types génériques tels que List[int] ou Generic[T] comme classes de base. Avant cette amélioration, tenter d'hériter d'un générique paramétré entraînait une TypeError car ces objets n'étaient pas de vraies classes, obligeant les développeurs à recourir à des solutions de contournement complexes de méta-classes qui compliquaient la conception des bibliothèques.

Le problème

Lorsque l'interpréteur traite une définition de classe, il doit calculer l'Ordre de Résolution des Méthodes (MRO) en utilisant l'algorithme de linéarisation C3. Cet algorithme nécessite que toutes les bases soient des classes. Le défi se pose lorsqu'un objet de base n'est pas une classe mais un alias générique ; l'interpréteur a besoin d'un protocole pour déterminer quelles vraies classes devraient remplacer cet alias lors de la construction du MRO sans rompre les sémantiques d'héritage.

La solution

Python a introduit le protocole __mro_entries__. Lorsque la création de la classe rencontre une base avec cette méthode, elle appelle base.__mro_entries__(original_bases) et attend un tuple de classes en retour. Ces classes remplacent la base originale dans le calcul du MRO. Par exemple, typing.Generic implémente cela pour retourner (Generic,), permettant de fonctionner comme une base tout en maintenant la logique paramétrée séparée.

from typing import Generic, TypeVar T = TypeVar('T') # Generic[T] n'est pas une classe, mais __mro_entries__ lui permet d'agir comme une class Container(Generic[T]): pass # Container.__mro__ inclut Generic, pas Generic[T] print(Container.__mro__) # (<class 'Container'>, <class 'typing.Generic'>, <class 'object'>)

Situation de la vie réelle

Une équipe de framework devait permettre aux utilisateurs de définir des modèles de données en utilisant des bases génériques paramétrées comme Model[UserType]. Leur approche initiale utilisait une méta-classe personnalisée pour intercepter la création de classe et extraire les paramètres de type, mais cela obligeait les utilisateurs à résoudre manuellement les conflits de méta-classes lors de la combinaison du framework avec des modèles Django ou SQLAlchemy.

Ils ont envisagé d'utiliser un décorateur de classe pour réécrire la classe après sa définition, mais cette approche rompait la vérification de type statique et l'autocomplétion de IDE parce que la transformation se produisait après que le vérificateur de type avait analysé le code source. Une autre alternative impliquait __init_subclass__, mais cela ne pouvait pas gérer le cas où la base elle-même n'était pas une classe.

L'équipe a implémenté __mro_entries__ sur leurs objets de fabrique génériques. Lorsque les utilisateurs écrivaient class UserModel(Model[UserType]), l'instance Model[UserType] retournait (Model,) depuis sa méthode __mro_entries__. Cela permettait à la classe d'hériter correctement de Model tout en stockant le paramètre de type spécifique pour la validation à l'exécution. La solution a éliminé les conflits de méta-classes, préservé le support complet de IDE et maintenu une hiérarchie d'héritage propre qui satisfaisait l'algorithme de linéarisation C3.

Ce que les candidats oublient souvent

Est-ce que __mro_entries__ affecte la vérification de type à l'exécution ou le comportement d'isinstance ?

Les candidats confondent souvent la construction du MRO avec la vérification d'instance. __mro_entries__ fonctionne exclusivement lors de la création de la classe pour construire le tuple __mro__. Il n'a aucun effet sur les vérifications d'isinstance() ou issubclass() à l'exécution. Ces opérations s'appuient sur les attributs __class__ et __bases__ des classes existantes, et non sur la substitution dynamique qui a eu lieu lors de la définition de la classe.

Pourquoi __mro_entries__ retourne-t-il un tuple plutôt qu'une seule classe ?

Le type de retour tuple s'adapte à des scénarios d'héritage multiple complexes. Bien que cela retourne généralement un tuple à un élément comme (Generic,), le protocole permet à un paramètre générique d'impliquer l'héritage de plusieurs mixins simultanément. Python décompresse ce tuple directement dans la liste des bases pour le calcul du MRO, donc retourner (A, B) fait effectivement hériter la classe à la fois de A et B au lieu de la base non-classe originale.

Quelle validation Python effectue-t-il sur les classes retournées par __mro_entries__ ?

L'interpréteur valide strictement que les classes retournées forment un graphe d'héritage valide. Si le tuple contient des classes qui créeraient un MRO incohérent — comme introduire un conflit d'héritage en diamant qui viole les contraintes de linéarisation C3Python lève une TypeError lors de la création de la classe. Cette validation garantit que la substitution dynamique ne peut pas contourner les règles fondamentales de cohérence d'héritage du langage.