Introduction

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.

En effet en manipulant les tableaux en python, vous avez certainement remarqué qu'il y a deux syntaxes pour appeler des fonctions :

tableau = [1, 3, 5, 8]
taille = len(tableau)
tableau.append(11)
  • le calcul de la longueur du tableau se fait par l'appel à la fonction len() avec une syntaxe identique aux foncitons que vous avez l'habitude d'écrire.
  • l'ajout d'un élément dans le tableau est un peu différent car la fonction append semble provenir du tableau lui même : dans ce cas, on ne parle pas de fonciton mais de méthode associée à l'objet tableau.

Un objet est une structure de donnée qui intègre des variables (que l'on nomme propriétés) et des fonctions (que l'on nomme méthodes). Nous allons voir l'intérêt de cette approche, omniprésente dans Python, en particulier lorsqu'on développe des interfaces graphiques, mais avant quelques petits repères historiques et éléments de contexte

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.

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'interaction entre les variables et les fonctions n'est pas toujours simple à gérer : les variables locales, globales, les effets de bords que provoquent certaines fonctions qui modifient des variables globales sont souvent source de bugs difficiles à déceler.

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

Création d'une classe

Nous allons voir un premier exemple simple basé sur la notion de pile vue dans une séquence précédente.

Une pile possède un comportement différent d'un tableau. On a utilisé un tableau pour simuler le comportement d'une pile mais faisant cela, on peut être tenté d'utiliser des fonctionnalités du tableau qui ne sont pas possibles avec une vraie pile comme accéder au dernier élément de la pile en faisant pile[0].

Pour y remédier nous allons créer un objet Pile qui se comportera exactement comme on le souhaite. Un objet se définit dans une classe qui va nous permettre de définir les propriétés et les méthodes que l'on souhaite intégrer à notre objet Pile.

classe Pile

Notre classe Pile va nous permettre de définir le modèle de l'objet que l'on souhaite créer. Ce modèle possèdera

  • 2 propriétés (variables intégrées à l'objet)
    • longueur : la longueur de la pile
    • sommet : la valeur du sommet de la pile
  • 2 méthodes (fonctions agissant sur cet objet)
    • empile(v) : empile la valeur v sur le sommet de la pile
    • depile() : sort une valeur de la pile et la renvoie.

Avec ces caractéristiques nous avons donc défini le prototype de notre pile.

Voyons en pratique comment cela se passe.

Définition de la classe

# Définir une classe - pour le moment vide
class Pile():
    pass

Pour le moment, on a créé une classe Pile.

On peut voir la classe comme le modèle de fabrication de l'objet. Ce n'est pas un objet réel, juste une manière de décrire comment il est constitué et comment il se comporte.

Une classe seule ne sert à rien. C'est un peu comme voir le nouveau modèle du smartphone de vos rèves sur un site de commerce en ligne : Vous voyez à quoi il ressemble, ses caractéristiques, son prix, ses fonctionnalités... mais vous ne le possédez pas !

Vous allez donc craquer et passer la commande. Quelques jours plus tard, vous allez posséder l'objet réel, le tenir dans vos mains, le manipuler. Vous avez créé ce qu'on appelle une instance de la classe.

# Créer une instance de l'objet Pile
ma_pile = Pile()
ma_pile.longueur = 1
ma_pile.sommet = 2
autre_pile = Pile()

Vous avez convaincu quelques uns de vos camarades qui ont aussi commandé le même modèle de smartphone. Ils vont aussi posséder leur propre instance. Ces smartphones fonctionneront de la même manière que le votre car fabriqué à partir des mêmes plans, mais ne possèderont pas les mêmes données : vos photos ou vos apps sont propres à votre instance de votre téléphone et n'apparaîtront pas sur celles de vos amis.

ma_pile.taille
autre_pile.taille

Nous avons créé deux instances de la classe Pile : ma_pile et autre_pile. ma_pile possède à présent deux propriétés : longueur et sommet. Ces propriétés n'existent pas sur aure_pile car l'initialisation de ces propriétés est faite en dehors de la classe, ce qui n'est pas une bonne chose : nous voulons que toutes nos piles soient fabriquées sur le même modèle et donc initialiser les propriétés à l'intérieur de la classe.

Voici comment procéder :

class Pile():
    """Implémentation basique d'une pile"""
    def __init__(self):
        """Initialisation de l'instance"""
        
        # Initialisation des propriétés
        self.longueur = 0
        self.sommet = None

Pour initialiser les propriétés, nous avons créé une méthode spéciale à l'intérieur de la classe : la méthode init(). Le nom de cette méthode lancée automatiquement à la création de chaque instance est imposé par Python et ne peut être changé. Attention aux 2 __ à suivre.

Le mot clé self désigne une instance de la classe - imaginez le remplacer par ma_pile ou autre_pile. Puisqu'au moment de concevoir ma classe, ces instances n'existent pas encore, le mot self a été introduit. Il est important de ne pas oublier le self car sinon longueur et sommet seront des variables locales à la fonction init() ce qui n'est pas du tout le but recherché ici !

Recréons à présent des instances de Pile et commençons à saisir des données :

ma_pile = Pile()
autre_pile = Pile()

ma_pile.longueur = 1
ma_pile.sommet = 2
print(ma_pile.longueur)
print(autre_pile.longueur)

Tout fonctionne comme prévu : mes deux instances possèdent les mêmes propriétés mais chacune possède ses valeurs qui lui sont propres.

Il est temps de définir nos méthodes. Commençons pas empile. La définition d'une méthode est similaire à la définition d'une fonction à deux détails près

  • les méthodes sont définies à l'intérieur d'une classe
  • le premier paramètre d'une méthode est toujours self

Pour empiler des valeurs dans ma pile je vais avoir besoin d'une structure qui mémorise les données de ma pile. Je vais donc créer une propriété cachée __reste qui contiendra toutes les valeurs de ma pile autre que le sommet. Les 2 __ sont une convention de nommage et signifie que la propriété ou la méthode n'a pas vocation à être appelée à l'extérieur de la définition de la classe, d'où la qualification de cachée.

class Pile():
    """Implémentation basique d'une pile"""
    def __init__(self):
        """Initialisation de l'instance"""
        
        # Initialisation des propriétés publiques
        self.longueur = 0
        self.sommet = None # None signifie que la pile est vide
        
        # Initialisation du reste de la pile
        self.__reste = []
    
    def empile(self, valeur):
        """empile la valeur passée en paramètre"""
        
        if self.longueur > 0:
            # Le sommet de la pile passe dans le reste
            self.__reste.append(self.sommet)
        # le nouveau sommet est la valeur qu'on empile
        self.sommet = valeur
        # La longueur de la pile augmente de 1
        self.longueur += 1
ma_pile = Pile()
ma_pile.empile(3)
ma_pile.empile(5)
print(ma_pile.sommet)
print(ma_pile.longueur)

En lisant la définition de la méthode empile, vous serez attentif aux points suivants :

  • le premier paramètre est self, valeur arrive en second
  • lorsqu'on invoque la méthode empile, on ne passe pas self, on passe juste valeur.
  • à chaque fois qu'on fait appel à une propriété, on utilise le préfixe self.

A vous de jouer

L'implémentation de notre pile n'est pas terminée. Vous allez devoir à présent implémenter la méthode depile(). Celle-ci ne prend pas de paramètres (a part bien sûr self que vous n'oublierez pas !) et renvoie la valeur qui a été sorti de la pile. Vous serez attentif

  • à modifier la propriété longueur
  • à ne pas provoquer d'erreur si il n'y a rien dans la pile. Dans ce cas, vous renverrez None.
class Pile():
    """Implémentation basique d'une pile"""
    def __init__(self):
        """Initialisation de l'instance"""
        
        # Initialisation des propriétés publiques
        self.longueur = 0
        self.sommet = None # None signifie que la pile est vide
        
        # Initialisation du reste de la pile
        self.__reste = []
    
    def empile(self, valeur):
        """empile la valeur passée en paramètre"""
        
        if self.longueur > 0:
            # Le sommet de la pile passe dans le reste
            self.__reste.append(self.sommet)
        # le nouveau sommet est la valeur qu'on empile
        self.sommet = valeur
        # La longueur de la pile augmente de 1
        self.longueur += 1
        
    # YOUR CODE HERE
    raise NotImplementedError()
# Testez votre classe dans cette cellule
ma_pile = Pile()
ma_pile.empile(3)
# Vérification du fonctionnement de la classe Pile
ma_pile = Pile()
ma_pile.empile(3)
ma_pile.empile(5)
assert ma_pile.sommet == 5
assert ma_pile.longueur == 2
assert ma_pile.depile() == 5
assert ma_pile.longueur == 1
assert ma_pile.depile() == 3
assert ma_pile.longueur == 0