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 l'instance courante 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, sous la forme d'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