IGI-3008 – ESIEE Paris – 2016-2017 Programmation avec Python TP8 Jean-Claude GEORGES Programmation orientée objet en Python Lors de ce TP, nous allons définir complètement une classe Intervalle permettant de modéliser des intervalles mathématiques fermés de réels strictement positifs et de les manipuler. À la fin du TP, nous pourrons écrire des programmes du type : >>> i1 = Intervalle(10.0, 12.0) >>> i2 = Intervalle(9.0, 11.0) >>> print(i1 & i2) # intersection <10.0, 11.0> >>> print(i1 + i2) # addition <19.0, 23.0> >>> if 20 in i1+i2: print("Possible") Définition d’une classe vide Une classe peut être vue en première approche comme un type de données. Un programmeur peut créer ses propres classes et les ajouter aux classes préexistantes de Python (int list, etc.) Le programme de la figue 1 crée une classe (vide). On peut instancier un élément de la classe, vérifier qu’il existe et même l’afficher. Sans autre précision, l’affichage donne le nom de la classe et l’adresse (en hexadécimal) de l’objet. Figure 1 — Une classe vide class MaClasse(): # définition d'une classe "vide" pass a = MaClasse() # création d'un nouvel objet a print(type(a)) # quel est le type de a ? print(a) # qu'est ce que a ? qui affichera : <class ' __main__.MaClasse'> <__main__.MaClasse object at 0x0279A4F0> DÉFINITION DE LA CLASSE INTERVALLE 2 Définition de la classe Intervalle Nous allons maintenant commencer l’écriture de la classe Intervalle. Pour cela, nous nommons la classe avec un nom parlant (une convention respectée fait commencer les noms de classe par une majuscule), et à l’intérieur de la définition, nous créons une méthode magique de nom spécial __init__ qui permet d’initialiser les objets de cette classe. Un objet de la classe Intervalle contiendra deux données (attributs) : une borne inférieure et une borne supérieure. La méthode __init__ aura donc trois paramètres : le premier étant l’objet que l’on initialise (traditionnellement nommé self) et deux paramètres qui permettent d’initialiser les bornes. L’accès aux données d’un objet se fait par la notation pointée : nomObjet.nomAttribut Exercice 1 — Création de la classe Intervalle Entrez le programme suivant : qu’affichera-t-il ? class Intervalle: def __init__(self, a, b): self.borne_inf = a self.borne_sup = b a = Intervalle(2,3) print(a) print(a.borne_inf) print(a.borne_sup) TP8 IGI-3008 (2016-2017) Les méthodes magiques Les méthodes magiques de Python sont des méthodes aux noms spéciaux permettant aux classes utilisateur d’avoir le même comportement et la même facilité d’utilisation que les classes prédéfinies. Par exemple, si une classe MaClassedéfinit une méthode __getitem__(), et si x est une instance de cette classe, alors l’utilisation de x[i] dans une expression fera appel à MaClasse.__getitem__(x, i) I http://docs.python.org/3.4/reference/datamodel.html# special-method-names I http://www.rafekettler.com/magicmethods.pdf PRÉPARATION EN VUE DE L’AFFICHAGE 3 Préparation en vue de l’affichage Une méthode spéciale de nom (__str__) est utilisée par la fonction str (et par print) pour donner une représentation utilisateur des objets sous forme de chaîne. Une autre méthode spéciale de nom (__repr__) est utilisée par repr et par Python pour une représentation "canonique" des objets sous forme de chaîne. La plupart du temps, on aura : eval(repr(unObjet)) == unObjet Comment fonctionne la fonction str de Python ? Lorsque on invoque la fonction str sur un objet (str(a)), Python recherche l’objet de nom a et regarde dans sa classe si une méthode __str__(self) est définie. Si oui, il l’appelle et retourne son résultat. Sinon, il regarde dans sa classe parente, et remonte jusqu’à la classe mère de toutes les classes : la classe prédéfinie object. Une méthode __str__ est définie dans la classe object : class object: # c'est pas le vrai, c'est juste pour expliquer #.... def __str__(self): """ str par défaut d'un objet sous la forme : '<__main__.Classe object at 0x02E29D50>' Exercice 2 — Affichage d’un Intervalle """ s = '<' s += self.__module__ s += +'.' s += type(self).__name__ s += " object at " s += "0x0"+hex(id(self)).upper()[2:] s += '>' Entrez le programme suivant : qu’affichera-t-il ? class Intervalle: def __init__(self, a, b): self.borne_inf = a #... self.borne_sup = b def __str__(self): return '<' + str(self.borne_inf)+ ', ' + str(self.borne_sup) + '>' def __repr__(self): C’est elle qui est utilisée si la aucune méthode __str__ n’est trouvée avant. return 'Intervalle(' + str(self)[1: -1] + ')' a = Intervalle(2,3) print(a) a TP8 Comment Python recherche-t-il les données ? Lorsque un identificateur apparaît dans une expression, Python cherche dans plusieurs dictionnaires de noms s’il est connu : d’abord dans l’espace local (la fonction), puis l’espace parent, etc., jusqu’au niveau principal du programme (__main__), puis aux noms prédéfinis. Dès qu’il trouve un nom, c’est la donnée référée par ce nom qui est utilisée. Les déclarations nonlocal et global devant un identificateur permettent de modifier ce comportement par défaut. IGI-3008 (2016-2017) SÉCURISATION MINIMALE 4 Sécurisation minimale Les quatre exercices suivants ont pour objectif de sécuriser la classe Intervalle. Il s’agit de ne pas avoir de donnée invalide dans un programme utilisant cette classe. Pour cela, nous mettrons en œuvre la règle minimale : si l’on détecte une donnée fallacieuse, on arrête le programme. Évidemment, comme nous le verrons plus tard, Python permet de gérer de manière plus élégante et moins brutale les erreurs. Exercice 3 — Quels problèmes de sécurité ? Avec la classe Intervalle définie comme ci-dessus, entrez les lignes suivantes dans le shell lancé : >>> print(Intervalle(0.25, 0.3)) >>> print(Intervalle(0.35, 0.1)) >>> print(Intervalle("abc", [1,2,3])) Que donneront les affichages ? Exercice 4 — Mettre les bornes dans l’ordre Le second intervalle pose un problème : les bornes ne sont pas dans l’ordre. Cela peut être corrigé par programme à l’initialisation (méthode __init__) : il suffit de stocker dans borne_in f le plus petit de a et b et dans borne_sup le plus grand (utilisez min et max). Modifiez la méthode __init__ de la classe Intervalle pour qu’elle crée automatiquement un intervalle dans le bon ordre. TP8 IGI-3008 (2016-2017) SÉCURISATION MINIMALE 5 La création d’un intervalle avec une initialisation non numérique pose un problème plus compliqué : on peut le corriger automatiquement s’il est possible de les traduire en float, mais sinon on ne peut rien faire. La sécurisation minimale consiste à arrêter le programme avec un message d’erreur. Python permet d’essayer (try) des instruction et en cas d’erreur d’exécuter un gestionnaire d’erreurs. Ici, nous allons simplement arrêter le programme avec un message signalant l’erreur. C’est la sécurité minimale. Il ne faudrait jamais qu’un programme continue son exécution avec des données frelatées. class Intervalle: def __init__(self, a, b): try: a = float(a) b = float(b) except: import sys sys.exit("Problème d'init intervalle. On arrête tout") self.borne_inf = min(a, b) self.borne_sup = max(a, b) def __str__(self): return '<'+ str(self.borne_inf)+', '+str(self.borne_sup)+'>' def __repr__(self): return 'Intervalle(' + str(self)[1:-1] + ')' print(Intervalle("0.35", 0.1)) # ça passe print(Intervalle("0.35", "a0")) # ça casse print("On ne verra pas ce message (pgm s'arrêté avant)") TP8 IGI-3008 (2016-2017) SÉCURISATION MINIMALE 6 Exercice 5 — Forcer des float convenables dans les bornes En vous inspirant de l’arrêt ci-dessus, modifiez le programme pour qu’il s’arrête également si l’une des données est négative ou nulle. Vous pouvez lancer une exception avec l’instruction raise Exception() qui interrompra le programme. L’accès direct à des données d’une instance de classe peut être dangereux. En effet, avec la modification dans l’exercice précédent de la méthode __init__, on ne peut plus créer d’intervalle mal formé. Toutefois, il est toujours possible à un programmeur d’écrire directement a.borne_sup = -2, ce qui mettra −2 dans la borne supérieure de l’intervalle a. On peut remédier à cet inconvénient en ne permettant d’accéder aux données que par des méthodes qui peuvent inclure des tests de protection. Ainsi, dans la méthode __init__, on peut cacher les noms des attributs borne_inf et borne_sup en les préfixant par deux tirets bas : __borne_inf et __borne_sup. Les données __borne_inf et __borne_sup ne seront visibles que depuis les méthodes de la classe, mais pas de l’extérieur. Par exemple, la définition de la classe Intervalle devient : import sys class Intervalle: def __init__(self, a, b): >>> class Essai: def __init__(self): self.__sensible = 99 self.modifiable = 1 def __str__(self): return str(self.__sensible)+' '+str(self.modifiable) __repr__=__str__ >>> a = Essai() >>> print(a.__sensible) # elle n'est pas là Traceback (most recent call last): File "<pyshell#48>", line 1, in <module> print(a.__sensible) AttributeError: 'Essai' object has no attribute '__sensible' >>> print(a.modifiable) # elle, oui 1 >>> a # pourtant elle est là 99 1 >>> print(a._Essai__sensible) # mais bien cachée 0 try: a = float(a) b = float(b) if a <= 0 or b <= 0: raise Exception() except: sys.exit("Pb init Intervalle") self.__borne_inf = min(a, b) self.__borne_sup = max(a, b) TP8 Pourquoi deux tirets bas devant un nom d’attribut ? Préfixer par deux tirets bas un nom d’attribut permet de diminuer le risque que, par mégarde, un programmeur écrive une instruction qui modifie une donnée sans qu’elle ne soit vérifiée. Deux tirets bas indiqueront à Python que la donnée est sensible : dans la définition de classe, Python transformera son nom en lui préfixant le nom de la classe, empêchant ainsi d’y accéder trop facilement. IGI-3008 (2016-2017) SÉCURISATION MINIMALE 7 def __str__(self): return '<'+ str(self.__borne_inf)+', '+str(self.__borne_sup)+'>' def __repr__(self): return 'Intervalle(' + str(self)[1:-1] + ')' a = Intervalle(2.0, 3.0) print(a.__borne_inf) #il est inaccessible de l'extérieur Pour modifier une valeur de l’intervalle, on peut écrire maintenant dans la classe Intervalle une méthode sécurisée : class Intervalle: # # les lignes déjà écrites... # def modif_sup(self, x): try: x = float(x) except: raise Exception() if x < self.borne_inf: raise Exception() self.__borne_sup = x # # les lignes déjà écrites... # qui permettra de protéger la borne supérieure en ne pouvant y écrire que des nombres supérieurs à la borne inférieure. TP8 IGI-3008 (2016-2017) OPÉRATIONS SUR LES INTERVALLES 8 Exercice 6 — Sécuriser la modification des bornes En vous inspirant de la méthode ci-dessus, ajoutez une méthode modif_inf à la classe Intervalle. Faites attention à ce qu’une valeur négative ne puisse pas être enregistrée. Écrivez également deux méthodes d’accès lire_inf(self) et un lire_sup(self) qui retourneront les valeurs des bornes. Opérations sur les intervalles Appartenance : x in I La méthode magique __contains__ permettant de tester si un objet en contient un autre. Exercice 7 — Appartenance Dans la classe Intervalle, écrivez la méthode __contains__(self, x) qui retournera True si x est dans l’intervalle, et False sinon. Intersection : I1 & I2 La méthode magique __and__ permet d’utiliser l’opérateur & entre deux objets. Le programmeur malin décide de programmer l’intersection de deux intervalles en utilisant cet opérateur et donc en écrivant cette méthode. Il remarque (à tort) que la borne inférieure de l’intersection est la plus grande des deux bornes inférieures et que la borne supérieure de l’intersection est la plus petite des deux bornes supérieures. TP8 IGI-3008 (2016-2017) OPÉRATIONS SUR LES INTERVALLES 9 Il écrit donc à toute allure : class Intervalle: # # les lignes déjà écrites... # def __and__(self, autre): if type(autre) != type(self): raise Exception() return Intervalle ( max(self.lire_inf(), autre.lire_inf()), min(self.lire_sup(), autre.lire_sup()) ) # # les lignes déjà écrites... # et la teste : >>> print(Intervalle(1,3) & Intervalle(2,4)) <2.0, 3.0> Ça marche ! ! ! Exercice 8 — Intersection Modérez l’enthousiasme du programmeur de la méthode ci-dessus et donnez un exemple pour lequel cela ne marche pas. Modifiez la méthode pour qu’elle retourne None dans ce cas. TP8 IGI-3008 (2016-2017) OPÉRATIONS SUR LES INTERVALLES 10 Exercice 9 — Union Union : I1 | I2 La méthode magique __or__ permet d’utiliser l’opérateur | entre deux objets. Sur le même modèle que ci-dessus, écrivez la méthode __or__(self, autre) qui retourne l’intervalle réunion des deux intervalles et None si leur intersection est vide. Exercice 10 — Autres opérations Sur le même principe, écrivez les méthodes • __add__(self, autre) associée à + qui retourne un nouvel Intervalle somme des deux intervalles <2, 5> + <3, 4> → <5, 9> • __mul__(self, autre) associée à * qui retourne un nouvel Intervalle produit des deux intervalles <2, 5> * <3, 4> → <6, 20> • inverse(self) qui retourne un nouvel Intervalle inverse de l’argument inverse(<2, 5>) → <0.2, 0.5> • __truediv__(self, autre) associée à / qui retourne un nouvel Intervalle quotient des deux intervalles <3, 4> / <2, 5> TP8 → <0.6, 2> IGI-3008 (2016-2017) UTILISATION D’UNE CLASSE 11 Utilisation d’une classe Exercice 11 — Résistances en parallèles Soient deux résistances R1 (1200 Ω à ±0, 1%) et R2(1800 Ω à ±0, 1%) placées en séries. Exprimez les valeurs des résistances sous formes d’intervalles. Calculez la valeur de la résistance équivalente des deux résistances en parallèles, d’abord 1 R × R2 avec la formule Req = , puis avec la formule Req = 1 1 1 R1 + R2 + R1 R2 TP8 IGI-3008 (2016-2017)