programmation objet

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.

chalk custom board

image: CHALK CUSTOM BOARD : L’ART D’HABILLER UN SKATE (urban art)

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()

skateboard classic

un premier skateboard bien classique

Et on envoie le message suivant:

>>> mon_premier_skate.skateboard_pour_parc()
True

Instanciation d’un second skateboard:

skateboard cruiser

un skateboard cruiser

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

Une représentation d'une classe

Une représentation d'une classe

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éthode get_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 liste self.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 cartes carte1 et carte2 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