Maîtrise de la Métaprogrammation Python : Réflexion, Méthodes Magiques et Singleton

  1. Méthodes liées et décorateurs de classe

En Python, les méthodes définies à l'intérieur d'une classe peuvent être liées soit à l'instance, soit à la classe elle-même. Cette liaison détermine quel objet est implicitement passé comme premier argument lors de l'appel.

  • Méthodes d'instance : Le premier paramètre est conventionnellement nommé self. Losrqu'une instance appelle cette méthode, l'instance elle-même est passée automatiquement.
  • Méthodes de classe : Le premier paramètre est cls. Elles sont appelées par la classe et reçoivent la classe comme argument implicite.

Décorateurs intégrés

Pour modifier le comportement de liaison, Python fournit des décorateurs natifs :

  • @classmethod : Transforme une méthode pour qu'elle reçoive la classe (cls) au lieu de l'instance. Idéal pour les constructeurs alternatifs.
  • @staticmethod : Détache la méthode de la classe et de l'instance. Elle se comporte comme une fonction ordinaire mais réside dans l'espace de noms de la classe.
  • @property : Permet d'accéder à une méthode comme s'il s'agissait d'un attribut de données, sans nécessiter de parenthèses lors de l'appel.

Génération d'identifiants uniques

Le module uuid est couramment utilisé pour générer des identifiants uniques. Contrairement à hashlib qui produit des hachages cryptographiques, uuid4() génère un UUID aléatoire de 128 bits.


import hashlib
import uuid

class UserSession:
    def __init__(self, username, role):
        self.username = username
        self.role = role
        self.session_id = self._generate_session_id()

    def display_info(self):
        print(f"Session active pour {self.username} (Rôle: {self.role})")

    @classmethod
    def create_admin_session(cls):
        """Constructeur alternatif pour créer une session administrateur."""
        return cls(username="admin_root", role="administrator")

    @staticmethod
    def _generate_session_id():
        """Génère un identifiant de session unique et haché."""
        raw_uuid = str(uuid.uuid4())
        hasher = hashlib.sha256(raw_uuid.encode('utf-8'))
        return hasher.hexdigest()

# Utilisation
admin_sess = UserSession.create_admin_session()
admin_sess.display_info()

print(f"ID de session : {admin_sess.session_id}")
print(f"Appel statique direct : {UserSession._generate_session_id()}")

  1. Inspection de types : isinstance et issubclass

Python offre des fonctions intégrées pour vérifier dynamiquement les relations d'héritage et d'instanciation :

  • isinstance(obj, classinfo) : Retourne True si obj est une instance de classinfo ou d'une de ses sous-classes.
  • issubclass(class, classinfo) : Retourne True si class est une sous-classe de classinfo.
  1. Le mécanisme de réflexion

La réflexion permet d'inspecter et de manipuler dynamiquement les attributs et méthodes d'un objet en utilisant des chaînes de caractères. Cela repose sur quatre fonctions intégrées :

  • hasattr(obj, name) : Vérifie si l'attribut name existe dans obj.
  • getattr(obj, name[, default]) : Récupère la valeur de l'attribut. Si l'attribut est une méthode, elle peut être appelée ensuite. Accepte une valeur par défaut si l'attribut est absent.
  • setattr(obj, name, value) : Assigne une valeur à un attribut, ou le crée s'il n'existe pas.
  • delattr(obj, name) : Supprime l'attribut spécifié de l'objet.

class ApiClient:
    def __init__(self, base_url):
        self.base_url = base_url
        self.timeout = 30

client = ApiClient("https://api.example.com")

# Vérification et récupération
if hasattr(client, "timeout"):
    current_timeout = getattr(client, "timeout")
    print(f"Timeout actuel : {current_timeout}")

# Modification dynamique
setattr(client, "retries", 3)
print(f"Nouvel attribut 'retries' : {getattr(client, 'retries')}")

# Suppression
delattr(client, "retries")

  1. Les méthodes magiques (Dunder methods)

Les méthodes magiques, entourées de doubles underscores (__), permettent de définir comment les objets se comportent face aux opérations intégrées de Python.

Cycle de vie et initialisation

  • __new__(cls, ...) : Méthode statique implicite responsable de la création de l'instance. Elle est appelée avant __init__ et doit retourner une instance.
  • __init__(self, ...) : Initialise l'instance créée par __new__.
  • __del__(self) : Appelée juste avant la destruction de l'objet par le ramasse-miettes.

Représentation et conversion

  • __str__(self) : Définit la représentation lisible par un humain (utilisée par print()). Doit retourner une chaîne de caractères.

Gestion dynamique des attributs

  • __getattr__(self, name) : Invoquée uniquement lorsque l'attribut recherché n'est pas trouvé par les mécanismes normaux de recherche.
  • __getattribute__(self, name) : Invoquée pour tout accès à un attribut, qu'il existe ou non. Si elle est définie, __getattr__ n'est appelée que si __getattribute__ lève une AttributeError.
  • __setattr__(self, name, value) : Intercepte toute assignation d'attribut. Pour éviter les boucles infinies, l'assignation interne doit passer par self.__dict__[name] = value ou super().__setattr__.
  • __delattr__(self, name) : Intercepte la suppression d'un attribut via l'instruction del.

Appel d'objet

  • __call__(self, ...) : Permet à une instance d'être appelée comme une fonction (ex: obj()).
  1. Le patron de conception Singleton

Le Singleton garantit qu'une classe n'a qu'une seule instance et fournit un point d'accès global à celle-ci. C'est particulièrement utile pour des ressources partagées comme les connexions à une base de données, les pools de threads ou les configurations globales, permettant ainsi d'optimiser l'utilisation de la mémoire.

Approche 1 : Via une méthode de classe

Cette méthode utilise un attribut de classe privé pour stocker l'instance et une méthode de classe pour contrôler l'instanciation.


class DatabasePool:
    _instance = None

    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.connections = []

    @classmethod
    def get_instance(cls, host="localhost", port=5432):
        if cls._instance is None:
            cls._instance = cls(host, port)
        return cls._instance

# Utilisation
db1 = DatabasePool.get_instance("192.168.1.10", 5432)
db2 = DatabasePool.get_instance("10.0.0.5", 3306) # Les paramètres sont ignorés car l'instance existe déjà

print(db1 is db2)  # True
print(db1.host)    # "192.168.1.10"

Approche 2 : Surcharge de __new__ (Recommandée)

En redéfinissant __new__, on intercepte la création de l'objet au niveau le plus bas. C'est l'approche la plus robuste car elle empêche toute instanciation multiple, même via le constructeur standard.


class AppConfig:
    _instance = None
    _initialized = False

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super(AppConfig, cls).__new__(cls)
        return cls._instance

    def __init__(self, environment="production", debug=False):
        # Empêche la réinitialisation des attributs lors des appels suivants au constructeur
        if not self._initialized:
            self.environment = environment
            self.debug = debug
            self._initialized = True

cfg1 = AppConfig(environment="staging", debug=True)
cfg2 = AppConfig(environment="production", debug=False)

print(cfg1 is cfg2)       # True
print(cfg1.environment)   # "staging" (les paramètres de cfg2 sont ignorés)

Approche 3 : Implémentation par décorateur

Un décorateur peut envelopper la classe pour intercepter les appels au constructeur et gérer le cache de l'instance de manière externe.


def singleton_decorator(cls):
    instances = {}
    
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
        
    return get_instance

@singleton_decorator
class Logger:
    def __init__(self, log_level="INFO"):
        self.log_level = log_level
        self.log_file = "app.log"

    def log(self, message):
        print(f"[{self.log_level}] {message}")

logger1 = Logger("DEBUG")
logger2 = Logger("ERROR")

print(logger1 is logger2)  # True
print(logger1.log_level)   # "DEBUG"

Étiquettes: Python Singleton Reflection MagicMethods metaprogramming

Publié le 6 octobre à 09h42