Programmation objet
Principe
En programmation orientée objet, on cherche à organiser les données avec la volonté de se rapprocher de l’architecture d’objets physiques.
Un objet physique, comme par exemple, une planche de skateboard, possède des caractéristiques propres.
Une planche, malgré la diversité, est toujours constituée:
- d’un deck (court, long)
- d’un tail (plat, relevé)
- de trucks (hauts, bas)
- de roues (à gommes tendres, dures)
- d’une décoration (image sous le deck)
C’est son état.
Cette planche peut repondre egalement à un ensemble de messages, qui forment ce que l’on appelle l'interface de l’objet. C’est au travers de ces méthodes que l’on peut interagir avec l’objet, ou que les objets peuvent interagir entre eux.
On peut lui associer une méthode skateboard_pour_parc
qui renvoie True
si la planche est de type street ou False
si la planche est de type cruising, ou prevue pour la decoration.
Un objet comprend donc :
- une partie figée qui représente son état, et qui est constituée de champs ou attributs.
- une partie dynamique qui décrit son comportement, à l’aide de déclarations de méthodes, qui doivent répondre à des messages.
Classe et instance de classe
La structure interne des objets et les messages auxquels ils repondent sont définis dans une classe. Une classe décrit les méthodes de création d’objets de ce type (on parle d'instance de classe), et les méthodes auxquelles répondront les objets de ce type dans la reception des messages.
Avec l’exemple précédent, la classe Skateboard pourrait contenir:
- les méthodes de création des instances des différents skateboards. On y définit les attributs pour chaque nouvelle planche de skateboard que l’on créé. Chaque planche de skate constitue une instance de classe.
- les méthodes de la classe skateboard, qui repondent aux messages du genre:
skateboard_pour_parc
? Un message est constitué du nom de la méthode, et permet d’interagir avec celui-ci. Ces méthodes sont accessibles à tous les skates qui sont des instances de la classe skateboard.
En python, la création d’une classe commence par le mot clé class suivi du nom de la classe, de deux points et d’une indentation.
class Skateboard:
Toute la partie indentée contient les méthodes de création des instances de cette classe et les méthodes de cette classe.
Lors de la programmation, on devra faire référence à l’instance de l’objet sur lequel on travaille: le mot clé self
permet ceci en python.
La méthode de création d’un objet est définie par def __init__
suivi au minimum de l’argument self
, et eventuellement d’autres arguments.
class Skateboard:
def __init__(self,deck='court',tail='releve',truck='bas',roues='dures',deco='None'):
self.deck = deck
self.tail = tail
self.truck = truck
self.roues = roues
self.deco = deco
Les attributs des objets de cette classe sont définis dans cette méthode. Ils sont codés sous la forme self.attribut
, et peuvent faire référence aux arguments passés lors de l’instanciation.
Enfin, chaque méthode de classe est définie à l’aide de: def nom_de_la_methode
. Celle-ci prend aussi au minimum, l’argument self
.
Script complet:
class Skateboard:
def __init__(self,deck='court',tail='releve',truck='bas',roues='dures',deco='None'):
self.deck = deck
self.tail = tail
self.truck = truck
self.roues = roues
self.deco = deco
def skateboard_pour_parc(self):
if self.deck == 'court' and self.tail == 'releve' and self.roues == 'dures':
return True
else:
return False
On instancie alors un premier skateboard de la manière suivante:
>>> mon_premier_skate = Skateboard()
Et on envoie le message suivant:
>>> mon_premier_skate.skateboard_pour_parc()
True
Instanciation d’un second skateboard:
>>> mon_sector_9 = Skateboard('long','plat','haut','molles','Marley')
>>> mon_sector_9.skateboard_pour_parc()
False
La méthode __repr__()
Cette méthode __repr__
s’utilise pour donner une représentation plus lisible de l’objet lorsqu’on veut l’afficher avec la fonction print
.
Exemple 1: On essaie d’afficher notre instance de classe mon_sector_9
créée plus haut:
>>> print(mon_sector_9)
<__main__.Skateboard object at 0x7fc6b0847550>
Ce n’est pas très explicite pour un humain. On préfèrera souvent choisir un descriptif personnalisé. Nous allons programmer la chaine de caractères qui sera renvoyée lors du print
.
On ajoute maintenant à la classe Skateboard
la méthode __repr__()
qui sera appelée lorsque l’on utilise print(objet)
:
class Skateboard:
def __init__(...
...
def __repr__(self):
if self.skateboard_pour_parc():
return "skateboard de type {} qui permet de rider en parc".format(self.deck)
else:
return "skateboard de type {} qui ne permet pas de rider en parc".format(self.deck)
On construit à nouveau l’instance de classe mon_sector_9
, et on fait print(mon_sector_9)
:
>>> mon_sector_9 = Skateboard('long','plat','haut','molles','Marley')
>>> print(mon_sector_9)
skateboard de type long qui ne permet pas de rider en parc
Remarquer que l’appel à la méthode skateboard_pour_parc
se fait en identifiant l’objet qui possède cette méthode: self.skateboard_pour_parc()
. Et ici, on n’ajoutera pas de nouvel argument. (On ne met pas self
en argument lors du message envoyé à cet objet).
Surcharge d’une fonction de la librairie standard: Ce que l’on vient de réaliser est une surcharge de la fonction print
de la librairie standard.
Mais il existe d’autres méthodes de surcharge, prévues pour les opérateurs, ainsi que pour certaines fonctions. On pourra se référer au lien suivant pour approfondir le sujet.
Encapsulation: getter et setter
Lire un attribut: accesseur ou getter
L’utilisateur du programme ne devrait pas utiliser la méthode pointée précédente nom_objet.nom_attribut
, permettant d’accéder aux valeurs des attributs: on ne veut pas forcement que l’utilisateur ait accès à la représentation interne des classes.
Pour utiliser ou modifier les attributs, on utilisera de préférence des méthodes dédiées dont le rôle est de faire l’interface entre l’utilisateur de l’objet et la représentation interne de l’objet (ses attributs).
Les attributs sont alors en quelque sorte encapsulés dans l’objet, c’est à dire non accessibles directement. la liste des méthodes devient une sorte de mode d’emploi de la classe.
Pour obtenir la valeur d’un attribut nous utiliserons la méthode des accesseurs (ou “getters”) dont le nom est généralement : getNom_attribut()
.
Exemple:
class Skateboard:
def __init__(...
...
def getTail(self):
return self.tail
En console, une fois l’instance de classe mon_sector_9
définie, on fait:
>>> mon_sector_9.getTail()
`long`
Modifier des attributs : les mutateurs ou setters
De la même manière, pour modifier la valeur d’un attribut, on devrait utiliser une méthode dédiée. Le nom de cette méthode commence en principe par set
:
Exemple:
class Skateboard:
def __init__(...
...
def setDeco(self, deco):
self.deco = deco
En console:
>>> mon_sector_9.setDeco = 'repeinte'
Docstrings
Chaque classe définit ses propres manières de stocker, d’acceder et de manipuler les données. C’est le Docstring qui permettra de prendre connaissance des détails, comme par exemple celui de la classe Sept_Familles
:
class Sept_Familles:
"""
NAME
Sept_Familles
DESCRIPTION
constructeur pour les cartes du jeu
PARAMETERS
famille (str): nom de la famille
qui(str): membre de la famille
choix parmi ('Grand-père','Grand-mère','Pére','Mère','Fils','Fille')
FUNCTIONS
get_Attributs
"""
On peut alors prendre connaissance du docstring en faisant:
>>> help(Sept_Familles)
Interface
L’interface d’une classe est représentée par le diagramme suivant:
Exercices
Exercice 1: Jeu de cartes des 7 familles : Les cartes
On définit une classe Sept_Familles
dont le contenu est défini dans le docstring (voir dans la fenêtre de l’editeur).
- Question 1: Compléter la classe
Sept_Familles
avec la méthodeget_Attributs
.
Celle-ci devra retourner un tuple constitué des 2 attributs de classe.
- Question 2: Testez alors votre classe. Définir les objets-cartes suivantes:
carte1 = Sept_Familles('Jongleurs','Grand-père')
carte2 = Sept_Familles('Jongleurs','Fille')
carte3 = Sept_Familles('Musiciens','Père')
carte4 = Sept_Familles('Musiciens','Fille')
Puis tester les méthodes de classe:
>>> carte1.get_Attributs()
-
Ajouter les autres membres de l’une des familles (carte5, carte6,…). Une famille est constituée de Grand-père, Grand-mère, Père, Mère, Fils, Fille.
-
Question 4: Ajouter la méthode
__repr__
qui permettra de décrire la carte:
Exemple de message attendu:
>>> print(carte1)
La carte est de la famille des Jongleurs. C'est le Grand-père.
Testez votre fonction pour quelques unes des cartes.
Exercice 2: Jeu de cartes des 7 familles: Les joueurs
Le joueur est un objet qui possède, pour attributs, les cartes qu’il a en main.
Chaque joueur est une instance de la classe Joueur
, et sera construit avec joueur1 = Joueur(carte1,...)
Difficulté à venir: On ne connait pas le nombre de cartes que le joueur peut avoir en main à l’instant t.
Python permet d’appeler une fonction avec un nombre inconnu d’arguments: il faut ajouter le paramètre *args
dans la déclaration de la fonction. On parcourt alors les arguments de la manière suivante:
for i in args:
...
La fonction __init__
aura alors pour arguments self
(qui est obligatoire), et *args
. On y definit une liste vide, self.C
, et on y ajoute des tuples constitués des attributs pour chacune des cartes du joueur. Cette fonction est donnée dans l’editeur, plus bas…
-
Question 1: Ecrire une méthode de classe
get_cartes
qui retourne la listeself.C
, afin de pouvoir accéder à la liste des cartes du joueur. -
Question 2: Créer l’objet
joueur1
. Celui-ci devra posséder les cartescarte1
etcarte2
définies plus haut.
Tester alors la méthode : joueur1.get_cartes()
pour afficher la liste.
- Question 3: Créer une méthode de classe
__repr__
pour afficher cette liste dans un message, comme par exemple:
>>> print(joueur1)
le joueur possède les cartes : [('Jongleurs', 'Grand-père'), ('Jongleurs', 'Fille')]
- Question 4: On souhaite maintenant avoir une interaction plus poussée avec le Joueur, selon les règles du jeu. L’idée est de créer une méthode
demande
qui prend en paramètre un tupple (famille
,qui
), et qui retourne un message selon la main du joueur, comme par exemple:
>>> joueur1.demande(('Jongleurs','Fils'))
pioche
>>> joueur1.demande(('Jongleurs','Fille'))
Le joueur a la carte
>>> joueur1.demande(carte1.getAttributs())
Le joueur a la carte
-
Question 5: Quelles fonctionnalités du jeu reste t-il à programmer pour avoir un jeu complet?
-
Question 6: (mini projet) Programmer l’une de ces fonctionnalités.
Correction des exercices: Lien
La suite en TP
- TP sur trajectoires de projectiles: Lien
- TP sur la programmation d’un jeu de Dominos: Lien. (Sans interface graphique)
Liens
- cours sur python.developpez.com: compléments sur les méthodes spéciales de surcharge des opérateurs, d'héritage et polymorphisme
- références : Classes sur docs.python.org
- prolongement sur les heritages et polymorphismes python doctor POO - debutants
- Resumé de cours : Lyceum.fr > POO
- Autres exercices : Lyceum.fr > POO > Exercices