Métaprogrammation en Python
L'une des techniques les plus importantes du développement logiciel est la suivante: « Ne vous répétez pas le code .» C'est-à-dire que, chaque fois que vous rencontrez un problème de création de code hautement répétitif (ou de couper ou coller du code source), il est souvent avantageux de rechercher un fichier. solution plus élégante.
En Python, ces problèmes sont souvent résolus sous la catégorie «métaprogrammation». En bref, la métaprogrammation consiste à créer des fonctions et des classes dont le but principal est de manipuler du code (par exemple, modifier, générer ou encapsuler du code existant). Les principales caractéristiques de ceci incluent les décorateurs, les décorateurs de classe et les métaclasses.
Décorateurs
Métaclasses
En Python, tout est associé à un type. Par exemple, si nous avons une variable ayant une valeur entière, son type est int.
Vous pouvez obtenir le type de n'importe quoi en utilisant la fonction type().
num = 23 print("Type de num est :", type(num)) liste = [1, 2, 4] print("Type de liste est :", type(liste)) nom = "ESSADDOUKI" print("Type de nom est :", type(nom))
class="brush: python" Type de num est : < class 'int' > Type de liste est : < class 'list' > Type de nom est : < class 'str' >
Chaque type en Python est défini par une classe. Ainsi, dans l'exemple ci-dessus, contrairement à C ou Java où int, char, float sont des types de données primaires, en Python, ils sont des objets de classe int ou de classe str.
Nous pouvons donc créer un nouveau type en créant une classe de ce type.
Par exemple, nous pouvons créer un nouveau type Personne en créant une classe Personne.
class Personne: pass p=Personne() print("Type de p est :", type(p))
Type de p est : < class '__main__.Personne'>
Une classe est également un objet et, comme tout objet, elle est une instance de quelque chose appelé Metaclass. Un type de classe spécial crée ces objets de classe. La classe type est la métaclasse par défaut qui est responsable de la création des classes.
Par exemple, dans l'exemple ci-dessus, si nous essayons de déterminer le type de classe Personne, il s'agit d'un type.
class Personne: pass print("Type de Personne est :", type(Personne))
Type de Personne est : < class 'type' >
Les classes étant également un objet, elles peuvent être modifiées de la même manière. Nous pouvons ajouter ou soustraire des champs ou des méthodes en classe de la même manière que nous le faisions avec d'autres objets.
Par exemple
class Personne: pass Personne.nom = "ESSADDOUKI" Personne.affiche = lambda self: print(self.nom) p = Personne() p.affiche()
ESSADDOUKI
Toute cette procédure peut être résumée comme suit : Métaclasse crée des classes et les classes créent des objets
Une métaclasse est responsable de la génération des classes. Nous pouvons donc écrire nos propres métaclasses personnalisées pour modifier la façon dont les classes sont générées en effectuant des actions supplémentaires ou en injectant du code. Généralement, nous n’avons pas besoin de métaclasses personnalisées, mais cela est parfois nécessaire.
Pour créer notre métaclasse personnalisée, notre métaclasse personnalisée doit hériter du métaclasse type et généralement réimplementer les méthodes suivantes :
- __new __ (): c’est une méthode appelée avant __init __ (). Il crée l'objet et le retourne. Nous pouvons remplacer cette méthode pour contrôler la manière dont les objets sont créés.
- __init __ (): cette méthode initialise simplement l'objet créé passé en paramètre
Nous pouvons créer des classes en utilisant directement la fonction type(). Il peut être appelé de différentes manières:
- Appelé avec un seul argument, il retourne le type.
- Appelé avec trois paramètres, il crée une classe. Les arguments suivants lui sont transmis :
- Nom de la classe
- Tuple ayant des classes de base héritées par la classe
- Dictionnaire de classe: Il sert d’espace de nom local pour la classe, contient les méthodes de classe et les attributs.
def TEST(self): print("je suis : ", self.__class__.__name__) class Personne: def Showme(self): print("je suis une personne") Test = type('Meta', (Personne, ), dict( nom="ESSADDOUKI", prenom="Mostafa", affiche=TEST)) # Afficher le type de TEST print("Type de TEST est : ", type(Test)) # Créer une instance de la classe TEST p = Test() print("Type de p: ", type(p)) p.affiche() p.Showme() print("Mon nom est ", p.nom)
Type de TEST est : < class 'type'> Type de p: < class '__main__.Meta'> je suis : Meta je suis une personne Mon nom est ESSADDOUKI
Créons maintenant une métaclasse sans utiliser directement type(). Dans l'exemple suivant, nous allons créer une métaclasse HeritageMultiple qui vérifiera si la classe en cours de création a hérité de plusieurs classes de base. Si c'est le cas, une erreur sera générée.
class HeritageMultipe(type): #cls : type classe # clsname : nom de la classe # bases : classes de base #clsdict : liste des attributs et méthodes def __new__(cls, clsname, bases, clsdict): # si il hérite de plusieurs classes déclencher une erreur if len(bases) > 1: raise TypeError("Héritage multiple est non autorisé!!!") # sinon exécuter __new__ de la classe mère (type) return super().__new__(cls, clsname, bases, clsdict) # définir une métaclass en utilisant le mot metaclass # maintenant la classe HeritageMultiple est utilisée pour # la création des classes class Base(metaclass=HeritageMultipe): pass class A(Base): pass class B(Base): pass # Cela déclenchera une erreur, parce que elle hérite de deux classes A et B class C(A, B): pass
Traceback (most recent call last): File "meta.py", line 34, in class C(A, B): File "meta.py", line 10, in __new__ raise TypeError("Héritage multiple est non autorisé!!!") TypeError: Héritage multiple est non autorisé!!!
Quand utiliser les métaclasses
La plupart du temps, nous n'utilisons pas de métaclasse, elles sont comme de la magie noire et servent généralement à quelque chose de compliqué, mais peu de cas où nous utilisons des métaclasses sont
- Comme nous l'avons vu dans l'exemple ci-dessus, les métaclasses se propagent dans les hiérarchies d'héritage. Cela affectera également toutes les sous-classes. Si nous avons une telle situation, alors nous devrions utiliser des métaclasses.
- Si nous voulons changer de classe automatiquement quand il est créé
- Si vous êtes développeur d'APIs, vous pouvez utiliser des métaclasses.
Si vous définissez une classe, vous l'appelez comme une fonction pour créer des instances. Si vous souhaitez personnaliser cette étape, vous pouvez le faire en définissant une métaclasse et en réimplémentant sa méthode __call __().
Pour illustrer cela, supposons que vous ne vouliez pas que quiconque crée des instances
class Restriction(type): def __call__(self, *args, **kwargs): raise TypeError("Ne peut pas instancier directement") # Exemple class TEST(metaclass=Restriction): pass obj = TEST()
Traceback (most recent call last): File "meta.py", line 12, in < module> obj = TEST() File "meta.py", line 3, in __call__ raise TypeError("Ne peut pas instancier directement") TypeError: Ne peut pas instancier directement
L'utilisation d'une métaclasse pour implémenter divers modèles de création d'instances peut souvent constituer une approche beaucoup plus élégante que d'autres solutions n'impliquant pas de métaclasses.
Vous souhaitez enregistrer automatiquement l'ordre dans lequel les attributs et les méthodes sont définis dans un corps de classe afin de pouvoir l'utiliser dans diverses opérations (par exemple, la sérialisation, la mise en correspondance avec des bases de données, etc.).
La capture d'informations sur le corps de la définition de classe est facilement réalisée grâce à l'utilisation d'une métaclasse. Voici un exemple de métaclasse qui utilise OrderedDict pour capturer l'ordre de définition des descripteurs:
from collections import OrderedDict class Typed: _expected_type = type(None) def __init__(self, name=None): self._name = name # redéfinir la mathode d'affectation __set__ # permet de comparer la valeur de l'attribut et le type d'attribut def __set__(self, instance, value): if not isinstance(value, self._expected_type): raise TypeError('type attendu : ' + str(self._expected_type)) instance.__dict__[self._name] = value # définir les types attendus class Integer(Typed): _expected_type = int class Float(Typed): _expected_type = float class String(Typed): _expected_type = str class OrderedMeta(type): def __new__(cls, clsname, bases, clsdict): # créer un dictionnaire à partir des attributs de la classe d = dict(clsdict) # liste contient l'ordre des attribut ordre = [] for name, value in clsdict.items(): # si l'attribut est de type attendu # ajouter dans ordre if isinstance(value, Typed): value._name = name ordre.append(name) # ajouter l'ordre dans le dictionnaire des attributs de classe d['_order'] = ordre # exécuter __new__ de la classe mère (type) return type.__new__(cls, clsname, bases, d)
Dans cette métaclasse, l'ordre de définition des descripteurs est capturé à l'aide d'un ordre commandé lors de l'exécution du corps de la classe. L'ordre des noms résultant est ensuite extrait du dictionnaire et stocké dans un attribut de classe _order. Ceci peut ensuite être utilisé par les méthodes de la classe de différentes manières.
voici une classe simple Etudiant pour bien illustrer l'utilisation de la métaclasse OrderedMeta
class Etudiant(metaclass=OrderedMeta): nom = String() prenom = String() age = Integer() note = Float() def __init__(self, nom, prenom, age, note): self.nom = nom self.prenom = prenom self.age = age self.note = note s = Etudiant('ESSADDOUKI', 'Mostafa', 31, 13.5) s = Etudiant('Kayouh', "mohamed", "30", 14.0)
Traceback (most recent call last): File "meta.py", line 69, in < module> s = Etudiant('Kayouh', "mohamed", "30", 14.0) File "meta.py", line 64, in __init__ self.age = age File "meta.py", line 14, in __set__ raise TypeError('type attendu : ' + str(self._expected_type)) TypeError: type attendu : < class 'int'>