Classes Et Interfaces


Classes


Brève introduction à la Programmation Orientée Objet (POO)


La programmation orientée objet (POO) est un concept de programmation très puissant qui permet de structurer ses programmes d'une manière nouvelle. Les classes sont les principaux outils de la POO. Ce type de programmation permet de structurer les logiciels complexes en les organisant comme des ensembles d'objets qui interagissent, entre eux et avec le monde extérieur. De plus, la POO amène de nouveaux concepts tels que le polymorphisme (Capacité à redéfinir le comportement des opérateurs) ou l'héritage (Capacité à définir une classe à partir d'une classe pré-existante et d'y ajouter de nouvelles fonctionnalités), concepts que nous ne développerons pas dans ce chapitre.

Mais qu'est-ce qu'une classe ?

↪ À Retenir :

Une classe peut être vue comme un moule permettant de créer autant d'objets souhaités sur un même modèle.
On appelle ces objets des instances (des représentants) de cette classe. Chaque instance peut posséder :

  • Des attributs : Variables associées aux instances
  • Des méthodes : Fonctions associées aux instances qui peuvent agir sur ces dernières ou encore les utiliser

Exemple :

On peut définir une classe Eleve qui contient les attributs suivants :

  • nom : Nom de l'élève
  • prenom : Prénom de l'élève
  • age : ...
  • classe : ...

ainsi que les méthodes suivantes :

  • souffle_bougies : Méthode permettant d'incrémenter l'attribut age de 1
  • change_classe : Méthode permettant de changer de classe
AttributsValeurs
nomMachin
prenomTom
age17
classeT1
Méthodes
souffle_bougies
change_classe
Une instance e1 de Eleve

Remarque :

En fait, en Python tout est objet. Une variable de type int est en fait un objet de type int, donc construit à partir de la classe int. Il en va de même pour les float et les str, mais également pour les objets de type list, tuple, dict, ... C'est la raison pour laquelle nous avons déjà rencontré la notion de méthode :

  • Les méthodes lower et upper pour les objets de type str
  • La méthode sort et la méthode append pour les objets de type list

Rappelons que la fonction type permet de déterminer la classe d'un objet :

>>> print(type(3))
<class 'int'>

>>> print(type([3, 4, 5]))
<class 'list'>


On notera que les classes sont l’unique moyen en langage python de définir de nouveaux types, propres à celui qui programme.

Création d'une classe et Instanciation


En Python, le mot-clé class permet de créer sa propre classe, suivi du nom de cette classe. Un nom de classe commence toujours par une majuscule et s'écrit de préférence en Camel Case. Comme pour les fonctions, cette « ligne de définition » attend un bloc d'instructions indenté définissant le corps de la classe.

Comme pour les fonctions, On pourra écrire la documentation de la classe entre triple quotes.

La création d’un objet de type MaClasse est identique à celle des types standards du langage Python : elle passe par une simple affectation. La syntaxe d'instanciation est identique à la syntaxe d’appel d’une fonction. La création d’une instance peut également faire intervenir des paramètres, comme nous le verrons ultérieurement.

Exemple :

# Création de la classe UneClasseVide
class UneClasseVide:
    '''Une classe vide sans attribut ni méthode'''
    pass


# Instanciation de deux objets de type UneClasseVide
instance_1 = UneClasseVide()
instance_2 = UneClasseVide()

print(f'Type de instance_1 : {type(instance_1)}')
print(isinstance(instance_2, UneClasseVide))

↪ À Retenir :

# Syntaxe pour définir une classe :
class NomClasse:
    '''Documentation de la classe'''
    # Corps de la classe
    ...


# Syntaxe pour instancier un objet de type NomClasse sans paramètre :
nom_instance = NomClasse()
# Syntaxe pour instancier un objet de type NomClasse sans paramètre :
nom_instance = NomClasse(param_1, param_2, ...)

# La fonction `isinstance` permet de savoir si un objet est une instance d'une classe spécifique :
isinstance(nom_instance, NomClasse)
exple_UneClasseVide.JPG

Attributs et méthodes d'instances


Les attributs d'instances


Rappelons que les attributs d'instances sont des variables associées à une instance.
La syntaxe pour accéder à un attribut d'instance, en lecture ou en écriture, est la suivante : nom_instance.attribut.
Si l'attribut n'existe pas, il est créé pour l'instance considérée.

Exemple n°1:

Q1. Compléter le code ci-dessous en respectant les consignes données :

class Eleve:
    '''Classe représentant un élève'''
    pass


# Instancier un élève e1 de la classe Eleve
...
# Créer un attribut nom ayant pour valeur 'Machin' pour l'élève e1
...
# Créer un attribut prenom ayant pour valeur 'Tom' pour l'élève e1
...
# Afficher "L'élève e1 s'appelle Tom Machin" en utilisant ces attributs
...

# Instancier un élève e2 de la classe Eleve
...
# Créer un attribut age ayant pour valeur 17 pour l'élève e2
...
# Créer un attribut classe ayant pour valeur 'T1' pour l'élève e2
...
# Afficher "L'élève e2 est en T1 et a 17 ans" en utilisant ces attributs
...


Q2. Visualiser le code complété sur pythontutor.com.

In [ ]:
class Eleve:
    '''Classe représentant un élève'''
    pass


# Instancier un élève e1 de la classe Eleve
e1 = Eleve()
# Créer un attribut nom ayant pour valeur 'Machin' pour l'élève e1
e1.nom = 'Machin'
# Créer un attribut prenom ayant pour valeur 'Tom' pour l'élève e1
e1.prenom = 'Tom'
# Afficher "L'élève e1 s'appelle Tom Machin" en utilisant ces attributs
print(f'L\'élève e1 s\'appelle {e1.prenom} {e1.nom}')

# Instancier un élève e2 de la classe Eleve
e2 = Eleve()
# Créer un attribut age ayant pour valeur 17 pour l'élève e2
e2.age = 17
# Créer un attribut classe ayant pour valeur 'T1' pour l'élève e2
e2.classe = 'T1'
# Afficher "L'élève e2 est en T1 et a 17 ans" en utilisant ces attributs
print(f'L\'élève e2 est en {e2.classe} et a {e2.age} ans')

Les méthodes d'instances


Une fonction définie au sein d'une classe est appelée méthode.
La syntaxe pour définir une méthode d'instance utilise le mot-clé def comme pour les fonctions.
Toute méthode d'instance prend obligatoirement un argument nommé self. Il s'agit d'une référence vers l'instance créée. Ainsi, le premier argument d'une méthode d'instance est l'instance elle-même.

La syntaxe générale pour exécuter une méthode à l'extérieur de la classe est la suivante : nom_instance.nom_methode().
Cette syntaxe est équivalente à : NomClasse.nom_methode(nom_instance).

Reprenons l'exemple de la méthode lower des objets de type str. On peut écrire indifféremment :

'ABC'.lower()
# est équivalent à :
str.lower('ABC')

Exemple :

class ClasseBidon:
    '''Une classe qui ne fait pas grand-chose'''

    def affiche_bonjour(self):
        '''Méthode affichant "Bonjour !"'''
        print('Bonjour !')

    def eleve_au_carre(self, entier:int) -> int:
        '''
        Cette méthode renvoie le carré d'un entier
        param entier : un nombre entier
        '''
        return entier*entier


# Instanciation d'un objet obj de la classe ClasseBidon
obj = ClasseBidon()

# Utilisations de la méthode affiche_bonjour 
obj.affiche_bonjour()
ClasseBidon.affiche_bonjour(obj)

# Utilisations de la méthode eleve_au_carre 
print(obj.eleve_au_carre(5))
print(ClasseBidon.eleve_au_carre(obj, 7))

Exemple n°2 :

On considère le code incomplet ci-dessous :

class Eleve:
    '''Classe représentant un élève'''

    def souffle_bougies(...):
        '''doc'''
        pass

    def change_classe(...):
        '''doc'''
        pass

# Instancier un élève e de la classe Eleve
...
# Créer un attribut age ayant pour valeur 17 pour l'élève e
...
# Créer un attribut classe ayant pour valeur 'T1' pour l'élève e
...
# Appliquer la méthode souffle_bougie à e et contrôler le résultat
...
# Appliquer la méthode change_classe à e et contrôler le résultat
...

Q1. Compléter la signature et le corps de la méthode souffle_bougies.
Cette méthode doit :

  • Incrémenter l'attribut age de 1 :
  • Afficher le message suivant : Joyeux anniv ! {nouvel âge} ans à présent
  • Retourner la nouvelle valeur de l'attribut age

Q2. Compléter la signature et le corps de la méthode change_classe.
Cette méthode prend en paramètre le nom de la nouvelle classe nom_classe et doit :

  • Changer la valeur de l'attribut classe en conséquence
  • Afficher le message suivant : Changement de classe : {nom de la classe actuelle} -> {nom de la nouvelle classe}

Q3. Écrire les instructions demandées.

Le constructeur : la méthode spéciale __init__


Il est même très vivement recommandé de déclarer les attributs d'instances lors de l'instanciation d'un objet à partir d'une classe. On peut également souhaiter initialiser les valeurs de ces attributs.

Pour cela, on utilise une méthode spéciale appelée constructeur. Ce constructeur est implicitement exécuté lors de la création de chaque instance. Il se présente comme une méthode et suit la même syntaxe à ceci près que son nom est imposé : __init__ (double underscores). Hormis le premier paramètre, invariablement self, il n’existe pas de contrainte concernant la liste des paramètres passés. En revanche, un constructeur ne doit pas retourner de résultat : Il ne peut donc pas contenir l'instruction return.

Exemple : Reprenons l'exemple de la classe Eleve

class Eleve:
    def __init__(self, nom, prenom=''):
        self.nom = nom
        self.prenom = prenom

e1 = Eleve('Machin')
e2 = Eleve('Truc', 'Tim')
print(f'L\'élève e1 a pour nom "{e1.nom}" et pour prénom "{e1.prenom}"')
print(f'L\'élève e2 a pour nom "{e2.nom}" et pour prénom "{e2.prenom}"')

Dans cet exemple, on constate qu'il est possible de créer des instances de Eleve de deux manières différentes :

  • Avec un seul paramètre, le paramètre nom : Dans ce cas, on affecte à :
    • L'attribut d'instance nom la valeur passée au paramètre nom
    • L'attribut d'instance prenom la valeur par défaut, c'est-à-dire la chaîne de caractère vide ''
  • Avec deux paramètres, les paramètres nom et prenom : Dans ce cas, on affecte à :
    • L'attribut d'instance nom la valeur passée au paramètre nom
    • L'attribut d'instance prenom la valeur passée au paramètre prenom

Exemple n°3 : On considère la classe Eleve de l'exemple n°2

Q1. Créer le constructeur de la classe Eleve qui aura pour paramètres nom, prenom, age et classe.
Ces paramètres prendront tous la valeur par défaut None.

Q2. Créer la méthode presentation qui devra produire l'affichage suivant :

Eleve :
Nom : {son nom}
Prénom : {son prenom}
Age : {son age} ans
Classe : {sa classe}

Q3. Modifier la méthode souffle_bougies de l'exemple précédent pour qu'elle affiche Age inconnu si l'âge n'est pas renseigné.

Q4. Créer les élèves ci-dessous et tester les trois méthodes pour chacun d'eux :

  • Un élève e1 dont on ne connaît ni le nom, ni le prénom, ni l'âge ni la classe
  • Un élève e2 âgé de 17 ans ayant pour nom 'Machin' et pour prénom 'Tom'
  • Un élève e3 âgé de 18 ans ayant pour nom 'Truc' et pour prénom 'Tim', affecté à la classe 'T1'
  • Un élève e4 affecté à la classe 'T1' et ayant pour prénom 'Bob'

Q5. Visualiser le code complété sur pythontutor.com.

In [ ]:
class Eleve:
    '''Classe représentant un élève'''

    def __init__(self, nom=None, prenom=None, age=None, classe=None):
        '''doc'''
        self.nom = nom
        self.prenom = prenom
        self.age = age
        self.classe = classe

    def presentation(self):
        '''doc'''
        print(f'ELEVE :\
                \nNom : {self.nom}\
                \nPrénom : {self.prenom}\
                \nAge : {self.age} ans\
                \nClasse : {self.classe}')

    def souffle_bougies(self):
        '''doc'''
        if self.age is not None:
            self.age += 1
            print(f'Joyeux Anniv ! {self.age} ans à présent.')
        else:
            print('Age inconnu')
        return self.age

    def change_classe(self, new_classe):
        '''doc'''
        print(f'Changement de classe : {self.classe} -> {new_classe}')
        self.classe = new_classe


# Instancier un élève e1 de la classe Eleve :
e1 = Eleve()
# Présentation de l'élève e1 :
e1.presentation()
# Anniversaire de l'élève e1 :
e1.souffle_bougies()
# L'élève e1 passe en terminale 'T2' :
e1.change_classe('T2')

# Instancier un élève e2 de la classe Eleve :
e2 = Eleve('Machin', 'Tom', 17)
# Présentation de l'élève e2 :
e2.presentation()
# Anniversaire de l'élève e2 :
e2.souffle_bougies()
# L'élève e2 passe en terminale 'T2' :
e2.change_classe('T2')

# Instancier un élève e3 de la classe Eleve :
e3 = Eleve('Truc', 'Tim', 18, 'T1')
# Présentation de l'élève e3 :
e3.presentation()
# Anniversaire de l'élève e3 :
e3.souffle_bougies()
# L'élève e3 passe en terminale 'T2' :
e3.change_classe('T2')

# Instancier un élève e4 de la classe Eleve :
e4 = Eleve(prenom='Bob', classe='T1')
# Présentation de l'élève e4 :
e4.presentation()
# Anniversaire de l'élève e4 :
e4.souffle_bougies()
# L'élève e4 passe en terminale 'T2' :
e4.change_classe('T2')

↪ À Retenir : Structure générale d'une classe

class NomClasse:
    '''doc'''
    
    def __init__(self, param1, param2=val_defaut_2, param3=val_defaut_3):
        '''doc'''
        pass
    
    def methode_1(self):
        '''doc'''
        pass
    
    def methode_2(self, param1, param2):
        '''doc'''
        pass


# instanciations d'objets de NomClasse
i_1 = NomClasse(val1)
i_2 = NomClasse(val1, val2)
i_3 = NomClasse(val1, val2, val3)
i_4 = NomClasse(param2=val2)

# Utilisation des méthodes sur une instance
i_3.methode_1()
i_3.methode_2(val1, val2)
exple_ClasseEleve.JPG
Exemple relatif à la classe Eleve

Complément 1 : Deux méthodes spéciales pour représenter un objet


Nous avons déjà rencontré un exemple de méthode spéciale avec la méthode __init__. Une méthode spéciale, en Python, voit son nom entouré de part et d'autre par deux underscores.
Le nom d'une méthode spéciale prend donc la forme suivante : __nom_methode_speciale__

Parmi les méthodes spéciales existantes, nous allons nous intéresser à deux méthodes spéciales qui permettent de contrôler comment l'objet est représenté et affiché. En effet, si on essaye d'afficher directement un objet dans l'interpréteur ou grâce à la fonction print, on obtient quelque chose d'assez laid comme le montre les exemples ci-dessous où e est une instance de la classe Eleve :

  • L'instruction print(e) affiche :
    <__main__.Eleve object at 0x000001DE30A60448>
    
  • Saisir e dans l'interpréteur renvoie :
    <__main__.Eleve at 0x1de30a60448>
    

Les deux méthodes spéciales permettant de configurer la représentation et l'affichage des objets sont les suivantes :

  • La méthode __repr__ :
    Elle ne prend aucun paramètre, excepté self, et renvoie la chaîne de caractères que l'on souhaite afficher quand on saisit directement le nom de l'objet dans l'interpréteur.
  • La méthode __str__ :
    Elle est spécialement utilisée pour afficher l'objet avec la fonction print.
    Elle est également appelée si vous désirez convertir votre objet en chaîne à l'aide du constructeur str.

On notera que, par défaut, si la méthode __str__ n'est pas définie, Python appelle la méthode __repr__ de l'objet.

Exemple n°4 :

Q1. Créer une représentation de la classe Eleve à l'aide de la méthode __repr__.

Q2. Créer une représentation de la classe Eleve à l'aide de la méthode __str__, différente de la représentation précédente.

Complément 2 : Attributs d'instances implicites


Certains attributs sont créés de manière implicite lors l'instanciation d'un objet. Ils contiennent des informations sur l’instance.

Nom de l'attribut Information
__module__ Contient le nom du module dans lequel est incluse la classe
__class__ Contient le nom de la classe de l’instance. Ce nom est précédé du nom du module suivi d’un point.
__dict__ Contient le dictionnaire des attributs de l’instance
__doc__ Contient la documentation associée à la classe

Complément 3 : Attributs et méthodes de classe


On peut souhaiter définir des attributs sur la classe elle-même, et non pas sur ses instances. On parle alors d'attribut de classe.
Ces attributs sont définis en dehors des méthodes de la classe.
Voici la syntaxe pour accéder en lecture ou en écriture à ses attributs :

  • NomClasse.nom_attribut_classe à l'extérieur de la classe
  • cls.nom_attribut_classe depuis la classe

Il est également possible de définir des méthodes de classe.
Le premier argument d'une telle méthode sera la classe elle-même, représentée par le mot-clé cls.
Le décorateur @classmethod devra précéder la définition de la fonction.

Exemple :

class Citron:
    forme = "ellipsoïde"    # attribut de classe
    saveur = "acide"        # attribut de classe

    def __init__(self, couleur="jaune", taille="standard", masse=100):
        self.couleur = couleur    # attribut d'instance
        self.taille = taille      # attribut d'instance
        self.masse = masse        # attribut d'instance (masse en gramme)

    def affiche_attributs_instance(self):    # méthode d'instance
        print(f'Attribut couleur de l\'instance : {self.couleur}\
                \nAttribut taille de l\'instance : {self.taille}\
                \nAttribut masse de l\'instance : {self.masse} g\n')

    @classmethod
    def affiche_attributs_classe(cls):    # méthode de classe
        print(f'Attribut forme de la classe Citron : {cls.forme}\
                \nAttribut saveur de la classe Citron : {cls.saveur}\n')


c1 = Citron()
c1.affiche_attributs_instance()
c2 = Citron("vert", "XXL", 150)
c2.affiche_attributs_instance()
Citron.affiche_attributs_classe()

Exemple n°5 :

On considère la classe Eleve ci-dessous :

class Eleve:
    '''Classe représentant un élève'''

    def __init__(self, nom=None, prenom=None, age=None, classe=None):
        '''Constructeur'''
        self.nom = nom
        self.prenom = prenom
        self.age = age
        self.classe = classe

Q1. Initialiser un attribut de classe compteur à zéro.
Cet attribut devra être incrémenté de 1 à la création de chaque nouvelle instance de Eleve.

Q2. Créer une méthode de classe combien qui retourne le nombre d'objets de la classe Eleve créés, puis la tester.

In [ ]:
class Eleve:
    '''Classe représentant un élève'''
    compteur = 0

    def __init__(self, nom=None, prenom=None, age=None, classe=None):
        '''Constructeur'''
        self.nom = nom
        self.prenom = prenom
        self.age = age
        self.classe = classe
        Eleve.compteur += 1

    @classmethod
    def combien(cls):
        '''doc'''
        return cls.compteur


# Instancier un élève e1 de la classe Eleve :
e1 = Eleve()
e2 = Eleve('Machin', 'Tom', 17)
e3 = Eleve('Truc', 'Tim', 18, 'T1')
e4 = Eleve(prenom='Bob', classe='T1')

Eleve.combien()

Interfaces


Définition et intérêt


Une interface est une description des capacités fournies par une unité de code. Dans les langages orientés objet comme Python, les interfaces sont souvent définies par une collection de signatures de méthodes qui doivent être fournies par une classe : Les méthodes y sont seulement déclarées. Cela permet de définir un ensemble de services visibles depuis l’extérieur (l’API : Application Programming Interface). Une classe qui implémente une interface doit obligatoirement implémenter chacune des méthodes déclarées dans l’interface

Les interfaces sont donc utiles pour spécifier une sorte de cahier des charges. En marquant qu'un type implémente une interface, nous donnons une garantie, vérifiable par un programme, que l'implémentation fournit les méthodes spécifiées par l'interface, sans nous préoccuper de la façon dont ces services sont réellement implémentés.
On notera enfin qu'une classe peut implémenter plusieurs interfaces.

Interfaces et Python


Dans ce cours, nous utiliserons la bibliothèque interface pour déclarer des interfaces et créer des classes implémentant ces interfaces. Les interfaces créées à l'aide de cette bibliothèque nous permettront de détecter les erreurs suivantes :

  • Méthodes manquantes ou non implémentées
  • Signature de méthode incompatible

Classe implémentant une interface


Exemple :

Voici sur un exemple la syntaxe pour créer une interface et une classe implémentant cette interface.

# import de la classe Interface et de la fonction implements
from interface import implements, Interface


# Définition de l'interface InterfaceOperations
class InterfaceOperations(Interface):
    def somme(self, x: int, y: int) -> int:
        pass

    def produit(self, x: int, y: int) -> int:
        pass


# Création de la classe Operations implémentant l'interface InterfaceOperations
class Operations(implements(InterfaceOperations)):
    def somme(self, x: int, y: int) -> int:
        return x + y

    def produit(self, x: int, y: int) -> int:
        return x * y

Exemple n°6 : Classe implémentant une interface

On considère l'interface InterfaceEleve ci-dessous.

from interface import Interface, implements


class InterfaceEleve(Interface):
    '''Classe représentant un élève'''

    def __init__(self, 
                 nom: str = None,
                 prenom: str = None,
                 age: int = None,
                 classe: str = None):
        '''Constructeur - initialisation des attributs d'instance'''
        pass

    def presentation(self):
        '''
        Méthode affichant les attributs de l'élève sous la forme suivante :
        Eleve :
        Nom : {son nom}
        Prénom : {son prenom}
        Age : {son age} ans
        Classe : {sa classe}
        '''
        pass

    def souffle_bougies(self) -> int:
        '''
        Méthode qui incrémente l'âge de 1 et renvoie la nouvelle valeur s'il
        est renseigné. Sinon, elle affiche 'âge inconnu' et renvoie None
        '''
        pass

    def change_classe(self, new_classe: str):
        '''Méthode changeant la valeur de l'attribut classe par new_classe'''
        pass

Q1. Créer la classe Eleve implémentant cette interface en vous aidant de l'exemple n°3.

Q2. Modifier la signature d'une fonction de la classe Eleve. Que se passe-t-il ?

Q3. Commenter la fonction change_classe. Que se passe-t-il ?

Plusieurs classes implémentant une même interface


Exemple n°7 : Différentes implémentations pour une même interface

Q1. Créer une classe OperationsV2 qui :

  • Implémente l'interface InterfaceOperations
  • Calcule le produit de deux entiers de manière itérative en utilisant uniquement l'opérateur +
In [ ]:
from interface import implements, Interface

# Définition de l'interface InterfaceOperations
class InterfaceOperations(Interface):
    def somme(self, x: int, y: int) -> int:
        pass

    def produit(self, x: int, y: int) -> int:
        pass
In [ ]:
# Création de la classe OperationsV2 implémentant l'interface InterfaceOperations
class OperationsV2(implements(InterfaceOperations)):
    def somme(self, x: int, y: int) -> int:
        return x + y

    def produit(self, x: int, y: int) -> int:
        res = 0
        for i in range(x):
            res += y
        return res

Q2. Créer une classe OperationsV3 qui :

  • Implémente l'interface InterfaceOperations
  • Calcule le produit de deux entiers de manière récursive comme indiqué ci-dessous : $produit(x, y) = \left\{\begin{array}{lll}0 & \text{si }x=0\text{ ou }y=0 \\ y & \text{si }x=1 \\ y + produit(x-1, y) & \text{sinon}\end{array}\right.$
In [ ]:
# Création de la classe OperationsV3 implémentant l'interface InterfaceOperations
class OperationsV3(implements(InterfaceOperations)):
    def somme(self, x: int, y: int) -> int:
        return x + y

    def produit(self, x: int, y: int) -> int:
        if x == 0 or y == 0:
            return 0
        if x == 1:
            return y
        return y + produit(x - 1, y)

Classes implémentant plusieurs interfaces


Exemple n°8 : Classes implémentant plusieurs interfaces

On considère les trois interfaces ci-dessous :

class InterfaceFigure(Interface):
    '''Interface pour créer des classes de figures géométriques en 2D'''

    def perimetre(self) -> float:
        pass

    def aire(self) -> float:
        pass

    def attributs(self) -> dict:
        '''Renvoie le dictionnaire des attributs de la figure'''
        pass
class InterfaceDisque(Interface):
    '''Interface pour créer des classes de disques'''

    def __init__(self,
                 centre: tuple = (0., 0.),
                 rayon: int = None):
        '''constructeur'''
        pass

    def diametre(self) -> float:
        '''renvoie le diamètre du cercle'''
        pass
class InterfaceRectangle(Interface):
    '''Interface pour créer des classes de rectangles'''

    def __init__(self,
                 sommet_1: tuple = (0., 0.),
                 longueur: int = None,
                 largeur: int = None):
        '''constructeur'''
        pass

    def est_carre(self) -> bool:
        '''renvoie True si c'est un carré et False sinon'''
        pass

    def liste_sommets(self) -> list:
        '''
        renvoie la liste des tuples représentant les 4 sommets
        Les deux premiers sommets seront sommet_1 et (x_1 + longueur, y_1)
        Les sommets seront créés dans le sens direct
        '''
        pass

Q1. Créer une classe Disque implémentant les interfaces InterfaceDisque et InterfaceFigure.
Ne pas oublier d'importer la constante pi de la librairie math.
Tester le code avec :

  • Une instance d1 utilisant les valeurs par défaut
  • Une instance d2 représentant un disque de centre (0., 0.) et de rayon 5
In [ ]:
from interface import Interface, implements


class InterfaceFigure(Interface):
    '''Interface pour créer des classes de figures géométriques en 2D'''

    def perimetre(self) -> float:
        pass

    def aire(self) -> float:
        pass

    def attributs(self) -> dict:
        '''Renvoie le dictionnaire des attributs de la figure'''
        pass


class InterfaceDisque(Interface):
    '''Interface pour créer des classes de disques'''

    def __init__(self,
                 centre: tuple = (0., 0.),
                 rayon: int = None):
        '''constructeur'''
        pass

    def diametre(self) -> float:
        '''renvoie le diamètre du cercle'''
        pass


class InterfaceRectangle(Interface):
    '''Interface pour créer des classes de rectangles'''

    def __init__(self,
                 sommet_1: tuple = (0., 0.),
                 longueur: int = None,
                 largeur: int = None):
        '''constructeur'''
        pass

    def est_carre(self) -> bool:
        '''renvoie True si c'est un carré et False sinon'''
        pass

    def liste_sommets(self) -> list:
        '''
        renvoie la liste des tuples représentant les 4 sommets
        Les deux premiers sommets seront sommet_1 et (x_1 + longueur, y_1)
        Les sommets seront créés dans le sens direct
        '''
        pass
In [ ]:
import math


class Disque(implements(InterfaceDisque, InterfaceFigure)):
    '''Classe implémentant InterfaceDisque et InterfaceFigure'''

    def __init__(self,
                 centre: tuple = (0., 0.),
                 rayon: int = None):
        '''constructeur'''
        self.centre = centre
        self.rayon = rayon

    def diametre(self) -> float:
        '''renvoie le diamètre du cercle'''
        if self.rayon is None:
            return None
        return 2*self.rayon

    def perimetre(self) -> float:
        '''renvoie le périmètre du disque'''
        if self.rayon is not None:
            return 2 * math.pi * self.rayon

    def aire(self) -> float:
        '''renvoie l'aire du disque'''
        if self.rayon is not None:
            return math.pi * self.rayon ** 2

    def attributs(self) -> dict:
        '''Renvoie le dictionnaire des attributs de la figure'''
        return {'centre': self.centre, 'rayon': self.rayon}
In [ ]:
d1 = Disque()
print(d1.attributs())
print(d1.aire())
print(d1.perimetre())
print(d1.diametre())

d2 = Disque((2., 3.), 5)
print(d2.attributs())
print(d2.aire())
print(d2.perimetre())
print(d2.diametre())

Q2. Créer une classe Rectangle implémentant les interfaces InterfaceRectangle et InterfaceFigure.
Tester le code avec :

  • Une instance r1 utilisant les valeurs par défaut
  • Une instance r2 représentant un rectangle de dimension 4 × 3 et ayant pour sommet le point (2., 1.)
In [ ]:
class Rectangle(implements(InterfaceRectangle, InterfaceFigure)):
    '''Classe implémentant InterfaceRectangle et InterfaceFigure'''

    def __init__(self,
                 sommet_1: tuple = (0., 0.),
                 longueur: int = None,
                 largeur: int = None):
        '''constructeur'''
        self.sommet_1 = sommet_1
        self.longueur = longueur
        self.largeur = largeur

    def est_carre(self) -> bool:
        '''renvoie True si c'est un carré et False sinon'''
        if self.longueur is not None and self.largeur is not None:
            return self.longueur == self.largeur

    def liste_sommets(self) -> list:
        '''
        renvoie la liste des tuples représentant les 4 sommets
        Les deux premiers sommets seront sommet_1 et (x_1 + longueur, y_1)
        Les sommets seront créés dans le sens direct
        '''
        if self.longueur is not None and self.largeur is not None:
            xs1, ys1 = self.sommet_1
            return [(xs1, ys1),
                    (xs1 + self.longueur, ys1),
                    (xs1 + self.longueur, ys1 + self.largeur),
                    (xs1, ys1 + self.largeur)]

    def perimetre(self) -> float:
        '''renvoie le périmètre du rectangle'''
        if self.longueur is not None and self.largeur is not None:
            return 2 * (self.longueur + self.largeur)

    def aire(self) -> float:
        '''renvoie l'aire du rectangle'''
        if self.longueur is not None and self.largeur is not None:
            return self.longueur * self.largeur

    def attributs(self) -> dict:
        '''Renvoie le dictionnaire des attributs de la figure'''
        return {'longueur': self.longueur, 'largeur': self.largeur}
In [ ]:
r1 = Rectangle()
print(r1.attributs())
print(r1.aire())
print(r1.perimetre())
print(r1.est_carre())
print(r1.liste_sommets())

r2 = Rectangle(longueur=4, largeur=3)
print(r2.attributs())
print(r2.aire())
print(r2.perimetre())
print(r2.est_carre())
print(r2.liste_sommets())

Application aux listes chaînées, aux piles et aux files


Les structures de données organisent le stockage dans des ordinateurs afin que nous puissions accéder et modifier efficacement les données.
Les listes chaînées, les piles et les files font partie des premières structures de données définies en informatique.

Les listes Chaînées


Définition :

Une liste chaînée est une structure de données très utilisée en informatique. Elle est fondamentale pour les structures de niveau supérieur comme les piles ou les files.

D'une manière générale, une liste chaînée est une collection d'éléments de données qui sont connectés par des références. Habituellement, chaque élément de la liste chaînée a le même type de données qui est spécifique à la liste.

Un élément d'une telle liste est appelé un noeud. À la différence des tableaux qui sont stockés séquentiellement en mémoire, les noeuds peuvent se trouver sur différents segments de mémoire : On passe d'un noeud à son successeur en suivant les références. La fin de la liste est marquée avec l'élément NULL (qui correspond à None en Python).

liste_chainee.JPG
Liste chaînée comportant 3 noeuds

Liste des opérations que l'on peut effectuer sur une liste chaînée :

  • Créer une nouvelle liste vide
  • Créer une nouvelle liste à partir d'une donnée et d'une autre liste
  • Tester si une liste est vide
  • Ajouter un noeud en tête de liste
  • Supprimer le noeud en tête de liste et renvoyer sa donnée
  • Compter et renvoyer le nombre de noeuds d'une liste (c'est-à-dire le nombre de données)

Interface d'une liste chaînée en Python :

class InterfaceListeChainee(Interface):
    def __init__(self, donnee=None, liste_chainee=None):
        '''
        Constructeur permettant de créer une nouvelle liste. Cette liste peut être :
            - La liste vide
            - La liste ayant pour tête le noeud contenant donnee
              et pour queue liste_chainee
        param donnee : Donnée à ajouter en tête de liste
        param liste_chainee : Liste représentant la queue de nouvelle liste
        '''
        pass

    def est_vide(self) -> bool:
        '''
        Méthode permettant de tester si la liste est vide
        return : True si la liste est vide, False sinon
        '''
        pass
         
    def ajoute_en_tete(self, donnee):
        '''
        Méthode permettant d'ajouter un noeud en tête de liste
        param donnee : donnée du noeud à ajouter
        '''
        pass
    
    def suppr_en_tete(self):
        '''
        Méthode permettant de supprimer le noeud en tête de liste et
        qui renvoie la donnée correspondante
        return : donnée du noeud en tête
        '''
        pass
    
    def longueur(self) -> int:
        '''
        Méthode qui renvoie la longueur de la liste chaînée
        return : nombre de données dans la liste
        '''
        pass

Exemple n°9 :

On suppose que la classe LinkedList implémente l'interface InterfaceListeChainee.
Expliquer chacune des lignes de cette suite d'instructions.
On précisera le contenu des listes et variables créées à chaque étape.

l = LinkedList()
print(l.est_vide())
l1 = LinkedList(2)
print(l1.est_vide())
l1.ajoute_en_tete(3)
l1.ajoute_en_tete(5)
l1.ajoute_en_tete(8)
t = suppr_en_tete()
l2 = LinkedList(9, l1)
c2 = l2.longueur()
l3 = LinkedList(15, LinkedList(4, LinkedList(7, LinkedList(6))))
c3 = l3.longueur()

Les piles


Les piles, comme son nom l'indique, suivent le principe du dernier entré, premier sorti : LIFO pour Last In First Out. Il est donc uniquement possible de manipuler le dernier élément introduit dans la pile. On prend souvent l'analogie avec une pile d'assiettes : Dans une telle pile, la seule assiette directement accessible est la dernière assiette qui a été déposée sur la pile ...

pile.JPG

Voici quelques exemples d'applications :

  • Les algorithmes récursifs utilisent une pile d'appel.
  • Dans un navigateur Web, une pile sert à mémoriser les pages Web visitées. L'adresse de chaque nouvelle page visitée est empilée et l'utilisateur dépile l'adresse courante pour accéder à la page précédente en cliquant le bouton « Afficher la page précédente ».
  • L'évaluation des expressions mathématiques en notation post-fixée (ou polonaise inverse) utilise une pile.
  • La fonction « Annuler la frappe » d'un traitement de texte mémorise les modifications apportées au texte dans une pile.
  • Un algorithme de recherche en profondeur utilise une pile pour mémoriser les noeuds visités.

Liste des opérations que l'on peut effectuer sur une pile :

  • Créer une pile vide
  • Tester si une pile est vide
  • Empiler un nouvel élément sur la pile (push)
  • Renvoyer l'élément au sommet de la pile tout en le supprimant (pop)
  • Renvoyer l'élément situé au sommet de la pile sans le supprimer de la pile (top)
  • Renvoyer le nombre d'éléments présents dans la pile (size)

Interface d'une pile en Python :

class InterfacePile(Interface):
    def __init__(self):
        '''Constructeur permettant la création d'une pile vide'''
        pass

    def is_empty(self) -> bool:
        '''
        Méthode permettant de tester si la pile est vide
        return : True si la pile est vide, False sinon
        '''
        pass

    def push(self, donnee):
        '''
        Méthode permettant d'empiler donnee sur la pile
        param donnee : donnée à empiler
        '''
        pass

    def pop(self):
        '''
        Méthode permettant de dépiler l'élément en haut de la pile
        return : donnée en haut de la pile
        '''
        pass

    def top(self):
        '''
        Méthode permettant d'accéder à l'élément en haut de la pile mais sans le supprimer
        return : donnée en haut de la pile
        '''
        pass

    def size(self) -> int:
        '''
        Méthode renvoyant le nombre d'éléments de la pile
        return : nombre d'élément de la pile
        '''
        pass

Exemple n°10 :

Q1. On suppose que la classe Pile implémente l'interface InterfacePile.
Expliquer chacune des lignes de cette suite d'instructions.
On précisera le contenu de la pile à chaque étape.

p = Pile()
p.push(5)
p.push(6)
p.push(7)
p.push(10)
print(p.is_empty())
print(p.top())
print(p.size())
print(p.pop())
print(p.is_empty())
print(p.top())
print(p.size())
print(p.pop())
print(p.pop())
print(p.pop())
print(p.is_empty())
print(p.top())
print(p.size())

Q2. Créer une classe Pile implémentant l'interface InterfacePile en utilisant le type list On veillera à respecter les contraintes suivantes :

  • Les méthodes pop et top renverront None si la pile est vide
  • On ajoutera à cette classe une méthode display permettant d'afficher les éléments de la pile
    Exemple : L'affichage produit pour la pile 12, 4, 88 est le sommet sera le suivant :
      8
      4
      12
In [ ]:
from interface import Interface, implements

class InterfacePile(Interface):
    def __init__(self):
        '''Constructeur permettant la création d'une pile vide'''
        pass

    def is_empty(self) -> bool:
        '''
        Méthode permettant de tester si la pile est vide
        return : True si la pile est vide, False sinon
        '''
        pass

    def push(self, donnee):
        '''
        Méthode permettant d'empiler donnee sur la pile
        param donnee : donnée à empiler
        '''
        pass

    def pop(self):
        '''
        Méthode permettant de dépiler l'élément en haut de la pile
        return : donnée en haut de la pile
        '''
        pass

    def top(self):
        '''
        Méthode permettant d'accéder à l'élément en haut de la pile mais sans le supprimer
        return : donnée en haut de la pile
        '''
        pass

    def size(self) -> int:
        '''
        Méthode renvoyant le nombre d'éléments de la pile
        return : nombre d'élément de la pile
        '''
        pass
In [ ]:
class Pile(implements(InterfacePile)):
    def __init__(self):
        self.liste = []
    
    def is_empty(self) -> bool:
        return len(self.liste) == 0
    
    def push(self, donnee):
        self.liste.append(donnee)
    
    def pop(self):
        if len(self.liste) == 0:
            return None
        return self.liste.pop()
    
    def top(self):
        if len(self.liste) == 0:
            return None
        return self.liste[-1]
    
    def size(self):
        return len(self.liste)
    
    def display(self):
        if len(self.liste) == 0:
            print('None')
        else:
            res = ''
            for donnee in reversed(self.liste):
                res += str(donnee) + '\n'
            print('Affichage pile :', res, sep='\n')

Q3. Tester le bon fonctionnement des méthodes de la classe Pile sur les instructions de la question 1.

In [ ]:
p = Pile()
p.push(5)
p.push(6)
p.push(7)
p.push(10)
p.display()
print('Pile vide ?', p.is_empty())
print('Sommet :', p.top())
print('Taille :', p.size())
print('On dépile :', p.pop())
p.display()
print('Pile vide ?', p.is_empty())
print('Sommet :', p.top())
print('Taille :', p.size())
print('On dépile :', p.pop())
print('On dépile :', p.pop())
print('On dépile :', p.pop())
p.display()
print('Pile vide ?', p.is_empty())
print('Sommet :', p.top())
print('Taille :', p.size())

Les files


Les files d'attente, comme son nom l'indique, suivent le principe du premier entré, premier sorti : FIFO pour First In First Out. Dans une file on ajoute donc des éléments à une extrémité de la file et on supprime des éléments à l'autre extrémité. On prend prendre l'analogie de la file d'attente pour les billets de cinéma : le premier à faire la queue est le premier à acheter un billet et à profiter du film.

file.JPG

En général, cette structure est utilisée pour mémoriser temporairement des transactions qui doivent attendre pour être traitées, comme pour les serveurs d'impression qui traitent les requêtes dans l'ordre dans lequel elles arrivent, et les insèrent dans une file d'attente. Un algorithme de parcours en largeur utilise également une file pour mémoriser les noeuds visités.

Liste des opérations que l'on peut effectuer sur une file :

  • Créer une file vide
  • Tester si une file est vide
  • Ajouter un nouvel élément à la file (enqueue)
  • Renvoyer l'élément en début de file tout en le supprimant (dequeue)
  • Renvoyer l'élément en début de file sans le supprimer de la file (first)
  • Renvoyer le nombre d'éléments présents dans la file (size)

Interface d'une file en Python :

class InterfaceFile(Interface):
    def __init__(self):
        '''Constructeur permettant la création d'une file vide'''
        pass

    def is_empty(self) -> bool:
        '''
        Méthode permettant de tester si la file est vide
        return : True si la file est vide, False sinon
        '''
        pass

    def enqueue(self, donnee):
        '''
        Méthode permettant d'ajouter donnee en fin de file
        param donnee : donnée à ajouter
        '''
        pass

    def dequeue(self):
        '''
        Méthode qui supprime et renvoie le premier élément de la file
        return : donnée en début de file
        '''
        pass

    def first(self):
        '''
        Méthode permettant d'accéder l'élément en début de file mais sans le supprimer
        return : donnée en début de file
        '''
        pass

    def size(self) -> int:
        '''
        Méthode renvoyant le nombre d'éléments de la file
        return : nombre d'éléments de la file
        '''
        pass

Exemple n°11 :

Q1. On suppose que la classe File implémente l'interface InterfaceFile.
Expliquer chacune des lignes de cette suite d'instructions.
On précisera le contenu de la file à chaque étape.

f = File()
f.enqueue(5)
f.enqueue(6)
f.enqueue(7)
f.enqueue(10)
print(f.is_empty())
print(f.first())
print(f.size())
print(f.dequeue())
print(f.is_empty())
print(f.first())
print(f.size())
print(f.dequeue())
print(f.dequeue())
print(f.dequeue())
print(f.is_empty())
print(f.first())
print(f.size())

Q2. Créer une classe File implémentant l'interface InterfaceFile en utilisant le type list. On veillera à respecter les contraintes suivantes :

  • Les méthodes dequeue et first renverront None si la file est vide
  • On ajoutera à cette classe une méthode display permettant d'afficher les éléments de la file
    Exemple : L'affichage produit pour la file 12, 4, 812 est le premier élément sera le suivant :
      8 4 12
In [ ]:
from interface import Interface, implements

class InterfaceFile(Interface):
    def __init__(self):
        '''Constructeur permettant la création d'une file vide'''
        pass

    def is_empty(self) -> bool:
        '''
        Méthode permettant de tester si la file est vide
        return : True si la file est vide, False sinon
        '''
        pass

    def enqueue(self, donnee):
        '''
        Méthode permettant d'ajouter donnee en fin de file
        param donnee : donnée à ajouter
        '''
        pass

    def dequeue(self):
        '''
        Méthode qui supprime et renvoie le premier élément de la file
        return : donnée en début de file
        '''
        pass

    def first(self):
        '''
        Méthode permettant d'accéder l'élément en début de file mais sans le supprimer
        return : donnée en début de file
        '''
        pass

    def size(self) -> int:
        '''
        Méthode renvoyant le nombre d'éléments de la file
        return : nombre d'éléments de la file
        '''
        pass
In [ ]:
class File(implements(InterfaceFile)):
    def __init__(self):
        self.liste = []
    
    def is_empty(self) -> bool:
        return len(self.liste) == 0
    
    def enqueue(self, donnee):
        self.liste.append(donnee)
    
    def dequeue(self):
        if len(self.liste) == 0:
            return None
        return self.liste.pop(0)
    
    def first(self):
        if len(self.liste) == 0:
            return None
        return self.liste[0]
    
    def size(self) -> int:
        return len(self.liste)
    
    def display(self):
        if len(self.liste) == 0:
            print('None')
        else:
            res = ''
            for donnee in reversed(self.liste):
                res += str(donnee) + ' '
            print(res)

Q3. Tester le bon fonctionnement des méthodes de la classe File sur les instructions de la question 1.

In [ ]:
f = File()
f.enqueue(5)
f.enqueue(6)
f.enqueue(7)
f.enqueue(10)
f.display()
print('File vide ?', f.is_empty())
print('Premier :', f.first())
print('Taille :', f.size())
print('On défile :', f.dequeue())
f.display()
print('File vide ?', f.is_empty())
print('Premier :', f.first())
print('Taille :', f.size())
print('On défile :', f.dequeue())
print('On défile :', f.dequeue())
print('On défile :', f.dequeue())
f.display()
print('File vide ?', f.is_empty())
print('Premier :', f.first())
print('Taille :', f.size())