Programmation Orientée Objet

Objets et POO sont au centre de la manière Python fonctionne. Vous n'êtes pas obligé d'utiliser la POO dans vos programmes - mais comprendre le concept est essentiel pour devenir plus qu'un débutant. Entre autres raisons parce que vous aurez besoin d'utiliser les classes et objets fournis par la librairie standard.

De plus, avant d'aborder la programmation d'interfaces graphiques qui utilisent abondamment les objet, des notions autour de la POO seront utiles.

Petit historique

La programmation en tant que telle est une matière relativement récente. Etonnament la programmation orientée objet remonte aussi loin que les années 1960. Simula est considéré comme le premier langage de programmation orienté objet.

Les années 1970 voient les principes de la programmation par objet se développent et prennent forme au travers notamment du langage Smalltalk

À partir des années 1980, commence l'effervescence des langages à objets : Objective C (début des années 1980, utilisé sur les plateformes Mac et iOS), C++ (C with classes) en 1983 sont les plus célèbres.

Les années 1990 voient l'âge d'or de l'extension de la programmation par objet dans les différents secteurs du développement logiciel, notemment grâce à l'émergence des systèmes d'exploitation basés sur une interface graphique (MacOS, Linux, Windows) qui font appel abondamment aux principes de la POO.

Nous verrons sur le prochain classeur comment une interface graphique peut se programmer au moyens d'objets (fenêtre, boutons, textes, champs de saisie etc...).

Programmation procédurale

La programmation procédurale est celle que vous avez utilisé jusqu'à maintenant : cela consiste à diviser votre programme en blocs réutilisables appelés fonctions.

Vous essayez autant que possible de garder votre code en blocs modulaires, en décidant de manière logique quel bloc est appelé. Cela demande moins d’effort pour visualiser ce que votre programme fait. Cela rend plus facile la maintenance de votre code – vous pouvez voir ce que fait une portion de code. Le fait d’améliorer une fonction (qui est réutilisée) peut améliorer la performance à plusieurs endroits dans votre programme.

Vous avez des variables, qui contiennent vos données, et des fonctions. Vous passez vos variables à vos fonctions – qui agissent sur elles et peut-être les modifient. L'inteaction entre les variables et les fonctions n'est pas toujours simple à gérer comme on l'a vu dans le classeur précédent ! ou bien une variable est locale et n'est pas visible des autres fonction, ou bien une variable est globale et toutes les fonctions sont suceptibles d'y avoir accès.

On touche ici aux limites de la programmation procédurale, lorsque le nombre de fonctions et de variables devient important.

Mais qu’est ce qu’un Objet ?

En Python les éléments de base de la programmation que nous avons rencontré comme les chaînes de caractères, ou les listes sont des objets. Ils possèdent des propriétés - variables qui stockent des valeurs - et des méthodes - fonctions qui agissent sur ces valeurs.

Voici un petit exemple d'objet qui vous est déjà familier :

In [ ]:
liste=[3,5,4,2,8,5,4]
In [ ]:
liste.sort()
liste

Ici nous avons fait appel à la méthode sort() de l'objet liste afin de trier notre liste.

Mais dans nos projets futurs, nous pouvons avoir envie de définir nos propres objets, c'est à dire d'enrichir la bibliothèque de types built-in standard de Python avec des objets que nous façonnerons selon nos besoins. C'est la qu'intervient la notion de classe.

Création d'une classe

En premier exemple, supposons que nous voulions travailler sur un logiciel de géométrie. Nous avons besoin d'un objet point qui est un nouveau type d'objet contenant deux informations :

  • l'abscisse de notre point
  • l'ordonnée de notre point.

Ces deux informations sont ce que nous appelons en POO des attributs ou des propriétés.

Assez de discours, créons notre classe :

In [ ]:
class Point():
    abscisse=0
    ordonnee=0

Et c'est tout !!! nous avons créé une classe contenant deux propriétés une abscisse et une ordonnée toutes deux initialisées à 0.

Comment ça marche ?

In [ ]:
print (Point.abscisse)
print (Point.ordonnee)
Point.abscisse=2
print (Point.abscisse)

Ca a l'air trè simple ! En réalité, nous allons vite être limité si nous n'utilisons que cette classe. En effet, nous créé un objet classe Point qui contient deux informations. Mais dans notre logiciel de géométrie, nous voulons créer plusieurs points !!

C'est le moment de parler de la notion d'instance. Une instance est un objet que nous créons en mémoire à partir d'une classe. Voici comment :

In [ ]:
p1=Point()
p2=Point()
p1.abscisse=2
p2.ordonnee=3
print (p1.abscisse,p1.ordonnee)
print (p2.abscisse,p2.ordonnee)

Nous y voila ! J'ai donc à présent la possibilité de créer autant de points que je veux. Il faut bien distinguer la notion de classe et la notion d'instance :

  • une classe peut être vue comme le prototype permettant de créer nos instances
  • les instances sont les véritables objets que nos manipulerons, créés à partir de notre prototype.

Pour bien comprendre ce phénomène, prenons une comparaison avec le monde des contructeurs automobile : Lorsqu'un contructeur va sortir une nouvelle voiture, il ne va pas immédiatement produire en série plusieurs millions de véhicules. Il va tout d'abord élaborer un prototype :

  • d'abord sur papier, il va dessiner sa nouvelle voiture, les formes, les équipements, chaque pièce de sa voiture etc...
  • ensuite il va réaliser une maquette, la tester en soufflerie pour affiner sa forme
  • enfin, il va réaliser un modèle fonctionnelle qu'il testera sur route : c'est le prototype.

C'est ce travail que nous réaliserons lorsque nous construirons notre classe. Construire une classe c'est construire un prototype unique.

Une fois notre prototype terminé, notre constructeur va passer à la production en série. Il va créer des millions d'instances de notre prototype qui sont les voitures créées en série à partir de notre prototype. Chaque instance pourra être personnalisé à partir de notre prototype : en effet chaque nouvelle voiture possèdera sa propre couleur qui n'est pas forcément celle de notre prototype, possèdera des options spécifique (gps, toit ouvrant etc...).

Retenez donc cette comparaison :

  • la classe correspond à notre prototype
  • l'instance correspond à la voiture produite en série à partie de notre protptype (la classe).

Notion de méthode

Nous avons créé notre objet point qui se caractérise par deux propriétés : abscisse et ordonnée. Mais si ce n'était que cela, pourquoi ne pas utiliser un tuple ! Nous allons donc enrichir notre classe (le prototype servant de modèle pour créer nos points) en y ajoutant des fonctions uniques : les méthodes.

Nous nous intéressons par exemple à la distance séparant notre point de l'origine du repère. Nous souhaiterions que notre objet point possède une méthode pour nous renvoyer cette information. Une méthode n'est autre qu'une fonction intégrée à un objet. Voici comment procéder. Nous allons modifier notre classe :

In [ ]:
from math import sqrt # On a besoin de la racine carrée !

class Point():
    abscisse=0
    ordonnee=0
    
    def distanceAZero (self):
        return sqrt (self.abscisse**2+self.ordonnee**2)

Regardons le résultat :

In [ ]:
p1=Point()
p1.abscisse=3
p1.ordonnee=4
print(p1.distanceAZero())

Et voila ! notre objet point commence à prendre tournure : il possède

  • deux propriétés : abscisse et ordonnee
  • une méthode : distanceAZero()

Cette méthode est une fonction encapsulée dans notre objet qui agit sur ses propriétés et effecture le travail demandé. Revenons sur la déclaration de cette méthode :

Une méthode se déclare comme une c=fonction classique à l'intérieur de la classe à ceci près qu'elle prend toujours en premier argument l'instance sur laquelle elle agit. Par convention, nous nommons cette instance self.

Nous voyons sur l'exemple de la distanceAZero l'avantage de disposer de cette information d'instance : nous voulons que la méthode agisse sur l'instance depuis laquelle elle a été appelée et non sur les propriétés de la classe (le prototype). La variable self nous permettra de connaître l'instance sur laquelle nous travaillons.

Reste à décrire la syntaxe un peu étrange de cette fonction : Si self est le premier argument, pourquoi ne le trouve t-on pas lors de l'appel de la fonction distanceAZero() ? Voici l'explication.

En réalité, nous devrions passer l'appel à la méthode de cette manière :

In [ ]:
print(Point.distanceAZero(p1))

Ainsi nous voyons bien que distanceAZero accepte bien l'instance sur laquelle elle agit en premier paramètre et que c'est une fonciton intégrée à la classe Point. Néanmoins cette syntaxe est très lourde ! Imaginez taper la ligne suivante à la place de

liste.append(5)
In [ ]:
list.append(liste,'autre syntaxe')
# Et pourtant cela foncitonne !
print(liste)

Dans la pratique, une méthode sera toujours appelée depuis une instance et le premier paramètre sera omis puisque il est donnée justement par l'instance qui appelle. Syntaxiquement, les deux formes

Point.distanceAZero(p1)

et

p1.distanceAZero()

sont équivalentes. Nous utiliserons systématiquement la seconde forme.

A vous de jouer

Vous allez enrichir la classe Point en ajoutant

  • une propriété nom contenant le nom du point (par défaut 'A')
  • une méthode distance(p) qui
    • affichera untexte du type "La distance AB=5" avec bien sur les vrais noms des points et la vraie distance
    • retournera la distance du point au point p passé en argument.

Attention, je rappelle qu'une méthode prend toujours en premier argument self.

In [ ]:
from math import sqrt # On a besoin de la racine carrée !

# Redéfinissez votre classe

Pour tester votre classe, validez la cellule suivante. La réponse doit être :

La distance  AB = 5.0
Out[...]:5.0
In [ ]:
p1=Point()
p1.abscisse=2
p1.ordonnee=3

p2.abscisse=-1
p2.ordonnee=7
p2.nom='B'

p1.distance(p2)

Surcharge des opérateurs

On peut améliorer un peu le comportement de notre classe en initialisant de manière plus propre les différentes propriétés. En effet, pour le moment, pour créer un point avec le bon nom et les coordonnées souhaitées, nous avons besoin de 4 lignes ! p1=Point() p1.abscisse=2 p1.ordonnee=3 p1.nom='P'

On peut faire beaucoup mieux en surchargeant la méthode init() qui est une méthode spéciale appelée automatiquement lors de la création d'ue instance. Cette méthode prend

  • en premier paramètre self bien évidemment!
  • en paramètres optionnels, des paramètres passés à la classe lors de la création.
In [ ]:
class Point():
    def __init__(self,x,y,nom):
        self.abscisse=x
        self.ordonnee=y
        self.nom=nom
    
    def distanceAZero (self):
        return sqrt (self.abscisse**2+self.ordonnee**2)
    
    def distance(self, p):
        d=sqrt((self.abscisse-p.abscisse)**2+(self.ordonnee-p.ordonnee)**2)
        print ("La distance ",self.nom+p.nom,"=",d)
        return d

Regardons comment créer notre point :

In [ ]:
p1=Point(2,3,'A')
p2=Point(-1,7,'B')
p1.distance(p2)

C'est quand même bien mieux ! Mais tout n'est pas parfait. Observez ce qui se passe si je veux afficher les coordonnées d'un point. Je peux avoir envie de faire cela :

In [ ]:
print(p1)
# beark

A vous de jouer

Il existe une autre méthode magique - en réalité, il y en a environs 80 - permettant de redéfinir le comportement des opérateurs intégrés à Python. Vous allez créer une méthode nommée str() qui

  • ne prendra pas d'argument autre que self bien sur
  • retournera une chaîne de caractère du type "A(2;3)"

Pour construire votre chaîne, vous pourrez utiliser la concaténation de chaines de caractères au moyen de l'opérateur +. Regardez l'exemple :

In [ ]:
x,y=2,3
chaine="A"+str(x)+"; etc..."
# etc... vous voyez le principe
print(chaine)
In [ ]:
# A vous de jouer
In [ ]:
# Et voila la magie qui s'opère !

p1=Point(2,3,'A')
print(p1)

Et voila, bien venue dans le monde merveilleux des objets.

En seconde partie, nous allons prendre un exemple plus sofistiqué sur les polynomes pour approfondir les notions que nous avons introduites dans ce classeur.

A bientôt !

In [ ]: