Prérequis : La fonction exec
La fonction exec exécute une chaîne de caractères contenant du code Python. Elle prend jusqu'à trois arguments :
- Une chaîne représentant le code à exécuter.
- (Optionnel) Un dictionnaire représentant l'espace de noms global. Par défaut, c'est
globals(). - (Optionnel) Un dictionnaire représentant l'espace de noms local. Par défaut, c'est
locals().
Tout nom créé durant l'exécution de la chaîne de code est stocké dans l'espace de noms local spécifié.
Démonstration de l'impact sur les espaces de noms
g = {'x': 10, 'y': 20} l = {}
exec(""" global x, z x = 100 z = 200 local_var = 300 """, g, l)
print(g) # {'x': 100, 'y': 20, 'z': 200, ...} print(l) # {'local_var': 300}
</div>### La méthode spéciale `__call__`
Cette méthode est invoquée lorsqu'une instance d'une classe est appelée comme une fonction, c'est-à-dire avec des parenthèses `()`. Elle est différente de la méthode `__init__`, qui est appelée lors de la *création* de l'instance (`mon_objet = MaClasse()`).
<div>```
class Gestionnaire:
def __init__(self, nom):
self.nom = nom
def __call__(self, message):
return f"[{self.nom}] Traitement du message : {message}"
gestion = Gestionnaire("Alpha")
# Création de l'instance (__init__)
resultat = gestion("Données entrantes")
# Appel de l'instance (__call__)
print(resultat) # [Alpha] Traitement du message : Données entrantes
En Python, tout est un objet, y compris les classes elles-mêmes. Lorsque l'interpréteur exécute une instruction class, il crée un objet qui représente cette classe. En conséquence, un objet de type type.
class Vehicule: roues = 4
ma_voiture = Vehicule() # 'ma_voiture' est une instance de Vehicule print(type(ma_voiture)) # <class 'main.Vehicule'>
'Vehicule' est lui-même un objet !
print(type(Vehicule)) # <class 'type'>
</div>Puisqu'une classe est un objet, elle peut être manipulée comme n'importe quel autre objet en Python :
- Assignée à une variable.
- Passée en argument de fonction.
- Retournée par une fonction.
- Créée dynamiquement à l'exécution.
<div>```
# Exemple de manipulation dynamique
def obtenir_classe(nom):
if nom == "voiture":
class Voiture:
pass
return Voiture
else:
class Camion:
pass
return Camion
ClasseChoisie = obtenir_classe("voiture")
instance = ClasseChoisie()
print(type(instance)) # <class '__main__.obtenir_classe.<locals>.Voiture'>
La fonction type() a une double vie. D'une part, elle renvoie le type d'un objet. D'autre part, elle peut être utilisée pour créer dynamiquement de nouveaux types (classes). C'est le constructeur de classe intégré de Python.
Sa syntaxe pour la création de classe est :
type(nom, bases, dictionnaire_attributs)
nom: Une chaîne représentant le nom de la classe.bases: Un tuple contenant les classes parentes (pour l'héritage).dictionnaire_attributs: Un dictionnaire contenant les attributs et méthodes de la classe.
Création dynamique d'une classe
ClasseDynamique = type('Dynamique', (object,), {'valeur': 42, 'afficher': lambda self: print(self.valeur)})
obj = ClasseDynamique() obj.afficher() # 42 print(type(ClasseDynamique)) # <class 'type'>
</div>Qu'est-ce qu'une Métaclassse ?
------------------------------
Une métaclassse est la classe d'une classe. Si une classe est un moule pour créer des instances (objets), une métaclassse est le moule pour créer des classes. La classe `type` est la métaclassse intégrée par défaut de Python.
<div>```
print(type(int)) # <class 'type'>
print(type(str)) # <class 'type'>
print(type(object))# <class 'type'>
# Notre classe Dynamique créée plus haut est une instance de 'type'
print(type(ClasseDynamique)) # <class 'type'>
L'attribut __metaclass__ (et la syntaxe Python 3)
En Python 2, on pouvait définir une métaclassse pour une classe en ajoutant un attribut __metaclass__. En Python 3, la syntaxe standard passe par un argument de mot-clé metaclass dans la définition de la classe.
Syntaxe Python 3
class MaClasse(metaclass=MaMétaclassse): pass
</div>Lors de la définition d'une classe, Python suit cette logique :
1. La classe a-t-elle un attribut `metaclass` ? Si oui, l'utiliser pour créer l'objet-classe.
2. Sinon, chercher dans les classes parentes une métaclassse héritée.
3. Si aucune n'est trouvée, utiliser `type` par défaut.
Création Personnalisée d'une Classe (Processus Détaillé)
--------------------------------------------------------
Pour comprendre ce qui se passe "sous le capot", nous pouvons reproduire manuellement la création d'une classe.
<div>```
# 1. Définir les composants
nom_classe = 'Client'
classes_parentes = (object,)
corps_de_classe = """
nom_entreprise = "ACME Inc."
def __init__(self, identifiant):
self.id = identifiant
def presenter(self):
print(f"Client #{self.id} de {self.nom_entreprise}")
"""
# 2. Traiter le corps : obtenir un espace de noms
espace_noms = {}
exec(corps_de_classe, {}, espace_noms)
# 3. Créer la classe en appelant le constructeur type()
Client = type(nom_classe, classes_parentes, espace_noms)
c = Client(123)
c.presenter() # Client #123 de ACME Inc.
Une métaclassse personnalisée doit généralement implémenter une méthode __new__ ou __init__ pour intercepter la création de la classe.
Exemple 1 : Validation à la Création de la Classe
Cette métaclassse vérifie que le nom de la classe commence par une majuscule et qu'une docstring est présente.
class ValidationMeta(type): def new(mcs, nom, bases, espace_noms): # Vérification 1 : Le nom de la classe doit commencer par une majuscule if not nom[0].isupper(): raise TypeError(f"Le nom de la classe '{nom}' doit commencer par une majuscule.")
# Vérification 2 : Une docstring doit être présente
docstring = espace_noms.get('__doc__')
if not docstring or not docstring.strip():
raise TypeError(f"La classe '{nom}' doit avoir une docstring non vide.")
# Appel au constructeur parent pour finaliser la création
return super().__new__(mcs, nom, bases, espace_noms)
Utilisation
class CompteUtilisateur(metaclass=ValidationMeta): """Classe représentant un compte utilisateur.""" def init(self, nom_utilisateur): self.utilisateur = nom_utilisateur
Ceci lèverait une TypeError :
class mauvais_nom(metaclass=ValidationMeta):
pass
</div>### Exemple 2 : Contrôle de l'Instanciation (Singleton via Métaclassse)
En sucrhargeant la méthode `__call__` de la métaclassse, on peut contrôler comment ses instances (qui sont des classes) sont appelées pour créer des objets.
<div>```
import threading
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
# Double vérification pour le thread-safety
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class ConnexionDB(metaclass=SingletonMeta):
"""Gestionnaire de connexion à la base de données (Singleton)."""
def __init__(self):
self.connecte = True
print("Initialisation de la connexion unique...")
# Les deux appels renvoient la même instance
conn1 = ConnexionDB() # Affiche le message d'initialisation
conn2 = ConnexionDB() # N'affiche rien, renvoie conn1
print(conn1 is conn2) # True
Quand nous appelons une_classe() pour créer une instance, c'est la méthode __call__ de sa métaclassse qui est exécutée. Cette méthode typiquement :
- Crée une nouvelle instance de la classe en appelant
__new__. - Initialise cette instance en appelant
__init__. - Retourne l'instance initialisée.
Notre SingletonMeta ci-dessus modifie ce processus pour ne pas recréer une instance si elle existe déjà.
Pourquoi et Quand Utiliser les Métaclassses ?
Les métaclassses sont un mécanisme puissant mais complexe. Elles sont principalement utiles pour :
- Créer des API expressives et déclaratives (comme l'ORM de Django).
- Appliquer des règles ou des validations à l'échelle de toutes les classes d'un framework.
- Instrumenter ou modifier automatiquement les classes (par exemple, enregistrement, injection de méthodes).
Conseil d'utilisation : Dans 99% des cas, un décorateur de classe (@decorator) ou un simple héritage suffit. Réservez les métaclassses aux cas où vous avez besoin de contrôler la création même des classes.
Exercices Pratiques
Exercice 1 : Uniformisation des Attributs
Créez une métaclassse qui transforme tous les attributs de données (non-appelables, ne commençant pas par __) d'une classe en lettres majuscules.
class MetaUniformisation(type): def new(mcs, nom, bases, espace_noms): esp_noms_modifie = {} for cle, valeur in espace_noms.items(): if not callable(valeur) and not cle.startswith('__'): esp_noms_modifie[cle.upper()] = valeur else: esp_noms_modifie[cle] = valeur return super().new(mcs, nom, bases, esp_noms_modifie)
class Profil(metaclass=MetaUniformisation): nom_complet = "Jean Dupont" age = 30 def saluer(self): print("Bonjour !")
print(Profil.NOM_COMPLET) # "Jean Dupont" print(Profil.AGE) # 30 p = Profil() p.saluer() # "Bonjour !"
</div>### Exercice 2 : Constructeur Automatique
Créez une métaclassse qui permet de créer des instances *sans* définir de méthode `__init__`. Les arguments passés lors de l'instanciation doivent être stockés comme des attributs (en majuscules) de l'objet. Les arguments doivent être nommés.
<div>```
class MetaConstructeur(type):
def __call__(cls, **kwargs):
# kwargs contient les attributs nommés
if not kwargs:
raise TypeError("Les attributs doivent être passés sous forme de mots-clés.")
instance = object.__new__(cls)
# Initialisation automatique : stockage en majuscules
for cle, valeur in kwargs.items():
setattr(instance, cle.upper(), valeur)
return instance
class Article(metaclass=MetaConstructeur):
pass
art = Article(titre="Python Avancé", prix=29.99)
print(art.TITRE) # "Python Avancé"
print(art.PRIX) # 29.99