Programmation Orientée Objet
La programmation orientée objet (POO) est un
de programmation informatique. Il consiste en la définition et l’interaction de briques logicielles appelées objets ; un objet représente un concept, une idée ou toute entité du monde physique,.Exemples : une voiture, une personne, une page d’un livre…
L’objet possède en interne une structure et un comportement, et il sait interagir avec ses pairs.
Il s’agit donc de représenter ces objets et leurs relations ; l’interaction entre les objets via leurs relations permet de concevoir et réaliser les fonctionnalités attendues, de mieux résoudre des problèmes. Dès lors, l’étape de modélisation revêt une importance majeure et nécessaire pour la POO. C’est elle qui permet de transcrire les éléments du réel sous forme virtuelle.
source : Wikipédia
L’objet et la classe
Un objet est un élément d’une classe (en quelque sorte un « type personnalisé » ; il faut entendre type au même titre qu’un nombre, une chaîne de caractères, …).
Définir une classe revient à définir une nouvelle structure de données, qui s’ajoute à celles définies par le langage.
Définition de classe
Avant de créer un objet, il faut avoir défini sa classe.
Une classe possède un nom et des membres, c’est à dire des attributs (ou champs) et des méthodes.
Chaque membre nommé membre d’un objet objet est accessible via l’expression : objet.membre.
- Les attributs (ou champs) : ils sont à l’objet ce que les variables sont à un programme.
les attributs sont donc typés (exemple int, float, bool, str, … ou n’importe quel autre classe !)
- Les méthodes sont des procédures ou fonctions destinées à traiter les données. Elles servent d’interface entre les données et le programme.
les méthodes acceptent donc des arguments et peuvent renvoyer des valeurs
Le fait de réunir dans un même objet les attributs et les méthodes se nomme l’encapsulation. Cela permet de cacher à l’utilisateur d’un objet certaines données ou procédures internes.
Remarque : en Python, tout est objet, même les modules. Par exemple le module math est un objet comprenant l’attribut pi , la méthode sqrt() , …
En Python, on définit une classe grâce à l’instruction class :
class Nom_de_la_classe: <bloc de définition de la classe>
Le bloc de définition d’une classe contient :
- des attributs de classe (voir plus bas …)
- des méthodes spéciales (leurs noms, réservés par le langage Python commence et finit par « __ » : deux tirets « bas »)
- des méthodes personnalisées
Instanciation d’objet
Une fois la classe définie, on peut créer autant d’objets de cette classe (on parle d’instance) que l’on veut :
mon_1er_objet = Nom_de_la_classe() mon_2eme_objet = Nom_de_la_classe() ...
L’opération qui permet de créer l’objet s’appelle la construction.
A chaque construction d’un objet, la méthode __init__() de la classe est exécutée.
Cette méthode permet en générale de définir les différents attributs de l’objet :
class Nom_de_la_classe: def __init__(self, attr1, attr2 = 0): self.attribut1 = attr1 self.attribut2 = attr2
… et la construction de l’objet permet de leur attribuer des valeurs :
mon_1er_objet = Nom_de_la_classe("Salut") print(mon_1er_objet.attribut1) # affiche 'Salut' print(mon_1er_objet.attribut2) # affiche 0
Attributs d’objet – Attributs de classe
Chaque objet peut contenir ses propres valeurs d’attributs :
Mais on peut également attribuer des attributs à la classe elle-même, de sorte que tous les objets de cette classe puissent les utiliser.
class Nom_de_la_classe: attribut_0 = 100 # attribut de la classe def __init__(self, attr1, attr2 = 0): self.attribut1 = attr1 # attribut de l'objet self.attribut2 = attr2 + Nom_de_la_classe.attribut_0
Accès à un attribut d’une classe :
print(Nom_de_la_classe.attribut_0) # affiche 100
Accès à un attribut d’un objet :
mon_1er_objet = Nom_de_la_classe("Bonjour") print(mon_1er_objet.attribut1) # affiche 'Bonjour' print(mon_1er_objet.attribut2) # affiche 100
Un peu de géométrie dans l’espace
Le point
Soit \(M\) un point de l’espace et de coordonnées cartésiennes \(x_M\), \(y_M\) et \(z_M\) dans un repère \(\mathrm{R}\left(O, \vec{x}, \vec{y}, \vec{z}\right)\).
On peut considérer le point comme un objet géométrique, possédant 3 attributs : ses coordonnées dans \(\mathrm{R}[latex].
Définition de classe Python :
class Point: def __init__(self, x, y, z): # méthode "constructeur" self.x = x # self.y = y # attributs: coordonnées self.z = z #
Remarque : self désigne l’objet lui même. On utilise ce terme dans tout ce qui est défini à l’intérieur de la classe (arguments et méthodes) pour éviter les ambiguïtés lorsque l’on s’intéresse aux membres de l’objet (self.x = « c’est mon x »).
>>> M = Point(1, 3, -2) >>> M <__main__.Point object at 0x004D4910>
Si on souhaite que la fonction print() affiche des informations plus explicites sur le point, on peut déclarer, à l’intérieur de la déclaration de classe, la méthode __repr__() (il s’agit d’un nom réservé, d’où les deux « _ ») :
def __repr__(self): # méthode pour l'affichage return "P(%.4f, %.4f, %.4f)" %(self.x, self.y, self.z)
Ce qui aura pour conséquence le comportement suivant :
>>> M P(1, 3, -2)
Remarque : toutes les méthodes ou attributs commençant par « __ » (deux tirets-bas) sont privés, c’est à dire qu’ils ne peuvent être utilisés que depuis l’intérieur même de l’objet.
On peut créer ses propres membres privés (en plus des noms déja réservés) pour mieux contrôler la modification des données de l’objet.
Le vecteur
De même que pour le point, on peut définir un vecteur par ses 3 coordonnées dans le repère $R$.
Définition de classe Python :
class Vecteur: def __init__(self, x, y, z): # méthode "constructeur" self.x = x # self.y = y # attributs : coordonnées self.z = z #
Parmi les noms de méthodes spéciales (elles sont toutes là), il en existe une permettant d’utiliser le symbole « + » pour faire une somme de deux objets de même type : la méthode __add__().
Le résultat doit permettre de faire ça (après avoir implémenté une méthode __repr__() comme pour le point) :
>>> u = Vecteur(1, 5, 2) ; v = Vecteur(1, 4, -1) >>> u + v v(2, 9, 1)
nom de la méthode | expression Python | écriture mathématique | |||
différence : | __sub__() | : | u – v | ⇔ | [latex]\vec{u}-\vec{v}\) |
norme : | __abs__() | : | abs(u) | ⇔ | \(\|\vec{u}\|\) |
négation : | __neg__() | : | -u | ⇔ | \(-\vec{u}\) |
produit par un scalaire : | __mul__() | : | k*u | ⇔ | \(k\times\vec{u}\) |
(dans ce dernier cas, les arguments étant de types différents, il faut aussi définir la fonction « réfléchie » __rmul__() , pour pouvoir faire k*u et u*k ) |
Droite
Il existe plusieurs manières de définir une droite : Point+Vecteur, deux Points, …
Pour tester si un objet est une instance d’une classe donnée, on peut utiliser la fonction isinstance(objet, nom_de_la_classe) , qui renvoie alors True ou False .
Pour passer à une fonction des arguments de nombre ou de type non prédéterminés, on peut utiliser l’opérateur « * » :
- l’argument *args dans la définition de la fonction va récupérer tous les arguments donnés lors de l’appel de la fonction dans un tuple nommé args .
(exemple : def mafct(*args) → mafct(« a », 2) → args = (« a », 2) )
- l’argument **kargs dans la définition de la fonction va récupérer tous les arguments donnés lors de l’appel de la fonction dans un dictionnaire nommé kargs .
(exemple : def mafct(**kargs) → mafct(k= »z », p=3) → kargs = {‘k’: « z », ‘p’: 3} )
- et on peut combiner les deux :
(exemple : def mafct(*args, **kargs) → mafct(« a », p= »3″)
→ args = (« a »,) et kargs = {p : « 3 »} )
Héritage, surcharge, polymorphisme
Nous souhaitons modéliser des cartes à jouer (d’un jeu de 32 cartes) et nous intéressons à leur rang.
class Carte: def __init__(self, couleur, valeur): """ Carte à jouer couleur = "Pique", "Carreau", "Coeur", "Trefle" valeur = '1', 'R', 'D', 'V', '10', '9', '8', '7' """ self.couleur = couleur self.valeur = valeur def obtenir_rang(self): """ Renvoie le rang de la carte dans le jeu """ return ['1', 'R', 'D', 'V', '10', '9', '8', '7'].index(self.valeur)
>>> carte = Carte("Pique", "V") # Valet de pique >>> carte <__main__.Carte instance at 0x02031620> >>> carte.obtenir_rang() 3
Remarque : le texte situé juste après la déclaration d’une classe ou d’une méthode constitue un message d’aide que l’on peut obtenir avec la fonction help() . L’ensemble des messages d’aides au sein d’une classe constitue une sorte de mode d’emploi de la classe, évitant à l’utilisateur d’aller chercher des informations dans le code.
>>> help(Carte) class Carte | Methods defined here: | | __init__(self, couleur, valeur) | Carte à jouer | couleur = "Pique", "Carreau", "Coeur", "Trefle" | valeur = '1', 'R', 'D', 'V', '10', '9', '8', '7' | | obtenir_rang(self) | Renvoie le rang de la carte dans le jeu
La valeur donnée ici par la fonction obtenir_rang() est le rang par défaut du jeu de carte Français, et n’est pas valable pour d’autres règles de jeu comme la Belote, la Manille, …
Pour différencier les cartes selon la règle du jeu auquel elles servent, nous pouvons créer d’autres classes. Mais pour conserver les membres de la classe Carte , ces nouvelles classes devront hériter de cette dernière.
Pour représenter cet ensemble nous pouvons utiliser un diagramme de classe (langage UML).
En Python, pour indiquer qu’une classe hérite d’une autre, il suffit de l’indiquer dans sa déclaration :
class Carte_de_Belote(Carte): # Carte_de_Belote hérite de Carte ...
Il faut bien entendu obtenir :
>>> vingtDePique = Carte_de_Belote("Pique", "V", atout = True) >>> vingtDePique.obtenir_rang() 0
Nous remarquons que les deux sous-classes Carte_de_Belote et Carte de manille possèdent comme leur classe parente des méthodes obtenir_rang() . La valeur renvoyée est pourtant la bonne, la méthode exécutée est comme nous le souhaitions celle de la sous classe Carte_de_Belote . Cette fonctionnalité s’appelle la surcharge (overloading) de fonction. On parle également de polymorphisme.
Cela permet de remplacer la méthode de la classe « mère » par une méthode spécifique à la classe « fille ». Ou bien de la compléter, car la méthode de la classe « mère » est encore appelable. La méthode suivante, déclarée dans la classe Carte_de_Belote , appelle la méthode de la classe Carte :
def obtenir_rang(self): return Carte.obtenir_rang(self)
Héritage multiple
Une classe peut hériter de plusieurs sous-classes, lui permettant de disposer des membres de chacune de ses classes « mères » .
class NouvelleClasse(ClasseMere1, ClasseMere2, ClasseMereN): ...
Remarque : en cas d’ambigüité sur les noms de membre (si plusieurs classes « mère » possèdent des membres de même nom), ce sont les premiers déclarés qui seront retenus.
Programmation orientée objet vs. Programmation procédurale
Le programme Python geoometrie.py est écrit en programmation procédurale.
[reveal heading= »%image% Cliquer pour voir/cacher le code de geoometrie.py »]#!/usr/bin/env python # -*- coding: utf-8 -*- """ Ce programme permet : - d'affichier des figures géométriques (de tailles et de couleurs différentes) - de modifier leur position en les faisant glisser - de modifier leur couleur en cliquant dessus Mais il n'est pas orienté objet ! """ import matplotlib.pyplot as plt from matplotlib.patches import Rectangle import matplotlib.patches as mpatches from random import random, randint from matplotlib import colors import six # Une liste de couleurs couleurs = [c[1] for c in list(six.iteritems(colors.cnames))] def couleur_aleatoire(): return couleurs[randint(0, len(couleurs)-1)] def dimension_aleatoire(maxi = 0.2): return max(0.1, maxi*random()) # Une figure, un système d'axes fig, ax = plt.subplots() ax.set_title('Faire glisser les formes ... ou changer leur couleur en cliquant') # Des cercles for i in range(5): ax.add_artist(mpatches.Circle((random(),random()), dimension_aleatoire(), color = couleur_aleatoire(), ec = "none", picker = True)) # Des rectangles for i in range(5): ax.add_artist(mpatches.Rectangle((random(),random()), dimension_aleatoire(0.4), dimension_aleatoire(0.4), color = couleur_aleatoire(), ec = "none", picker = True)) # Variables globales dx, dy = 0.0, 0.0 # Coordonnées relatives ar = None # artiste (forme géométrique) mvt = False # Pour indiquer qu'un mouvement a commencé # Ce qui se produit quand une forme est sélectionnée def quandChoix(event): global dx, dy, ar ar = event.artist if isinstance(ar, mpatches.Circle): xdata, ydata = ar.center elif isinstance(ar, mpatches.Rectangle): xdata, ydata = ar.xy dx, dy = event.mouseevent.xdata - xdata, event.mouseevent.ydata-ydata # Ce qui se produit quand la souris se déplace def quandMouvement(event): global mvt if event.inaxes is None: # Le mouvement est en dehors du système d'axes return if event.button != 1: # Le bouton de la souris n'est pas appuyé return if ar == None: # Pas de forme sélectionnée return mvt = True # Un mouvement a commencé x, y = event.xdata, event.ydata # Les coordonnées de la souris if isinstance(ar, mpatches.Circle): ar.center = (x-dx, y-dy) elif isinstance(ar, mpatches.Rectangle): ar.set_x(x-dx) ar.set_y(y-dy) fig.canvas.draw() # Ce qui se produit quand le bouton de la souris est relaché def quandRelache(event): global ar, mvt if not mvt and ar is not None: ar.set_color(couleur_aleatoire()) fig.canvas.draw() ar = None mvt = False # Connection du canvas avec des événements fig.canvas.mpl_connect('motion_notify_event', quandMouvement) fig.canvas.mpl_connect('pick_event', quandChoix) fig.canvas.mpl_connect('button_release_event', quandRelache) plt.show()[/reveal]
La programmation procédurale est constituée de procédures et fonctions sans liens particuliers agissant sur des données dissociées, et cela peut mener rapidement à des difficultés en cas de modification de la structure des données. Dans le cas de geoometrie.py ce type de programmation impose de différencier les types de donnée (isinstance()) car les données des objets Circle et Rectangle ne sont pas structurées de la même manière.
Sources :