Programmation Orientée Objet

La programmation orientée objet (POO) est un paradigme 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.

Écrire une fonction distance(P1, P2)  admettant 2 objets Points  comme arguments et renvoyant la valeur de la distance entre ces deux points.

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 ), il en existe une permettant d’utiliser le symbole « + » pour faire une somme de deux objets de même type : la  méthode __add__().

Écrire la méthode __add__(self, v)  , acceptant comme argument le vecteur lui-même (self ) et un autre vecteur v , et en renvoyant le vecteur somme.

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)

Faire de même avec les opérateurs suivants :
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 )
Implémenter deux méthodes prod_scal(self, u)  et prod_vect(self, u)  réalisant les produits scalaires et vectoriels.

Droite

Il existe plusieurs manières de définir une droite : Point+Vecteur, deux Points, …

Créer une classe Droite() , dont le constructeur peut admettre différents types d’arguments (pourvu que ceux-ci puissent permettre la définition univoque de l’objet).

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 »} )

 

Améliorer la fonction distance()  pour qu’elle admette également un Point  et une Droite  comme arguments, et en calcule la distance relative !
Plus dur : étendre les possibilités de la fonction distance()  au calcul de la distance entre deux droites…

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
   ...

Continuer l’implémentation de la classe Carte_de_Belote .

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.

Implémenter les classes définissant des cartes de jeu de Belote et de Manille en respectant le diagramme ci-dessous.

 

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.

 

Tester et analyser le programme fourni. Identifier les attributs et les méthodes communs à la classe  Forme_geometrique , ou au contraire spécifiques à ses sous-classes (choisir parmi la liste ci-dessous et compléter le diagramme de classes ci-dessous).

 

Le réécrire en utilisant de la programmation orientée objet, et en respectant le diagramme de classe.
Ajouter une classe permettant de gérer de la même manière des polygones réguliers

 

Sources :

Vous aimerez aussi...

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *