Vocabulaire de la programmation objets

Objectifs :

  • Écrire la définition d’une classe.
  • Accéder aux attributs et méthodes d’une classe.

Les classes font partie d'un paradigme (méthode) de programmation appelé programmation orientée objet (POO). La programmation orientée objet essaie de simuler la réalité à l'aide d'objets. Elle se concentre sur la création de types d'objets réutilisables appelés classes.
Lorsque l'on souhaite utiliser une classe dans un programme, on crée alors un objet à partir de cette classe, d'où l'expression « orienté objet ». Chaque instance peut avoir ses propres attributs, ce qui définit son état. Une instance peut aussi avoir des méthodes (définies par la classe de l'instance) pour modifier son état.

1. Types natifs Python

In [ ]:
 
In [ ]:
nbre = 1.7
In [ ]:
type(nbre)
In [ ]:
verite = True
In [ ]:
type(verite)
In [ ]:
# Une chaîne de caractères
ma_chaine = "abracadabra"
In [ ]:
type(ma_chaine)
In [ ]:
ma_chaine.upper()
In [ ]:
# Une nouvelle liste
ma_liste = [1, 3]
In [ ]:
type(ma_liste)
In [ ]:
ma_liste.append(5)
In [ ]:
ma_liste
In [ ]:
ma_liste.insert(1, "a")
In [ ]:
ma_liste
In [ ]:
# Un nouveau tuple
mon_tuple = ("a", "b", "c", "a")
In [ ]:
type(mon_tuple)
In [ ]:
mon_tuple.count("a")
In [ ]:
# Un nouveau dictionnaire
mon_dico = {'one': 1, 'two': 2, 'three': 3}
In [ ]:
type(mon_dico)
In [ ]:
cles = list(mon_dico.keys())
In [ ]:
print(cles)

etc.
Les objets (mon_string, ma_liste, mon_tuple, mon_dico) sont des instances de chacune de leur classe.
Ces objets créés héritent des propriétés (attributs) et des comportements (méthodes) propres à la classe.
Par exemple, on peut mesurer la longueur d'une séquence (str, tuple, list) à l'aide de la fonction len(), insérer un objet dans une liste à l'aide la méthode append(), récupérer les clés d'un dictionnaire grâce à la méthode keys()...

2. Types définis par l'utilisateur

Un type défini par l'utilisateur est également appelé une classe.

In [ ]:
class Point():
    """
        Représente un point dans le plan
    """
    pass

L'objet classe est comme une usine de création d'objets. Pour créer un point, on appelle Point comme s'il s'agissait d'une fonction.

In [ ]:
#Créer le point A
A = Point()
In [ ]:
print(A)

La procédure print() n'est pas encore définie pour notre nouvelle classe Point !
La valeur de retour est une référence à un objet Point (dont l'adresse mémoire est 0x000001ABEA09E6A0), que nous identifions grâce à A.
La création d'un nouvel objet s'appelle instanciation, et l'objet créé est une instance de la classe.

In [ ]:
B = Point()
In [ ]:
print(B)

2.1. Attributs d'instance - attributs de classe

Python est un langage de programmation orienté objet, ce qui signifie qu'il fournit des fonctionnalités qui prennent en charge la programmation orientée objet.

Créons une classe Personne

In [ ]:
class Personne:
    """
        Modélise une personne qui fréquente le lycée.
    """
    # C'est l'usine à fabriquer des personnes qui fréquentent le lycée
    pass
In [ ]:
#Un autre exemple
class Fusee:
    """
    Modélise une fusée dans un jeu
    ou une simulation en physique.
    """
    pass

2.1.1. Attributs d'instance : La méthode __init__

Les propriétés que tous les objets (personnes) issus de la classe (l'usine Personne) doivent avoir sont définies dans une méthode spéciale (méthode dunder) appelée __init__ (deux caractères de soulignement, suivis de init, puis de deux autres traits de soulignement).
Chaque fois qu'un nouvel objet personne est instancié (est créé), __init__() définit l'état initial de l'objet en attribuant les valeurs des propriétés de l'objet. Autrement dit, il initialise chaque nouvelle instance de la classe. On l'appelle, par abus de langage, le constructeur de la classe.

In [ ]:
class Point():
    """
        Représente un point dans le plan
    """
    def __init__(x, y):
        self.x = x
        self.y = y
In [ ]:
class Personne:
    """
        Représente une personne qui fréquente le lycée ;
        chacune ayant un prénom, un nom et un âge.
    """
    ecole = "Lycée NSI"         #Attribut (variable) de classe
        
    def __init__(self, prenom, nom, age) -> None:
        print("Une nouvelle personne vient d'intégrer le lycée !")
        self.prenom = prenom
        self.nom = nom
        self.age = age

Les attributs créés dans __init__() sont appelés attributs d'instance (des données locales). La valeur d'un attribut d'instance est spécifique à une instance particulière (personne) de la classe Personne : tous les objets (personnes) ont un prénom, un nom et un âge mais, les valeurs des attributs prenom, nom et age sont propres à chaque personne.

2.1.2. Attributs de classe

Les attributs de classe sont des attributs qui ont la même valeur pour toutes les instances de classe. On peut définir un attribut de classe en affectant une valeur à un nom de variable en dehors de la méthode __init__().
Par exemple, toutes les personnes (toutes les instances) créées sont du lycée Nsi (attribut de classe).

In [ ]:
#Créer une instance (un élève) 
personne1 = Personne("Moïse", "Dupond", 16)
In [ ]:
isinstance(personne1, Personne)
In [ ]:
personne2 = Personne("Ben", "Achraf", 15)
In [ ]:
#L'objet personne2 est une instance de la classe Personne, Vrai ou Faux ?
isinstance(personne2, Personne)
In [ ]:
#Les instances personne1 et personne2 sont les mêmes, Vrai ou Faux ?
personne1 == personne2
In [ ]:
#attribut d'instance
personne1.nom
In [ ]:
#attribut d'instance
personne2.age
In [ ]:
#attribut de classe
personne1.ecole
In [ ]:
#attribut de classe
personne2.ecole
In [ ]:
#Un autre exemple
class Fusee:
    """
    Modélise une fusée dans un jeu
    ou une simulation en physique.
    """
    def  __init__ (self, x=0, y=0): 
        # Chaque fusée a une position (x ; y).
        print("Une fusée vient d'être créée !")
        self.x = x 
        self.y = y
In [ ]:
# Créez une instance de Fusée
ma_fusee = Fusee(1, 25)
print(ma_fusee)

Modifier des attributs

L'un des principaux avantages de l'utilisation de classes pour organiser les données est que les instances sont garanties d'avoir les attributs que l'on attend. Ainsi, toutes les instances ont des attributs prénom, nom et age.
Toutefois, bien que l'existence des attributs soit garantie, leurs valeurs peuvent être modifiées dynamiquement par l'utilisateur final !

In [ ]:
personne1.age = 18
In [ ]:
personne1.age
In [ ]:
personne1.ecole = "Lycée Informatique"
In [ ]:
del personne1.nom
In [ ]:
personne1.nom

Espace de noms

In [ ]:
#espace de noms
vars(Personne)
In [ ]:
Personne.__dict__
In [ ]:
Personne.__dict__["ecole"]
In [ ]:
#espace de noms
vars(personne1)
In [ ]:
#espace de noms
vars(personne2)
In [ ]:
#espace de noms
vars(Personne)

L'un des principaux avantages de l'utilisation de classes pour organiser les données est que les instances sont garanties d'avoir les attributs que l'on attend. Ainsi, toutes les instances ont des attributs prénom, nom et age.
Toutefois, bien que l'existence des attributs soit garantie, leurs valeurs peuvent être modifiées dynamiquement par l'utilisateur final !

In [ ]:
personne1.age = 18
In [ ]:
personne1.age
In [ ]:
personne1.ecole = "Lycée Informatique"
In [ ]:
del personne1.nom
In [ ]:
personne1.nom

2.2. Méthodes (comportements) d'instance

2.2.1. Définition

Les méthodes d'instance sont des fonctions définies à l'intérieur d'une classe et ne peuvent être appelées qu'à partir d'une instance de cette classe. Tout comme __init__(), le premier paramètre d'une méthode d'instance est toujours self. L'appel d'une méthode se fait à l'aide de la notation pointée.

In [ ]:
class Personne:
    """
    Modélise une personne du lycée ;
    chacune a pour attributs un prénom, un nom et un age.
    """
    ecole = "Lycée Nsi" #variable de classe (variable statique)
    
    def __init__(self, prenom, nom, age):
        assert isinstance(age, int)
        self.prenom = prenom
        self.nom = nom
        self.age = age
        self.amis = []
        
    # Une méthode d'instance
    def presenter(self):
        return f"{self.prenom} {self.nom} agé(e) de {self.age} ans."
    
    def ajouter_amis(self, nom):
        if nom not in self.amis:
            self.amis.append(nom)
In [ ]:
pers1 = Personne("Margot", "Yacine", 15)
In [ ]:
print(Margot) 
In [ ]:
pers1.presenter()
In [ ]:
pers1.ajouter_amis("Alice")
In [ ]:
pers1.amis
In [ ]:
#Un autre exemple
class Fusee:
    """
    Modélise une fusée dans un jeu
    """
    def  __init__ (self, x = 0, y = 0): 
        # Chaque fusée a une position (x ; y)
        if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):
            raise ValueError("x et y doivent être des nombres")
        self.x = x 
        self.y = y
        
    def  mettre_en_mvt(self, pas_horizontal = 0, pas_vertical = 1):
        # Incrémentez la position y de la fusée.
        self.x += pas_horizontal
        self.y += pas_vertical
In [ ]:
ariane = Fusee(1, 2)
In [ ]:
ariane.mettre_en_mvt()
In [ ]:
ariane.y

À retenir

instance.methode() # appel sans argument
instance.methode(arg1, arg2) # appel avec arguments
retour = instance.methode() # récupérer la valeur de retour

Exercice :

  • Créer, en compréhension, une flotte de 5 fusées et les stocker dans une liste, ma_flotte.
  • Montrer, par l'adresse mémoire, que chaque fusée est un objet séparé
  • Faire monter la première fusée

2.2.2. Les méthodes __str__ et __repr__

__str__ ( __repr__ ) est une méthode spéciale, comme __init__, qui est censée renvoyer une représentation sous forme de chaîne de caractères d'un objet.
Autrement dit, lorsque l'on utilise l'instruction print() sur un objet, il invoque la méthode str (repr pour le développeur).

In [ ]:
repr("a")
In [ ]:
repr(1)
In [ ]:
str("a")
In [ ]:
str(1)
In [ ]:
class Personne:
    """
    Modélise une personne du lycée ;
    chacune a pour attributs un prénom, un nom et un age.
    """
    ecole = "Lycée Nsi" #variable de classe (variable statique)
    
    def __init__(self, prenom, nom, age):
        assert isinstance(age, int)
        self.prenom = prenom
        self.nom = nom
        self.age = age
        self.amis = []
        
    # Une méthode d'instance
    def presenter(self):
        return f"{self.prenom} {self.nom} agé(e) de {self.age} ans."
    
    def ajouter_amis(self, nom):
        if nom not in self.amis:
            self.amis.append(nom)

    def __str__(self):
        return f"{self.prenom} {self.nom} agé(e) de {self.age} ans."

    def __repr__(self):
        return f"Personne(prénom = {self.prenom}, nom = {self.nom}, âge = {self.age}, amis = {self.amis})"
In [ ]:
pers1 = Personne("Margot", "Yacine", 15)
In [ ]:
print(pers1)
In [ ]:
pers1
In [ ]:
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"M({self.x} ; {self.y})"
        
    def __repr__(self):
        return f"Point({self.x}, {self.y})"
In [ ]:
M = Point(1, 2)
In [ ]:
M
In [ ]:
print(M)

2.2.3. Autres méthodes

In [ ]:
class Personne:
    """
    Représente un personne du lycée ;
    attributs : nom, age, exergue.
    """
    #variables de classe
    ecole = "Lycée Saint Pierre"     
    total = 0
    
    def __init__(self, prenom, nom, age):
        self.prenom = prenom
        self.nom = nom
        self.age = age
        self.nom_prenom = self.nom + self.prenom
        Personne.total += 1
    # méthode d'instance
    def __str__(self):
        return f"{self.prenom} {self.nom} agé(e) de {self.age} ans."
    
    # Une méthode d'instance    
    def __repr__(self) -> str:
        return f"{type(self).__name__}(prénom = {self.prenom}, nom = {self.nom}, âge = {self.age})"

    #Une autre méthode d'instance
    def __len__(self):
        """
        Renvoie le nombre de lettres contenues dans nom_prenom.
        """
        return len(self.nom_prenom)
    
    #Encore une autre méthode
    def __contains__(self, lettre):
        """
            Renvoie true si lettre est dans self.nom_prenom, false dans le cas contraire.
        """
        return lettre in self.nom_prenom
In [ ]:
pers1 = Personne("Margot", "Yacine", 15)
pers2 = Personne("Mathieu", "Laure", 10)
In [ ]:
len(pers1)
In [ ]:
Personne.total
In [ ]:
"e" in pers1
In [ ]:
#Un autre exemple
class Fusee:
    """
    Modélise une fusée dans un jeu.
    """
    def  __init__ (self, x = 0, y = 0): 
        # Chaque fusée a une position (x ; y)
        assert isinstance(x, (int, float)) and isinstance(y, (int, float))#x et y sont soit des int soit des float
        self.x = x 
        self.y = y
        
    def  mettre_en_mouvement(self, pas_horizontal = 0, pas_vertical = 1):
        # Incrémentez la position y de la fusée.
        self.x += pas_horizontal
        self.y += pas_vertical
    
    def get_distance(self, une_autre_fusee):
        import math
        """Renvoie la distance de cette fusée à une_autre_fusee."""
        distance = math.sqrt((self.x - une_autre_fusee.x)**2 + (self.y - une_autre_fusee.y)**2)
        return distance
    
    def distance_securite(self, une_autre_fusee):
        """Vérifie la distance de sécurité avec une fusée tierce."""

2.3. Encapsulation

2.3.1. Définition

L'un des rôles principaux d'une classe est d'encapsuler (de cacher, d'envelopper) les données et les détails d'implémentation internes d'un objet. L'objectif étant de protéger les objets de la classe des usagers qui seraient tentés de modifier directectement leur état.
La conséquence : les objets ne sont accessibles qu'au travers de leur interface c'est-à-dire via leurs attributs et méthodes.

In [ ]:
class Personne:
    """
        Représente une personne qui fréquente le lycée ;
        chacune ayant un prénom, un nom et un âge.
    """
    ecole = "Saint Pierre Institut"         #Attribut (variable) de classe
        
    def __init__(self, prenom, nom, age):
        self.prenom = prenom
        self.nom = nom
        self.age = age
        
    # méthode d'instance
    def __str__(self):
        return f"{self.prenom} {self.nom} agé(e) de {self.age} ans."
    
    # Une méthode d'instance
    def __repr__(self) -> str:
        return f"{type(self).__name__}(prénom = {self.prenom}, nom = {self.nom}, âge = {self.age})"  
In [ ]:
personne1 = Personne("Jean", "Dupont", 25)
personne2 = Personne("Marie", "Martin", 30)

En Python, par défaut, les attributs sont publics, donc modifiables par l'utilisateur final.

In [ ]:
personne2.prenom = "Aïssatou"
In [ ]:
print(personne2)

Mais quelquefois, on ne souhaite pas que l'usager altère certains attributs d'une instance :

  • en préfixant son nom d'un caractère de soulignement bas.

In [ ]:
class Personne:
    """
        Représente une personne qui fréquente le lycée ;
        chacune ayant un prénom, un nom et un âge.
    """
    ecole = "Lycée Nsi"         #Attribut (variable) de classe
        
    def __init__(self, prenom, nom, age):
        self._prenom = prenom
        self._nom = nom
        self._age = age
    
    # méthode d'instance
    def __str__(self):
        return f"{self._prenom} {self._nom} agé(e) de {self._age} ans."
    
    # Une méthode d'instance
    def __repr__(self) -> str:
        return f"{type(self).__name__}(prénom = {self.prenom}, nom = {self.nom}, âge = {self.age})"
    
In [ ]:
personne1 = Personne("Jean", "Dupont", 25)
personne2 = Personne("Marie", "Martin", 30)
In [ ]:
personne1.prenom = "Yves"
In [ ]:
print(personne1)
In [ ]:
personne1._prenom = "Yves"
In [ ]:
print(personne1)

le tiret bas ( _ ) qui préfixe une variable permet simplement de signaler aux autres programmeurs que ladite variable est à usage interne ; mais elle demeure accessible et modifiable.

In [ ]:
personne1._nom
  • en préfixant son nom de deux caractères de soulignement bas : le name mangling
In [ ]:
class Personne:
    """
        Représente une personne qui fréquente le lycée ;
        chacune ayant un prénom, un nom et un âge.
    """
    ecole = "Saint Pierre Institut"         #Attribut (variable) de classe
    nbre_personnes = 0
        
    def __init__(self, prenom, nom, age):
        self.__prenom = prenom
        self.__nom = nom
        self.__age = age
        type(self).nbre_personnes += 1
        
    # méthode d'instance
    def __str__(self):
        return f"{self.__prenom} {self.__nom} agé(e) de {self.__age} ans."
    
    # Une méthode d'instance
    def __repr__(self) -> str:
        return f"{type(self).__name__}(prénom = {self.prenom}, nom = {self.nom}, âge = {self.age})"
    
In [ ]:
personne1 = Personne("Jean", "Dupont", 25)
personne2 = Personne("Marie", "Martin", 30)
In [ ]:
personne1.__nom
In [ ]:
dir(personne1)

Malgré le double tiret bas ( _ ) qui préfixe une variable, elle reste toujours accessible de l'extérieur. Mais le code est un peu plus complexe que précedemment.

In [ ]:
personne1._Personne__nom
In [ ]:
personne1._Personne__nom = "Françoise"
In [ ]:
print(personne1)
In [ ]:
repr(personne1)
In [ ]:
#Créer une instance (un élève) 
personne1 = Personne("Mathieu", "Dupond", 16)
In [ ]:
personne1._nom
In [ ]:
personne1._age
In [ ]:
print(personne1)
In [ ]:
#espace de nom
vars(personne1)

2.5. Héritage

Une classe permet de créer des objets (instances). La classe et ses objets sont liés par une relation d'héritage. Autrement dit, les objets reçoivent tous les attributs et méthodes définis dans la classe.
Par ailleurs, l'héritage permet de créer un sous-ensemble d'une classe. Ainsi, il permet d'étendre une classe, notamment par

  • l'ajout de nouvelles méthodes
  • la redéfinition de certaines des méthodes existantes
  • l'ajout de nouveaux attributs aux instances
In [ ]:
class Eleve(Personne):
    def __init__(self, prenom, nom, age, niveau):
        super().__init__(prenom, nom, age) # On charrie l'initialisation de la classe parent
        self.niveau = niveau  
In [ ]:
eleve1 = Eleve("Claude", "Adam", 16, "première")
In [ ]:
isinstance(eleve1, Eleve)
In [ ]:
isinstance(eleve1, Personne)
In [ ]:
eleve1._prenom
In [ ]:
eleve1.ecole
In [ ]:
print(eleve1)

2.6. Polymorphisme

Le polymorphisme est la propriété de pouvoir traiter différents types d'objets de la même manière et de faire en sorte que chacun de ces objets réagisse à sa manière.
En POO, le polymorphisme signifie qu'une même méthode appliquée à des objets de classes différentes liées par une relation d'héritage permet d'obtenir des résultats différents mais appropriés.
Les méthodes str et repr, par exemple, sont attachées aussi bien aux objet Fusee qu'aux objets Navette.

2.7. Modules et classes

Plus le fichier contenant les classes se densifient plus il devient difficile de le manipuler. Aussi est-il conseillé d'enregistrer les classes dans un fichier à part, sous forme de module puis de les importer quand on en a besoin.
On peux enregistrer une ou plusieurs classes dans un fichier.
Exemple :
Enregistrer la classe Personne dans un fichier nommé personne.py.

In [ ]:
#Dans un nouveau fichier contenu dans le même répertoire que personne.py
from personne import Personne
une_personne = Personne("Ben", "Yacine", 25)
print(une_personne)

Exercice :

  • À partir de la classe personne, ci-après, créer la classe Professeur où chaque professeur enseigne une matière.
  • On considère, ci-après, les classes Fusee et Navette.
In [ ]:
#Un autre exemple
class Fusee:
    """
    Modélise une fusée dans un jeu
    """
    def  __init__ (self, x = 0, y = 0): 
        # Chaque fusée a une position (x ; y)
        assert isinstance(x, (int, float)) and isinstance(y, (int, float))#x et y sont soit des int soit des float
        self.x = x 
        self.y = y
    
    def  mettre_en_mouvement(self, pas_horizontal = 0, pas_vertical = 1):
        # Incrémentez la position y de la fusée.
        self.x += pas_horizontal
        self.y += pas_vertical
    
    def get_distance(self, une_autre_fusee):
        import math
        """Renvoie la distance de cette fusée à une_autre_fusee."""
        
    
    def __str__(self):
        return f"Je me trouve à l'altitude {self.y} et horizontalement à {self.x} de mon point de départ."
    
    def __repr__(self):
        return f"Fusee({self.x}, {self.y})"
In [ ]:
#un autre exemple
class Navette(Fusee):
    """Modélise une navette spatiale (fusée réutilisable).""" 
    def __init__(self, x=0, y=0, nbre_vols=0):
        super().__init__(x, y)
        self.nbre_vols = nbre_vols
        
    def __str__(self):
        return f"Je suis une navette spatiale et me trouve à l'altitude {self._y} et horizontalement à {self._x} de mon point de départ."
    
    def __repr__(self):
        return f"Navette({self._x}, {self._x})"