19. Magic Methods Avancés

__getattr__ vs __getattribute__, __new__, __del__, __copy__/__deepcopy__, __instancecheck__, __subclasshook__, __set_name__, __init_subclass__.


19.1 __getattr__ vs __getattribute__

La différence fondamentale :

class Exemple:
    def __init__(self):
        self.x = 10
 
    def __getattr__(self, name):
        """Appelé SEULEMENT si l'attribut n'existe pas."""
        return f"{name} n'existe pas"
 
    def __getattribute__(self, name):
        """Appelé TOUJOURS en premier."""
        if name == "secret":
            raise AttributeError("Interdit")
        return super().__getattribute__(name)
 
e = Exemple()
e.x         # 10 (via __getattribute__ normal)
e.y         # "y n'existe pas" (via __getattr__)
# e.secret  # AttributeError

Cas d’usage :

  • __getattr__ : proxies, délégation, attributs virtuels
  • __getattribute__ : contrôle d’accès, logging, validation

Proxy par __getattr__

class Proxy:
    def __init__(self, obj):
        self._obj = obj
 
    def __getattr__(self, name):
        if name.startswith("_"):
            raise AttributeError(name)
        return getattr(self._obj, name)
 
    def __setattr__(self, name, value):
        if name == "_obj":
            super().__setattr__(name, value)
        else:
            setattr(self._obj, name, value)
 
class Data:
    def __init__(self):
        self.valeur = 42
 
d = Data()
p = Proxy(d)
print(p.valeur)  # 42 (délégue à d)
p.valeur = 100
print(d.valeur)  # 100

19.2 __setattr__, __delattr__

class Validé:
    def __setattr__(self, name, value):
        if name == "âge" and (not isinstance(value, int) or value < 0):
            raise ValueError("Âge invalide")
        super().__setattr__(name, value)
 
    def __delattr__(self, name):
        if name == "âge":
            raise AttributeError("Ne peut pas supprimer l'âge")
        super().__delattr__(name)

19.3 __new__ — constructeur réel

Appelé avant __init__. Crée l’instance. Retourne une instance.

class Singleton:
    _instance = None
 
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
 
    def __init__(self, valeur):
        # Attention : __init__ est appelé à chaque fois
        if not hasattr(self, 'initialisé'):
            self.valeur = valeur
            self.initialisé = True

Cas d’usage :

  • Singleton (comme ci-dessus)
  • Classes immuables (comme tuple, int, str)
  • Pool d’objets
  • __new__ avec héritage de types immuables

Sous-classer tuple avec __new__

class Point:
    def __new__(cls, x, y):
        instance = super().__new__(cls)
        instance.x = x
        instance.y = y
        return instance
 
class PointTuple(tuple):
    def __new__(cls, x, y):
        return super().__new__(cls, (x, y))
 
    @property
    def x(self):
        return self[0]
 
    @property
    def y(self):
        return self[1]
 
p = PointTuple(3, 4)
p.x, p.y, p[0], p[1]  # 3 4 3 4

19.4 __del__ — destructeur

Appelé lors de la garbage collection. Pas garanti d’être appelé.

class Ressource:
    def __init__(self, nom):
        self.nom = nom
        print(f"Ouverture de {nom}")
 
    def __del__(self):
        print(f"Fermeture de {self.nom}")

Ne pas utiliser pour la gestion de ressources → préférer les context managers (with).

19.5 __copy__ et __deepcopy__

import copy
 
class Arbre:
    def __init__(self, valeur, enfants=None):
        self.valeur = valeur
        self.enfants = enfants or []
 
    def __copy__(self):
        """Copie superficielle personnalisée."""
        return Arbre(self.valeur, list(self.enfants))
 
    def __deepcopy__(self, memo):
        """Copie profonde personnalisée."""
        enfants = [copy.deepcopy(e, memo) for e in self.enfants]
        return Arbre(copy.deepcopy(self.valeur, memo), enfants)
a = Arbre(1, [Arbre(2), Arbre(3)])
b = copy.copy(a)      # __copy__
c = copy.deepcopy(a)  # __deepcopy__

19.6 __instancecheck__ et __subclasscheck__

Personnaliser isinstance et issubclass :

class Meta(type):
    def __instancecheck__(cls, instance):
        print(f"isinstance check pour {instance}")
        return type.__instancecheck__(cls, instance)
 
    def __subclasscheck__(cls, subclass):
        print(f"issubclass check pour {subclass}")
        return type.__subclasscheck__(cls, subclass)
 
class MaClasse(metaclass=Meta):
    pass
 
isinstance(MaClasse(), MaClasse)  # True après logs

Avec Protocol et __subclasshook__ :

from typing import Protocol
 
class Volant(Protocol):
    def voler(self) -> None: ...
 
    @classmethod
    def __subclasshook__(cls, other):
        for c in other.__mro__:
            if "voler" in c.__dict__:
                return True
        return NotImplemented
 
class Oiseau:
    def voler(self):
        print("Vole")
 
issubclass(Oiseau, Volant)  # True (via __subclasshook__)

19.7 __set_name__ — connaître le nom de l’attribut

Vue en leçon 10b. Rappel :

class Descripteur:
    def __set_name__(self, owner, name):
        self._name = f"_{name}"
 
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self._name)
 
    def __set__(self, obj, value):
        setattr(obj, self._name, value)
 
class Point:
    x = Descripteur()
    y = Descripteur()

19.8 __init_subclass__ — hook à l’héritage

Vue en leçons 10b et 18. Rappel :

class PluginBase:
    registry = {}
 
    def __init_subclass__(cls, tag=None, **kwargs):
        super().__init_subclass__(**kwargs)
        if tag:
            PluginBase.registry[tag] = cls
 
class Plugin(PluginBase, tag="maths"):
    pass

19.9 __class_getitem__ — subscriptable sur la classe

class Pile:
    def __class_getitem__(cls, item):
        return f"Pile[{item}]"
 
Pile[int]        # "Pile[int]"

19.10 __length_hint__ — estimation de taille

class Itérable:
    def __init__(self, n):
        self.n = n
 
    def __iter__(self):
        return iter(range(self.n))
 
    def __length_hint__(self):
        return self.n  # estimation, pas obligé d'être exact
 
list(Itérable(5))  # [0, 1, 2, 3, 4]

19.11 __reversed__ — inversion personnalisée

class Plage:
    def __init__(self, start, stop):
        self.range = range(start, stop)
 
    def __iter__(self):
        return iter(self.range)
 
    def __reversed__(self):
        return iter(reversed(self.range))
 
list(reversed(Plage(0, 5)))  # [4, 3, 2, 1, 0]

19.12 __aiter__, __anext__, __aenter__, __aexit__

class AsyncContext:
    async def __aenter__(self):
        print("Entrée async")
        return self
 
    async def __aexit__(self, *args):
        print("Sortie async")
 
async def main():
    async with AsyncContext() as ctx:
        ...

19.13 Tableau récapitulatif

MéthodeDéclencheurUsage
__new__cls(args)Contrôle de création d’instance
__init__Après __new__Initialisation
__del__GCNettoyage (ne pas compter dessus)
__getattr__Attribut manquantProxy, délégation
__getattribute__Tout accès attributContrôle d’accès
__setattr__obj.x = valValidation
__delattr__del obj.xProtection
__copy__copy.copy(obj)Copie superficielle
__deepcopy__copy.deepcopy(obj)Copie profonde
__instancecheck__isinstance(obj, cls)Logique de type personnalisée
__subclasscheck__issubclass(sub, cls)Héritage virtuel
__set_name__Création de classeNom du descripteur
__class_getitem__Cls[T]Génériques

🔗 ← Retour au cours · ← précédent · Suivant →