Classes

La programmation orientée objet est une méthode de programmation dérivant de l’approche « type abstrait » présentée plus haut. Nous en présenterons ici deux notions : l’encapsulation et la généricité. En nous arrêtons là, nous pourrions donner l’impression que l’approche « objet » n’est qu’une variation syntaxique de l’approche « type abstrait ». En fait, elle comporte d’autres notions beaucoup plus élaborées (héritage, surcharge, polymorphisme...) que nous n’aborderons pas ici mais qui marquent plus nettement la différence entre les deux approches.

Cette différence ressort aussi dans la terminologie utilisée en programmation objet, qui se distingue de celle de la programmation « classique » : les types abstraits y sont appelés classes. Les valeurs dont le type est une classe sont des objets, et sont des instance de la classe en question. Les procédures et fonctions définissant les opérations disponibles pour une classe sont les méthodes de cette classe.

Instances et encapsulation

Le principe d’encapsulation reprend en fait les principes de l’approche « type abstrait » : le programmeur utilisant un objet ne devrait pas avoir à connaître sa structure interne, mais seulement la spécification de son comportement, définit par ses méthodes. Le terme « encapsulation » rend bien cette idée que l’objet est « renferme » (comme une capsule) son implémentation.

La programmation orientée objet pousse ce principe jusqu’à proposer une syntaxe particulière pour l’utilisation des objets. Puisque les méthodes font partie intégrante de l’objet, elles ne sont plus utilisées comme des fonctions où des procédures autonomes : on utilise la syntaxe pointée (comme pour les champs d’une structure) pour accéder aux méthodes, comme l’illustre le tableau suivant :

Syntaxe classique Syntaxe objet
v = file_vide(f) v = f.file_vide()
ajoute_en_queue(f,e) f.ajoute_en_queue(e)
retire_tête(f) f.retire_tête()

Il est intéressant de noter que, dans cette nouvelle syntaxe, le paramètre représentant l’instance (f dans nos exemples) est sorti des parenthèses. Cela ne signifie pas qu’il n’est plus passé en paramètre : il est implicitement passé en paramètre du fait de la syntaxe pointée. On verra dans la section suivante comment manipuler ce paramètre implicite dans l’implémentation des méthodes.

Les instances de classes contiennent également des données, à l’instar des champs des types structurés. Le principe d’encapsulation (déjà vu à propos des types abstraits) interdit normalement d’utiliser ces données en dehors des méthodes de la classe, c’est pourquoi on les nommes des attributs privés de l’objet. Dans certains langages, ces attributs sont techniquement inaccessibles en dehors des méthodes. En Python, on se contente de marquer ces attributs comme privés en commençant leur nom par un blanc souligné (_) ; ainsi les programmeurs savent qu’ils ne doivent pas les utiliser.

Déclaration d’une classe

La déclaration d’une classe commence par la ligne suivante :

  • le mot-clé class,
  • le nom de la classe [1],
  • le caractère deux points (:).
[1]Par convention, le nom de la classe commencera toujours par une majuscule, et on ne le préfixe en général pas par « T » (comme c’est l’habitude pour les types structurés).

Le reste de la déclaration est indenté par rapport à la première ligne, et comprend la définition de toutes les méthodes.

Interface

Lorsqu’on ne souaite décrire que l’interface d’une classe, on se contentera d’y donner la première ligne de ses méthodes, avec leur spécification en documentation.

Note

En Python, l’interface et l’implémentation sont en général mélées dans le même fichier. Dans ce cours, on présentera cependant toujours l’interface seule de la classe, avant de présenter son implémentation. La première est en effet tout ce qu’on a besoin de connaître pour écrire un programme utilisant la classe, tandis que la deuxième est nécessaire à l’ordinateur pour l’exécution de ce programme.

La déclaration d’une méthode est similaire à la déclaration d’une procédure ou fonction, à l’exception du paramètre implicite représentant l’objet dont on appelle la méthode. Ce paramètre est conventionnellement nommé self, et est toujours le premier paramètre de la méthode. Il est indiqué sans type − ce serait redondant, puisque son type est forcément la classe où il apparaît. Il doit apparaître comme paramètre d’entrée, de sortie ou d’entrée-sortie, afin d’indiquer le rôle de la méthode sur l’objet auquel on l’applique : consultation (entrée), modification (entrée/sortie) ou initialisation (sortie). Notons que les méthodes de ce dernier typesont appelées initialiseurs[2] ; en Python, l’initialiseur est unique, et toujours nommé __init__.

[2]Dans de nombreux langages de programmation (C++, Java notamment), on les appelle constructeurs. Cette appellation nous semble cependant trompeuse, car ce ne sont généralement pas ces méthodes qui créent (i.e. allouent) l’objet ; elles ne sont chargées que d’initialiser ses attributs privés.

On donne ci-dessous un exemple d’interface de classe :

class FeuTricolore:

    def __init__(self):
        """
        :sortie self:
        :post-cond: le feu est au vert
        """

    def etat(self):
        """
        :entrée self:
        :sortie e: str
        :post-cond: e est l'état courant du feu,
                    parmi "vert", "orange" ou "rouge"
        """

    def change_etat(self):
        """
        :entrée-sortie self:
        :post-cond: passe à l'état suivant, selon le cycle
                    "vert" -> "orange" -> "rouge" -> "vert"
        """

Initialiseur et création d’un objet

L’initialiseur d’une classe a une structure particulière :

  • il s’appelle toujours __init__ ;
  • il peut avoir des paramètres d’entrée, mais self est son unique paramètre de sortie ;
  • il a la responsabilité d’initialiser tous les attributs privés de l’objet.

Pour créer un objet, on utilise une syntaxe similaire à celle des structures de données

ft = FeuTricolore()

Les paramètres acceptés par la classe sont ceux déclarés dans la méthode __init__ (à l’exception bien sûr du paramètre implicite self). Cette dernière est appelée implicitement à la création de l’objet ; de sorte que l’objet automatiquement initialisé.

Implémentation

Lorsqu’on souhaité décrire l’implémentation d’une classe, on décrira complètement les méthodes.

Il peut être nécessaire pour implémenter les méthodes de définir des fonctions supplémentaires, ayant elles aussi accès aux attributs privés de l’objet. On appelle ces fonctions des méthodes privées de l’objet ; en Python, on utilisera la même convention de nommage que pour les attributs privés (nom commençant par un _). Par opposition, on qualifiera les méthodes présentes dans l’interface de méthodes publiques.

class FeuTricolore:

    def __init__(self):
        #### attributs privés
        self._num_etat = 0  # parmi 0 (vert), 1 (orange), 2 (rouge)

    def etat(self):
        if self._num_etat == 0:
            e = "vert"
        elif self._num_etat == 1:
            e = "orange"
        else:
            e = "rouge"
        return e

    def change_etat(self):
        self._num_etat = self._num_etat_suivant()

    #### méthodes privées

    def _num_etat_suivant(self):
        """
        :entrée self:
        :sortie s: int
        :post-cond: s est le numéro du prochain état
        """
        s = (self._num_etat + 1) % 2
        return s

Classes génériques

Certains types abstraits ou classes peuvent être déclinés de différentes manières. C’est le cas des structures appelées à stocker plusieurs valeurs d’un même type, comme la pile ou la file : leur implémentation ne dépend pas du type de valeur stockée. Pour des raison de gain de temps et de maintenabilité, on souhaiterait donc pouvoir écrire ces classes une seule fois, et le réutiliser quel que soit le type de valeur qu’on y souhaite stocker. On appelle une telle classe une classe générique.

En Python et dans les langages faiblement typés, on se contentera en général de ne pas contraindre le type des éléments manipulés par la classe, ou d’utiliser le type le plus abstrait (object en Python). On utilisera ensuite les pré/post-conditions pour imposer le type des éléments d’un objet donné. Par exemple, Une fonction prenant en paramètre d’entrée une pile pourra imposer dans ses pré-condition que cette pile ne contienne que des entiers. Mais le respect de cette pré-condition restera de la responsabilité du programmeur.

Dans certains langages fortement typés comme C++ ou Java, une syntaxe particulière permet d’exprimer une classe générique, puis de la « décliner » selon les types d’éléments voulus.

Ré-écriture orientée objet : la classe File

Dans cette section, nous allons ré-écrire le type abstrait Tfile sous forme d’une classe File. Nous en profiterons pour rendre ce type générique (rappelons que l’implémentation précédente était spécialisée pour stocker des chaînes de caractères). Le même travail peut bien sûr être fait pour le type abstrait Tpile.

######## interface ########

class File:

    def __init__(self):
        """
        :sortie self:
        :post-cond: self est une file vide
        """

    def est_vide(self):
        """
        :entrée self:
        :sortie v: bool
        :post-cond: v est True si et seulement si self est vide
        """

    def est_pleine(self):
        """
        :entrée self:
        :sortie p: bool
        :post-cond: v est True si et seulement si self est pleine
        """

    def ajoute_en_queue(self, e):
        """
        :entrée/sortie self:
        :entrée e: object
        :pré-cond: self n'est pas pleine
        :post-cond: e est ajouté en queue de self
        """

    def tete(self):
        """
        :entrée self:
        :sortie t: object
        :pré-cond: self n'est pas vide
        :post-cond: e est l'élément en tête de self
        """

    def retire_tete(self):
        """
        :entrée/sortie self:
        :pré-cond: self n'est pas vide
        :post-cond: l'élément stocké en tête de self est retiré
        """

######## implémentation ########

from algo import *

class Tmaillon(Struct):
    valeur = object
    suivant = SAME

class File:

    def __init__(self):
        #### attributs privés
        self.mtete = None   # Tmaillon de tête
        self.mqueue = None  # Tmaillon de queue

    def est_vide(self):
        v = (self.mtete == None)
        return v

    def est_pleine(self):
        # cette implémentation ne limite pas le nombre
        # d'éléments que la file peut stocker
        p = False
        return p

    def ajoute_en_queue(self, e):
        m = Tmaillon(valeur=e, suivant=None)
        if self.mqueue != None:
            self.mqueue.suivant = m
        else:
            self.mtete = m
        self.mqueue = m

    def tete(self):
        t = self.mtete.valeur
        return t

    def retire_tete(self):
        self.mtete = self.mtete.suivant
        if self.mtete == None:
            self.mqueue = None