PythonProgrammationDéveloppeur Python

À travers quel méthode de protocole les descripteurs **Python** reçoivent-ils automatiquement leur nom d'attribut assigné et la classe contenant lors de la création de la classe, et quelle limitation se présente lorsque les descripteurs sont attachés à des classes après leur déclaration ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse à la question.

Historique.

Avant Python 3.6, les descripteurs nécessitant la connaissance de leur nom d'attribut s'appuyaient sur des métaclasses personnalisées ou des décorateurs de classe manuels pour analyser le dictionnaire de la classe et injecter des noms. Cette approche était verbeuse, sujette aux erreurs et créait des conflits de métaclasses dans des hiérarchies complexes. PEP 487 a introduit le protocole __set_name__ dans Python 3.6 pour éliminer cette lourdeur en permettant à l'interpréteur de notifier les descripteurs automatiquement.

Problème.

Une instance de descripteur est créée lors de l'exécution du corps de la classe, mais à ce moment-là, elle n'a aucune connaissance intrinsèque du nom de la variable à laquelle elle est liée ou de la classe dans laquelle elle se trouve. Cette information est essentielle pour générer des messages d'erreur significatifs, enregistrer des champs dans des systèmes ORM ou construire des schémas de sérialisation. Sans notification externe, le descripteur reste anonyme, forçant les développeurs à répéter le nom de l'attribut comme un argument chaîne, violant les principes DRY.

Solution.

Lorsque type.__new__ construit une classe, elle parcourt le mappage de l'espace de noms renvoyé par __prepare__. Pour chaque valeur possédant une méthode __set_name__, l'interpréteur appelle value.__set_name__(owner_class, attribute_name). Cette méthode reçoit la classe en cours de construction et la chaîne de l'attribut, permettant au descripteur de stocker ces métadonnées. Cependant, si un descripteur est assigné à un attribut de classe après l'achèvement du processus de création de la classe (monkey-patching), __set_name__ n'est pas invoqué automatiquement car le mécanisme de type n'est plus actif.

class TrackedDescriptor: def __set_name__(self, owner, name): self.owner = owner self.name = name def __get__(self, instance, owner): if instance is None: return self return f"{self.owner.__name__}.{self.name}" class Model: field = TrackedDescriptor() # Model.field.name == 'field' # Model.field.owner == Model

Situation de la vie réelle

Contexte.

Lors de la création d'une bibliothèque de gestion de configuration, nous avions besoin de descripteurs pour représenter les variables d'environnement. Lorsque qu'une valeur était manquante ou invalide, l'erreur devait spécifier le nom exact de l'attribut dans la classe (par exemple, Config.database_url is required), et non pas simplement un message générique.

Problème.

Au départ, les utilisateurs devaient spécifier le nom manuellement : database_url = EnvVar('database_url'). Cela a conduit à des bugs pendant le refactoring où la chaîne littérale et le nom de la variable divergeaient, causant des erreurs d'exécution cryptiques.

Différentes solutions envisagées :

Injection de métaclasse. Nous avons implémenté un ConfigMeta qui inspectait attrs et appelait attr.set_name(name) sur chaque descripteur. Cela fonctionnait mais forçait toutes les classes utilisateurs à hériter de notre métaclasse, ce qui rompait la compatibilité avec d'autres bibliothèques utilisant leurs propres métaclasses comme abc.ABCMeta. Cela imposait également une surcharge cognitive pour les utilisateurs peu familiers avec les métaclasses.

Patching avec un décorateur de classe. Nous avons créé un décorateur @config qui itérait sur cls.__dict__ après la création de la classe et patchait les noms. Cela évitait les conflits de métaclasses mais était optionnel ; oublier le décorateur entraînait des descripteurs cassés. Cela s'exécutait également après la création de la classe, donc les descripteurs ne pouvaient pas utiliser leurs noms pendant les hooks __init_subclass__, limitant les capacités d'introspection.

Protocole __set_name__. Nous avons ajouté __set_name__ à notre descripteur EnvVar. Cela ne nécessitait aucun changement dans le code de l'utilisateur, fonctionnait automatiquement pendant la définition de la classe et permettait au descripteur de connaître son nom avant que __init_subclass__ ne se termine, facilitant une validation précoce.

Solution choisie.

Nous avons adopté __set_name__ car il fournissait une abstraction sans coût pour les utilisateurs et s'intégrait au modèle de données natif de Python. Cela a complètement éliminé le problème de collision de métaclasses.

Résultat.

L'API est devenue déclarative : database_url = EnvVar(). Les outils de refactoring pouvaient renommer les attributs en toute sécurité, et les messages d'erreur restaient précis. La base de code a été réduite de 150 lignes de code de métaclasse, et nous avons constaté moins de rapports de bogues liés aux incohérences dans les clés de configuration.

Ce que les candidats manquent souvent

Quand exactement __set_name__ est-il invoqué durant le cycle de création de la classe ?

Il est invoqué par type.__new__ immédiatement après que le corps de la classe a terminé de s'exécuter et que le dictionnaire de l'espace de noms est peuplé, mais avant que __init_subclass__ ne soit appelé sur les classes parentes. Ce timing est critique car il permet aux descripteurs de finaliser leur état avant que les sous-classes ne soient initialisées. Cela ne déclenche pas l'ajout d'attributs à une classe déjà créée (par exemple, setattr(MyClass, 'new_attr', descriptor())), car le protocole de création de classe a conclu. Comprendre cette distinction est vital pour la manipulation dynamique des classes.

Pourquoi __set_name__ reçoit-il à la fois la classe propriétaire et le nom comme arguments plutôt que d'en déduire à partir de self ?

L'instance de descripteur existe indépendamment de la classe ; elle peut être instanciée avant la création de la classe et théoriquement pourrait être assignée à plusieurs classes (bien que rare). L'argument owner garantit que le descripteur connaît la classe spécifique dans laquelle l'assignation s'est produite, ce qui est nécessaire pour gérer correctement l'héritage. Si un descripteur est défini dans une classe de base, __set_name__ est appelé avec la classe de base ; s'il est remplacé dans une sous-classe avec une nouvelle instance, il est appelé avec la sous-classe. Cela permet des registres par classe sans contamination croisée entre les classes de base et dérivées.

Comment __set_name__ interagit-il avec les méthodes de protocole descripteur __set__ et __get__ ?

__set_name__ est purement un hook d'initialisation et ne participe pas au protocole d'accès aux attributs (__get__ / __set__). Cependant, il permet à ces méthodes de fonctionner correctement en fournissant le contexte nécessaire pour les opérations. Une erreur courante consiste à supposer que __set_name__ sera appelé à nouveau lorsqu'un descripteur est hérité par une sous-classe qui ne le remplace pas. Puisque la même instance de descripteur est réutilisée, __set_name__ n'est pas ré-invoquée ; par conséquent, les descripteurs suivant l'état par classement doivent utiliser __init_subclass__ ou vérifier owner dans __get__ pour gérer l'héritage, plutôt que de compter uniquement sur __set_name__ pour une logique spécifique aux sous-classes.