Historique de la question
Dans le modèle de données de Python, l'accès aux attributs suit un protocole strict où __getattribute__ est défini sur la classe de base object et sert d'intercepteur principal pour chaque recherche d'attribut. Cette méthode est invoquée de manière inconditionnelle pour tous les accès aux attributs, qu'ils existent ou non, faisant d'elle la première ligne de défense dans la chaîne de résolution. En revanche, __getattr__ est un crochet optionnel que l'interpréteur appelle seulement lorsque la recherche normale dans le dictionnaire d'instance et la hiérarchie de classe échoue à localiser le nom demandé.
Le problème
Lorsque une sous-classe redéfinit __getattribute__ pour personnaliser le comportement, comme la journalisation ou le contrôle d'accès, tout accès direct à un attribut dans le corps de la méthode — comme self.attr ou self.__dict__ — déclenche la même méthode redéfinie de manière récursive. Cela crée une boucle infinie car le mécanisme de recherche a été détourné sans cas de base pour terminer la récursion, épuisant finalement la pile d'appels et déclenchant une RecursionError.
La solution
Pour implémenter __getattribute__ en toute sécurité, vous devez déléguer à l'implémentation de base en utilisant super().__getattribute__(name) ou object.__getattribute__(self, name). Cela contourne la logique redéfinie et effectue la récupération effective de l'attribut à partir du dictionnaire d'instance ou de la hiérarchie de classe sans ré-entrer dans la méthode personnalisée. Le motif garantit que vous pouvez envelopper, valider ou transformer le résultat tout en maintenant l'intégrité du modèle d'objet et en évitant les boucles infinies.
Exemple de code
class SafeProxy: def __init__(self, wrapped): # Doit utiliser super() ici pour éviter la récursion lors de l'initialisation super().__setattr__('_wrapped', wrapped) def __getattribute__(self, name): # Journaliser l'accès avant la récupération print(f"Accès : {name}") # Déléguer à l'objet pour éviter la récursion infinie return super().__getattribute__(name)
Scénario
Une équipe de développement a besoin de mettre en œuvre un audit pour un modèle ORM hérité où chaque accès à un champ doit être enregistré pour des raisons de conformité sans modifier les classes de modèles d'origine. Ils nécessitent une solution qui intercepte les lectures de manière transparente pour éviter de casser la logique commerciale existante à travers des centaines de modules.
Description du problème
Le système nécessite d'intercepter à la fois les attributs existants et manquants pour enregistrer les horodatages et les actions des utilisateurs. La simple sous-classe et l'ajout de journaux aux méthodes individuelles sont impraticables en raison du grand nombre de champs dynamiques. La solution doit être transparente pour le code existant et ne peut pas altérer l'interface publique des modèles.
Solution 1 : Monkey-patch des méthodes du modèle
Cette approche consiste à remplacer dynamiquement les méthodes sur la classe à l'exécution pour injecter des appels de journalisation, ciblant des comportements spécifiques sans altérer les définitions sources. Cela permet une application conditionnelle basée sur la configuration et évite les complications d'héritage. Cependant, cela échoue à intercepter l'accès direct aux attributs de données ou aux valeurs simples, nécessite une maintenance pour chaque nouvelle méthode et échoue lorsque les détails d'implémentation internes changent.
Solution 2 : Utiliser __getattr__ pour la journalisation
L'implémentation de __getattr__ pour enregistrer l'accès aux attributs manquants ne fournit qu'un mécanisme de secours simple. Il est sécurisé contre les problèmes de récursion et facile à mettre en œuvre avec un minimum de code standard. Malheureusement, cela ne se déclenche que pour les attributs non trouvés dans l'instance ou la classe, manquant la majorité des accès aux champs existants, ce qui conteste l'exigence d'audit pour une journalisation complète.
Solution 3 : Classe proxy avec __getattribute__
Créer une classe d'enveloppement qui implémente __getattribute__ intercepte toutes les lectures d'attributs avant de déléguer à l'instance ORM enveloppée, capturant chaque accès uniformément. Cela maintient la transparence par la composition et permet un pré- et post-traitement sans toucher au code hérité. L'inconvénient est la nécessité d'une gestion prudente de la récursion et d'un léger surcoût de performance en raison de l'appel de méthode supplémentaire à chaque accès d'attribut.
Solution choisie
L'équipe a sélectionné l'approche de proxy avec __getattribute__ parce que les réglementations de conformité exigeaient de capturer chaque lecture d'attribut, y compris les champs de données simples que les méthodes ne touchent jamais. Le motif de proxy a fourni des capacités d'interception complètes tout en maintenant l'encapsulation, permettant à l'héritage ORM de rester intact et inconscient de la couche d'audit. Ce choix a sacrifié une performance minimale pour une couverture complète et une intégrité d'audit.
Résultat
L'implémentation a réussi à enregistrer plus de 50 000 accès à des attributs par heure en production sans une seule erreur de récursion ni modification de la base de code héritée. Le motif de délégation utilisant super() a assuré un fonctionnement stable, et le proxy pouvait être désactivé dans les environnements de test en supprimant simplement l'instantiation de l'enveloppe, démontrant la flexibilité de l'approche.
Pourquoi l'accès à self.__dict__ dans __getattribute__ déclenche-t-il une récursion infinie ?
Lorsque vous écrivez self.__dict__ dans une méthode __getattribute__ redéfinie, Python doit rechercher l'attribut nommé __dict__ sur l'instance. Cette recherche invoque à nouveau votre méthode __getattribute__ personnalisée, qui essaie d'accéder à self.__dict__ encore une fois, créant un cycle sans fin. Pour briser cette boucle, vous devez utiliser object.__getattribute__(self, '__dict__'), ce qui contourne votre redéfinition et récupère le dictionnaire directement à partir de l'implémentation de base.
Comment __getattribute__ affecte-t-il les protocoles de descripteur différemment de __getattr__ ?
__getattribute__ se trouve au tout début de la chaîne de résolution d'attributs, ce qui signifie qu'il intercepte les recherches avant que le protocole de descripteur ne vérifie les méthodes __get__. Si votre implémentation retourne une valeur sans déléguer à super(), les descripteurs tels que property ou des descripteurs de données personnalisés sont complètement contournés. En revanche, __getattr__ n'exécute que lorsque le protocole de descripteur et la recherche dans le dictionnaire d'instance ont tous deux échoué, il n'intercepte donc jamais les descripteurs qui existent dans la hiérarchie de classe.
Quelle est la conséquence de lever une AttributeError manuellement à l'intérieur de __getattribute__ ?
Contrairement à l'accès standard aux attributs où une AttributeError pourrait déclencher __getattr__ comme solution de secours, Python traite __getattribute__ comme la source autoritaire. Si votre implémentation personnalisée lève une AttributeError, l'interpréteur propage l'exception immédiatement sans tenter d'appeler __getattr__. Cela signifie que vous ne pouvez pas vous fier à __getattr__ pour gérer les attributs manquants si votre crochet principal échoue ; à la place, vous devez gérer les clés manquantes dans __getattribute__ ou vous assurer que vous déléguez à l'implémentation parente qui déclenche l'exception correctement.