TP8 Programmation orientée objet - FR

publicité
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)
Téléchargement