Algo & Prog 1 Python : CM séance 04
4. Programmation orientée objet
La Programmation orientée objet (POO) est un paradigme de programmation, qui consiste à manipuler des briques logicielles appelées objets.
Un objet possède à la fois :
- des données internes, appelées attributs ;
- des fonctions pour les manipuler, appelées méthodes.
Le type class
permet de déclarer un objet en Python.
4.1. Déclaration et instanciation
Exemple : on déclare la classe Toto
class Toto :
pass
Notez la majuscule (convention pour dire que c'est une classe définie par le programmeur).
C'est un nouveau type :
>>> type(Toto)
<class 'type'>
On peut ensuite fabriquer un objet de la classe Toto
, mémorisé à l'aide d'une
variable t
:
>>> t = Toto()
>>> type(t)
<class '__main__.Toto'>
>>> isinstance (t, Toto)
True
On dit que l'objet désigné par t
est une instance de la classe (= un exemplaire) ;
en fabriquant t
, on dit qu'on a instancié la classe.
Conventions de nommage
Définies dans la PEP 8 :
- NomDeLaClasse : en CamelCase, première lettre en majuscule
- instances, attributs et méthodes : en snake_case, première lettre minuscule
- CONSTANTES : en TRUMP_CASE, tout en majuscule
4.2. Attributs d'instance
Chaque instance peut mémoriser des attributs, c'est-à-dire des variables internes :
class Toto :
pass
>>> t = Toto()
>>> t.ga = "bu"
>>> t.zo = "meu"
>>> t.ga
'bu'
On dit que l'on a surchargé l'instance avec des attributs.
Ces attributs n'appartiennent qu'à cette instance :
>>> u = Toto()
>>> u.ga
AttributeError: 'Toto' object has no attribute 'ga'
Mais où sont stockés ces attributs ? Dans ce dictionnaire automatique :
>>> t.__dict__
{'ga': 'bu', 'zo': 'meu'}
>>> t.__dict__['zo']
'meu'
>>> for cle, val in t.__dict__.items() :
print("t.", cle, " = ", val, sep="")
t.ga = bu
t.zo = meu
On peut donc rajouter aussi des attributs à t
en faisant
t.__dict__[clé] = valeur
:
>>> t.__dict__['foo'] = 'bar'
>>> t.foo
'bar'
4.3. Méthodes
Une classe peut définir des méthodes, c'est-à-dire des fonctions attachées aux objets de la classe.
Exemple : création d'une méthode saluer
class Hello :
def saluer(self, qui) :
print("Hello", qui, "!")
>>> h = Hello()
>>> h.saluer("World")
Hello World !
Notez le paramètre self
, obligatoire au début de chaque méthode ;
il désigne l'instance courante quand une méthode est appelée.
Qui fournit le paramètre self
lors d'un appel de la méthode ? Quand on écrit
>>> h.saluer("World")
c'est exactement comme si on avait appelé :
Hello.saluer(h, "World")
4.4. Constructeur et destructeur
La méthode __init__
est une méthode spéciale, appelée le constructeur de la
classe (par abus de langage : c'est en fait l'initialisateur).
Elle est appelée automatiquement à chaque instanciation.
Exemple :
class Bingo :
def __init__(self) :
print("bingo !")
>>> b = Bingo()
bingo !
>>> c = Bingo()
bingo !
écrire __init__
avec 2 _
devant et derrière, et non _init_
.
Il existe aussi la méthode __del__
appelée chaque fois qu'une instance est
détruite, on l'appelle le destructeur.
Exemple :
class Bingo :
def __init__(self) :
print("bingo !")
def __del__(self) :
print("instance détruite")
>>> b = Bingo()
bingo !
>>> del b # on détruit explicitement la variable
instance détruite
>>> c = Bingo()
bingo !
>>> c = 1 # on affecte une autre valeur -> l'objet n'est plus
instance détruite # référencé -> il est automatiquement détruit
>>> d = Bingo()
bingo !
>>> e = d # maintenant d et e désignent le même objet
>>> d = 1 # autre valeur -> objet non détruit car encore référencé
>>> e = 1 # autre valeur -> objet détruit car plus référencé
instance détruite
4.5. Initialisation avec paramètres
class Point :
def __init__(self, px, py) :
print("Initialisation d'un point")
self.x = px
self.y = py
>>> p = Point()
TypeError: __init__() missing 2 required positional arguments: 'px' and 'py'
>>> p = Point(3, 5)
Initialisation d'un point
>>> p.x
3
Lorsqu'on appelle p = Point(3,5)
, la méthode __init__
est appelée avec
les paramètres p, 3, 5
(plus exactement, le futur p
, sous le nom self
).
La méthode __init__
surcharge ensuite self
avec les attributs x
et y
,
en y mémorisant les paramètres px
et py
.
Comme la méthode __init__
est appelée automatiquement à chaque instanciation,
ceci garantit que chaque instance de la classe possédera des attributs
x
et y
.
On peut maintenant utiliser ces attributs dans d'autres méthodes :
class Point :
def __init__(self, px, py) :
print("Initialisation d'un point")
self.x = px
self.y = py
def afficher(self) :
print (self.x, self.y)
>>> p = Point(3,5)
Initialisation d'un point
>>> p.afficher()
3 5
En résumé :
La syntaxe générale est :
class NomDeLaClasse :
def __init__ (self, paramètres_création) :
# corps du constructeur :
# mémorisation dans des attributs : self.foo = ...
def __del__ (self) :
# corps du destructeur optionnel
def autre_methode (self, autres_paramètres) :
# corps méthode
# utilisation des attributs : self.foo
Le paramètre self
est passé en premier à chaque méthode, il désigne l'instance
courante (this
en java et en C++).
Instanciation = création d'une instance de la classe :
instance = NomDeLaClasse (paramètres_creation)
Appel d'une méthode :
instance.autre_methode (autres_paramètres)
4.6. Tout est objet
Tout est objet en python (sauf les opérateurs) :
-
les valeurs littérales :
>>> type(1) # <class 'int'> >>> type("") # <class 'str'>
-
les instances :
>>> a = 1 # ou a = int(1) >>> type(a) # <class 'int'> >>> isinstance (a, int) # True
-
les types :
>>> type(int) # <class 'type'> >>> type(str) # <class 'type'>
-
les fonctions :
>>> def f() : pass >>> type(f) # <class 'function'>
-
les exceptions :
>>> type(ValueError) # <class 'type'> >>> type(Exception) # <class 'type'>
Mais on ne peut pas rajouter des attributs à leurs instances : ces classes sont gelées.
>>> a = 1
>>> a.toto = 2
AttributeError: 'int' object has no attribute 'toto'
On peut examiner une classe ou une instance avec dir()
:
>>> dir(Point)
>>> p = Point(3,5)
>>> dir(p) # il y a en plus x, y
Les attributs entourés de doubles _
(comme __init__
) sont réservés, et ont
un rôle spécial. Exemple :
>>> p.__class__.__name__ # 'Point'
Les widgets de TkInter sont des classes :
>>> import tkinter
>>> type(tkinter.Tk) # <class 'type'>
>>> type(tkinter.Canvas) # <class 'type'>
>>> dir(tkinter.Canvas) # méthodes nombreuses !
On peut surcharger les objets TkInter (servira en TP) :
zone_dessin = tkinter.Canvas(...)
zone_dessin.foo = bar
4.7. Attributs de classes
Ce sont des attributs partagés entre toutes les instances.
class Pouce :
cm = 2.54 # attribut de classe
def __init__ (self, x) :
self.x = x
def convertir (self) :
print (self.x, "pouces =", self.x*self.cm, "centimètres")
>>> p = Pouce(3)
>>> p.convertir() # 3 pouces = 7.62 centimètres
>>> q = Pouce(5)
>>> q.convertir() # 5 pouces = 12.7 centimètres
>>> Pouce.cm # 2.54
>>> Pouce.cm = 8
>>> p.cm ; q.cm # 8 8
Attention :
>>> p.cm = 10
>>> p.cm ; q.cm # 10 8
>>> Pouce.cm = 12
>>> p.cm ; q.cm # 10 12
Explication :
>>> Pouce.__dict__ # .... 'cm': 12
>>> p.__dict__ # {'cm': 10, 'x': 3}
>>> q.__dict__ # {'x': 5}
→ affecter p.cm = 10
a surchargé l'instance, ce qui a masqué l'attribut de classe.
>>> del p.cm
>>> p.cm # 12
>>> del Pouce.cm
>>> p.cm # AttributeError
4.8. Représentations
Chaque objet a une valeur str
par défaut :
class Angle :
def __init__(self, degre) :
self.degre = degre
>>> a = Angle(180)
>>> print(a) # équivalent à : print(str(a))
<__main__.Angle object at 0x7f09c1d3e820>
On peut redéfinir la représentation de la classe = le résultat de str()
:
class Angle :
def __init__(self, degre) :
self.degre = degre
def __str__(self) :
return str(self.degre)+'°'
>>> a = Angle(180)
>>> str(a)
'180°'
>>> print(a)
180°
Par contre,
>>> a # équivalent à : repr(a)
<__main__.Angle object at 0x7f09c1d3e820>
>>> repr(a)
<__main__.Angle object at 0x7f09c1d3e820>
On peut également redéfinir la représentation d'une instance par repr()
:
class Angle :
def __init__(self, degre) :
self.degre = degre
def __str__(self) :
return str(self.degre)+'°'
def __repr__(self) :
return "Instance de Angle valant "+str(self)
>>> a = Angle(120)
>>> print(a)
120°
>>> a
Instance de Angle valant 120°
4.9. Surcharge des opérateurs
Dans notre exemple précédent, on ne peut pas encore utiliser les opérateurs de calcul :
>>> a = Angle(180)
>>> b = Angle(30)
>>> a+b
TypeError: unsupported operand type(s) for +: 'Angle' and 'Angle'
Pour que cela soit possible il faut d'abord définir l'opérateur +
.
class Angle :
def __init__(self, degre) :
self.degre = degre
def __str__(self) :
return str(self.degre)+'°'
def __add__(self, other) :
if not isinstance(other, Angle) :
raise TypeError("type opérande non supporté par + : " +\
"Angle et "+str(type(other)))
res = self.degre + other.degre
return Angle(res)
>>> a = Angle(180)
>>> b = Angle(30)
>>> c = a+b
>>> print(c)
210°
Test avec un mauvais type :
>>> d = a+50
TypeError: type opérande non supporté par + : Angle et <class 'int'>
Solution :
class Angle :
...
def __add__(self, other) :
if isinstance(other, Angle) :
res = self.degre + other.degre
elif isinstance(other, (int, float)) :
res = self.degre + other
else :
raise TypeError("type opérande non supporté par + : " +\
"Angle et "+str(type(other)))
return Angle(res)
>>> a = Angle(180)
>>> b = Angle(30)
>>> c = a + b + 15 + 1.234
>>> print(c)
226.234°
Conclusion : toujours prévoir une garde = un test de compatibilité de types lorsqu'on surcharge des opérateurs.
On peut ainsi redéfinir tous les opérateurs de calcul.
Par exemple, on peut redéfinir l'opérateur +=
d'auto-addition
avec __iadd__(self, other)
,
en modifiant puis renvoyant self
, de manière à conserver la même référence :
class Angle :
...
def __iadd__(self, other) :
res = self.__add__(other) # ou encore : res = self + other
self.degre = res.degre
return self
>>> a = Angle(180)
>>> b = Angle(30)
>>> a += b
>>> print(a)
210°
Autres opérateurs de calculs :
+ __add__(self, other) += __iadd__(self, other)
- __sub__(self, other) -= __isub__(self, other)
* __mul__(self, other) *= __imul__(self, other)
** __pow__(self, other) **= __ipow__(self, other)
/ __truediv__(self, other) /= __itruediv__(self, other)
// __floordiv__(self, other) //= __ifloordiv__(self, other)
% __mod__(self, other) %= __imod__(self, other)
Opérateurs unaires de signe : pas besoin de garde
- __neg__(self)
+ __pos__(self)
abs() __abs__(self)
Opérations logiques :
not() __not__(self)
and __and__(self, other)
or __or__(self, other)
Opérateurs de comparaison :
== __eq__(self, other)
!= __ne__(self, other)
< __lt__(self, other)
> __gt__(self, other)
<= __le__(self, other)
>= __ge__(self, other)
Opérateurs de conteneur :
len(instance) __len__(self)
other in instance __contains__(self, other)
instance[clé] ou
instance.get(clé) __getitem__(self, clé)
instance[clé] = valeur ou
instance.set(clé, valeur) __setitem__(self, clé, valeur)
del instance[clé] __delitem__(self, clé)
On peut presque tout surcharger... sauf l'affectation.
Voir : https://docs.python.org/3/reference/datamodel.html#special-method-names