Chapitre 2 : POO et classes.

🕐 Historique :

  • Les débuts (années 1960) : les toutes premières idées de la programmation orientée objet apparaissent avec le langage Simula, conçu en Norvège par Ole-Johan Dahl et Kristen Nygaard. On y trouve déjà les notions de classe et d'héritage.
  • La diffusion (années 1970-1980) : au centre de recherche Xerox PARC, l'équipe d'Alan Kay met au point Smalltalk, considéré comme le premier langage entièrement orienté objet. Il met en avant des idées essentielles comme l'encapsulation, l'héritage et le polymorphisme.
  • La généralisation (années 1990) : des langages comme C++ puis Java rendent la POO incontournable dans le monde professionnel. Java séduit notamment grâce à son principe « écrire une fois, exécuter partout ».
  • Aujourd'hui : la POO est présente dans la plupart des langages courants (Python, Ruby, PHP, etc.). Chacun l'adapte à sa manière, et elle continue d'évoluer.

La programmation orientée objet est ainsi devenue l'une des grandes façons de concevoir un programme : elle aide à structurer les gros projets et influence la manière dont les développeurs réfléchissent pour résoudre un problème.

1. Paradigmes de programmation

Un paradigme de programmation est la façon dont un programme (ou un bout de programme) est construit.

2. Exemples

Vous avez vu jusque-là deux paradigmes de programmation : la programmation impérative et la programmation fonctionnelle.

Voici deux façons de compter le nombre de fois où la lettre "c" est présente dans 'coucou' :

3. POO

Les lettres POO signifient Programmation Orientée Objet. C'est un paradigme de programmation.

Cela désigne la possibilité dans un langage informatique de créer divers objets avec des attributs et des méthodes associées.

4. Les classes, attributs, méthodes et instances

En Python, une classe sert de modèle pour créer des objets. Chaque objet créé à partir d’une classe est appelé une instance.

Une classe contient :

class Joueur:
    def __init__(self, pseudo):
        self.pseudo = pseudo        # attribut
        self.points_de_vie = 100    # attribut

    def change_pseudo(self, nouveau_pseudo):   # méthode
        self.pseudo = nouveau_pseudo

    def get_pseudo(self):   # méthode
        return self.pseudo

# Création d’une instance
joueur1 = Joueur("Alice")
print(joueur1.pseudo)        # accès à un attribut
print(joueur1.get_pseudo())  # appel d’une méthode

Ici, Joueur est une classe, joueur1 une instance, pseudo et points_de_vie sont des attributs, et change_pseudo, get_pseudo des méthodes.

5. Exercice : classe CompteBancaire (dépôt, retrait, solde).

Dans cet exercice, vous allez créer une classe CompteBancaire pour simuler la gestion d'un compte bancaire :

  • Chaque compte a un titulaire et un solde initial.
  • Implémentez une méthode pour déposer de l'argent sur le compte.
  • Implémentez une méthode pour retirer de l'argent du compte, en veillant à ne pas autoriser un solde négatif.
  • Implémentez une méthode pour afficher le solde actuel du compte.
  1. Complétez la classe CompteBancaire.
  2. Créez une instance avec le titulaire "Jean Dupont" et un solde initial de 1000 euros.
  3. Effectuez un dépôt de 250 euros, puis un retrait de 500 euros et affichez le solde après ces opérations.
  4. Essayez de retirer un montant supérieur au solde disponible et gérez cette situation dans votre code.
class CompteBancaire: def __init__(self, titulaire, solde_initial): .... def deposer(self, montant): .... def retirer(self, montant): if ....: self.solde -= montant else: print("Fonds insuffisants") def afficher_solde(self): ... # Exemple d'utilisation compte = CompteBancaire("Jean Dupont", 1000) compte.deposer(250) compte.retirer(500) compte.afficher_solde() compte.retirer(2000) # Retrait impossible


    
>>>

6. Public / privé

Dans la plupart des langages (Java, C++...), la commande private interdit l’accès depuis l’extérieur.

En Python, il n’y a pas de vrai private : on utilise _nom (interne) ou __nom (privé par convention).

7. Exemple :

class Joueur:
    def __init__(self,nom):
        self.pseudo=self.__crea_nom(nom)
        self.__point_de_vie=100
    
    def affiche_pv(self):
        return self.__point_de_vie

    def __crea_nom(self,mot):
        if mot=="aucun":
            return "Anonyme"
        else:
            return mot

Dans le code ci-dessus :

8. Exercice : classe Livre (attributs privés et validation).

Dans cet exercice, vous allez créer une classe Livre pour gérer les informations d'une bibliothèque :

  • Le titre et l'auteur sont des attributs publics.
  • L'annee_publication est un attribut privé, vérifié lors de l'initialisation pour s'assurer qu'elle est inférieure ou égale à 2030.
  • La méthode affiche_info() doit afficher le titre, l'auteur et l'année de publication.
  • La méthode privée __verifie_annee() est utilisée pour valider l'année.
  1. Complétez la définition de la classe Livre.
  2. Créez deux instances de Livre avec les informations suivantes :
    • Livre 1 : Titre "Les Misérables", Auteur "Victor Hugo", Année de publication 1862.
    • Livre 2 : Titre "1984", Auteur "George Orwell", Année de publication 1949.
  3. Utilisez la méthode affiche_info() pour afficher les informations de chaque livre.
  4. Essayez d'accéder directement à l'attribut privé __annee_publication pour observer ce qui se passe.
class Livre: def __init__(self, titre, auteur, annee_publication): self.titre = .. .. if self.__verifie_annee(annee_publication): .. else: self.__annee_publication = None def affiche_info(self): .. def __verifie_annee(self, annee): .. # Exemple d'utilisation livre1 = ..


    
>>>

9. Exercice : (sujet 20 - 2025) classe Paquet_de_cartes.

On dispose d’une classe Carte permettant de créer des objets modélisant des cartes à jouer.

Complète la classe Paquet_de_cartes ci-dessous :

  • Le constructeur doit créer un paquet de 52 cartes (13 valeurs pour chaque couleur : pique, cœur, carreau, trèfle).
  • La méthode get_carte(pos) doit renvoyer la carte à la position donnée.
  • Ajoute une assertion pour vérifier que pos est bien compris entre 0 et 51.

⭐ Exemples :

>>> jeu = Paquet_de_cartes()
>>> carte1 = jeu.get_carte(20)
>>> print(carte1.get_valeur() + " de " + carte1.get_couleur())
8 de coeur
>>> carte2 = jeu.get_carte(0)
>>> print(carte2.get_valeur() + " de " + carte2.get_couleur())
As de pique
>>> carte3 = jeu.get_carte(52)
AssertionError: paramètre pos invalide
class Carte: def __init__(self, c, v): """ Initialise les attributs couleur (1 à 4), et valeur (1 à 13). """ self.couleur = c self.valeur = v def get_valeur(self): valeurs = ["As", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Valet", "Dame", "Roi"] return valeurs[self.valeur - 1] def get_couleur(self): couleurs = ["pique", "coeur", "carreau", "trèfle"] return couleurs[self.couleur - 1] class Paquet_de_cartes: def __init__(self): """ Crée un paquet de 52 cartes classées par valeurs croissantes, d'abord pique, puis coeur, carreau et trèfle. """ .. .. .. .. def get_carte(self, pos): """ Renvoie la carte à la position pos (0 ≤ pos ≤ 51). """ .. .. # Exemple d utilisation jeu = Paquet_de_cartes() c1 = jeu.get_carte(20) print(c1.get_valeur(), "de", c1.get_couleur()) # 8 de coeur


    
>>>

10. Exercice : (sujet 41 - 2025) carré semi-magique.

Dans cet exercice, on appelle carré d’ordre n un tableau de n lignes et n colonnes dont chaque case contient un entier naturel.

Exemples :

1 7
7 1

c2

Un carré d'ordre 2

3 4 5
4 4 4
5 4 3

c3

Un carré d'ordre 3

2 9 4
7 0 3
6 1 8

c3bis

Un autre carré d'ordre 3

Un carré est dit semi-magique lorsque les sommes des éléments situés sur chaque ligne et chaque colonne sont égales.

  • Ainsi c2 et c3 sont semi-magiques car la somme de chaque ligne et chaque colonne est égale à 8 pour c2 et 12 pour c3.
  • Le carré c3bis n’est pas semi-magique car la somme de la première ligne est égale à 15 alors que celle de la deuxième ligne est égale à 10.

La classe Carre en page suivante contient des méthodes qui permettent de manipuler des carrés :

  • La méthode constructeur crée un carré sous forme d’un tableau à deux dimensions à partir d’une liste d’entiers, et d’un ordre.
  • La méthode affiche permet d’afficher le carré créé.

Exemples :

>>> liste = (3, 4, 5, 4, 4, 4, 5, 4, 3)
>>> c3 = Carre(liste, 3)
>>> c3.affiche()
[3, 4, 5]
[4, 4, 4]
[5, 4, 3]

Compléter la méthode est_semimagique qui renvoie True si le carré est semi-magique, False sinon. Puis tester la fonction est_semimagique sur les carrés c2, c3 et c3bis.

# Carrés fournis c2 = [1, 7, 7, 1] c3 = [3, 4, 5, 4, 4, 4, 5, 4, 3] c3bis = [2, 9, 4, 7, 0, 3, 6, 1, 8] class Carre: def __init__(self, liste, n): self.ordre = n self.tableau = [[liste[i + j * n] for i in range(n)] for j in range(n)] def affiche(self): """Affiche le carré""" for i in range(self.ordre): print(self.tableau[i]) def somme_ligne(self, i): """Somme des valeurs de la ligne i""" somme = 0 for j in range(self.ordre): somme = somme + self.tableau[i][j] return somme def somme_col(self, j): """Somme des valeurs de la colonne j""" somme = 0 for i in range(self.ordre): somme = somme + self.tableau[i][j] return somme def est_semimagique(self): s = self.somme_ligne(0) # test de la somme de chaque ligne for i in range(....): if self.somme_ligne(i) != ....: return .... # test de la somme de chaque colonne for j in range(....): if self.somme_col(j) != ....: return .... return .... # Exemples d'utilisation (tu peux modifier ou commenter) C2 = Carre(c2, 2) C3 = Carre(c3, 3) C3B = Carre(c3bis, 3) print(C2.est_semimagique(), C3.est_semimagique(), C3B.est_semimagique())


    
>>>

11. Exercice : (sujet 22 - 2025) Classe AdresseIP (IPv4)

On définit une classe gérant une adresse IPv4.

On rappelle qu’une adresse IPv4 est une adresse de longueur 4 octets, notée en décimale à point, en séparant chacun des octets par un point. On considère un réseau privé avec une plage d’adresses IP de 192.168.0.0 à 192.168.0.255.

On considère que les adresses IP saisies sont valides.

Les adresses IP 192.168.0.0 et 192.168.0.255 sont des adresses réservées.

Le code ci-dessous implémente la classe AdresseIP.

Compléter le code ci-dessous et instancier trois objets : adresse1, adresse2, adresse3 avec respectivement les arguments suivants : '192.168.0.1', '192.168.0.2', '192.168.0.0'.

Vérifier que :

>>> adresse1.liste_octets()
[192, 168, 0, 1]
>>> adresse1.est_reservee()
False
>>> adresse3.est_reservee()
True
>>> adresse2.adresse_suivante().adresse  # acces valide à adresse
# ici car on sait que l'adresse suivante existe
'192.168.0.3'
class AdresseIP: def __init__(self, adresse): self.adresse = ... def liste_octets(self): """renvoie une liste de nombres entiers, la liste des octets de l'adresse IP""" # Note : split découpe la chaine de caractères # en fonction du séparateur return [int(i) for i in self.adresse.split(".")] def est_reservee(self): """renvoie True si l'adresse IP est une adresse réservée, False sinon""" reservees = [ ... ] return ... def adresse_suivante(self): """renvoie un objet de AdresseIP avec l'adresse IP qui suit l'adresse self si elle existe et None sinon""" octets = ... if ... == 254: return None octet_nouveau = ... + ... return AdresseIP('192.168.0.' + ...) # Objets pour les tests / exemples adresse1 = AdresseIP('192.168.0.1') adresse2 = AdresseIP('192.168.0.2') adresse3 = AdresseIP('192.168.0.0') adresse254 = AdresseIP('192.168.0.254') adresse255 = AdresseIP('192.168.0.255')

    
>>>

12. Sujet écrit ASIE 2025 : complétion d'une classe Date (getters et setters).

À faire dans le cahier.

Pour travailler sur des dates, on a créé la classe Date dont le code est écrit ci-dessous

class Date:
    def __init__(self, jour, mois, annee):
        self.jour = ...
        self.mois = ...
        self.annee = ...
        self.nb_jours_par_mois = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]


    def get_jour(self):
        return self.jour


    def get_mois(self):
        return self.mois

        
    def get_annee(self):
        return ...
    
        
    def set_jour(self, jour):
        self.jour = jour

        
    def set_mois(self, mois):
        self.mois = ...

        
    def set_annee(self, annee):
        self.annee = annee

        
    def est_bissextile(self):
        ...
    

Partie A : Accès et modification des données

Le constructeur de la classe Date prend en paramètres trois entiers représentant le jour, le mois et l’année, puis les affecte respectivement aux attributs jour, mois et annee.

  1. Recopier et compléter les trois premières lignes à l'intérieur du constructeur.
  2. Indiquer à quelle date correspond l’instance de la classe Date suivante :

    d = Date(1, 5, 2000)

  3. Écrire le code permettant de créer une instance d de la classe Date qui représente la date du 19 juin 2024.

  4. La méthode get_annee renvoie la valeur de l’attribut annee.

    Recopier et compléter les lignes de la méthode get_annee.

  5. La méthode set_mois modifie l’attribut mois en lui affectant la valeur passée en argument.

    Recopier et compléter les lignes de la méthode set_mois.

L’attribut nb_jours_par_mois contient une liste qui correspond au nombre de jours pour chaque mois.

Le mois de février contient généralement 28 jours mais lors des années bissextiles il en contient 29.

  1. La classe Date dispose d’une méthode est_bissextile, qui utilise uniquement l’attribut annee, et qui renvoie True si l’année de l’instance courante est bissextile et False sinon. On veut compléter la méthode __init__ pour ajuster le nombre de jours par mois pour les années bissextiles.

    Recopier et compléter les lignes suivantes :

    class Date:
        def __init__(self, jour, mois, annee):
            self.jour = ...
            self.mois = ...
            self.annee = ...
            self.nb_jours_par_mois = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
            if .... :
                self.nb_jours_par_mois[...] = 29

Partie B : sur l’année de l’instance courante

Pour déterminer le nombre de jours au cours d’une année, il faut savoir si elle est bissextile.

Une année est bissextile si elle est divisible par 4 mais pas par 100 ou si elle est divisible par 400.

  1. Écrire le code de la méthode est_bissextile.

    On rappelle qu’un entier a est divisible par l’entier n si a % n == 0.

On dote la classe Date de la méthode nb_jours_passes qui renvoie le nombre de jours passés dans l’année de l’instance courante.

def nb_jours_passes(self):
    nb_jours = self.jour
    mois = self.mois - 2
    while mois >= 0:
        nb_jours = nb_jours + self.nb_jours_par_mois[mois]
        mois = mois - 1
    return nb_jours
  1. Indiquer quel sera l’affichage en console après l’exécution des deux instructions suivantes :

    >>> d1 = Date(20, 3, 2001)
    >>> d1.nb_jours_passes()
                

On dote la classe Date de la méthode nb_jours_restants qui renvoie le nombre de jours restants dans l’année de l’instance courante, soit 366 ou 365 moins le nombre de jours déjà passés, selon que l’année est bissextile ou non.

  1. Recopier et compléter le script de la méthode nb_jours_restants ci-après :

    def nb_jours_restants(self):
        j = 365
        if ...:
            j = 366
        return j -    ...

Partie C : entre deux dates

On dote la classe Date de la méthode nb_jours_depuis qui prend en paramètre une autre instance other de la classe Date et qui renvoie le nombre de jours écoulés entre la date de l’instance other et la date de l’instance courante.

def nb_jours_depuis(self, other):
    if other.get_annee() > self.get_annee():
        return -1
    if other.get_annee() == self.get_annee():
        if other.nb_jours_passes() > self.nb_jours_passes() :
            return -1
        if other.nb_jours_passes() == self.nb_jours_passes() :
            return 0
    nb_jours = self.nb_jours_passes() + other.nb_jours_restants()
    for annee in range(other.get_annee()+1, self.get_annee()):
        d_suivant = date(1, 1, annee)
        if d_suivant.est_bissextile():
            nb_jours += 366
        else:
            nb_jours += 365
    return nb_jours

On crée les instances de la classe Date suivantes :

>>> d1 = Date(15, 6, 2024)
>>> d2 = Date(15, 6, 2024)
>>> d3 = Date(15, 7, 2024)
>>> d4 = Date(15, 6, 2025)
>>> d5 = Date(15, 6, 2022)
  1. Indiquer quels seront alors les affichages en console après l’exécution de chacune des instructions suivantes (on précise que l’année 2024 est bissextile) :
    >>> d1.nb_jours_depuis(d2)
    >>> d1.nb_jours_depuis(d3)
    >>> d1.nb_jours_depuis(d4)
    >>> d1.nb_jours_depuis(d5)
    

Le timestamp est le nombre de secondes qui se sont écoulées depuis le 1er janvier 1970 à 00h00. Il s’agit de la date de la mise en marche du système d’exploitation UNIX.

Par exemple, le 01/01/2024 à 00:00:00 correspond au timestamp de 1704063600.

  1. Recopier et compléter le code de la méthode timestamp qui renvoie le nombre de secondes qui se sont écoulées depuis le 1er janvier 1970.
    def timestamp(self):
        d = ...
        return self.nb_jours_depuis(d) * 24 * 3600
    

13. Les méthodes __str__ et __repr__ :

Par défaut, lorsqu'on affiche un objet avec print(), Python affiche quelque chose de peu lisible comme <__main__.Joueur object at 0x7f3a...>.

Pour rendre nos objets plus lisibles, on peut définir deux méthodes spéciales :

Si seul __repr__ est défini, il sert aussi de __str__.

class Joueur:
    def __init__(self, pseudo, points_de_vie):
        self.pseudo = pseudo
        self.points_de_vie = points_de_vie

    def __str__(self):
        return self.pseudo + " (" + str(self.points_de_vie) + " PV)"

    def __repr__(self):
        return "Joueur(" + repr(self.pseudo) + ", " + repr(self.points_de_vie) + ")"

joueur1 = Joueur("Alice", 100)
print(joueur1)        # Alice (100 PV)        -> utilise __str__
joueur1               # Joueur('Alice', 100)  -> utilise __repr__ (en console)

Ici, __str__ donne une description agréable à lire, tandis que __repr__ donne une représentation technique qui ressemble au code de création de l'objet.

14. Exercice : classe Fraction (méthodes __str__ et __repr__).

Dans cet exercice, vous allez créer une classe Fraction représentant une fraction avec un numerateur et un denominateur.

  • Le constructeur prend en paramètres le numérateur et le dénominateur.
  • La méthode __str__ doit renvoyer la fraction sous une forme lisible, par exemple "3/4".
  • La méthode __repr__ doit renvoyer une représentation technique, par exemple "Fraction(3, 4)".
  1. Complétez la classe Fraction.
  2. Créez une instance représentant la fraction 3/4.
  3. Affichez-la avec print() pour observer le résultat de __str__.
  4. Affichez le résultat de repr() pour observer le résultat de __repr__.

⭐ Exemples :

>>> f = Fraction(3, 4)
>>> print(f)
3/4
>>> repr(f)
'Fraction(3, 4)'
class Fraction: def __init__(self, numerateur, denominateur): self.numerateur = .. self.denominateur = .. def __str__(self): # forme lisible : "numerateur/denominateur" return .. def __repr__(self): # forme technique : "Fraction(numerateur, denominateur)" return .. # Exemple d'utilisation f = Fraction(3, 4) print(f) # 3/4 print(repr(f)) # Fraction(3, 4)


    
>>>

15. Projet : programme mettant en œuvre la programmation orientée objet.

Projet : créer un programme Python faisant intervenir les classes.

Consignes :