La programmation orientée objet (POO) en Python

Généralités

En Python, la plupart des variables qu’on manipule sont des objets.

Dans la vie courante, un objet est “quelque chose” qui a :

  • une identité

  • des caractéristiques

  • un comportement

Exemple : Ma voiture possède :

  • un identifiant → son n° d’immatriculation

  • un comportement → un ensemble d’actions réalisables. Ex. : démarrer, rouler, klaxonner …​

  • des caractéristiques → sa marque, sa cylindrée, sa couleur qui sont des caractéristiques qui ne changent pas dans le temps.
    Mais aussi le nombre de litres de carburant dans le réservoir, le kilométrage qui sont également des caractéristiques mais qui évoluent dans le temps.

En Python, c’est pareil !

Exemple :
>>> # On définit un nombre complexe dont l'identifiant est 'z'
>>> z = 1 + 2j
>>> # On affiche les caractéristiques de 'z' à savoir sa partie réeelle et sa partie imaginaire
>>> z.real
1.0
>>> z.imag
2.0
>>> # On déclenche une action sur 'z' qui consiste à lui faire calculer son conjugé
>>> z.conjugate() (1)
(1-2j)

En Python, on appelle attribut une caractéristique d’un objet.

Une action est, quant à elle, appelée méthode.

On parle également de classe pour faire référence au type Python qui servira de modèle pour construire — on dit aussi instancier — autant d’objets que nécessaire.

Exemple :
>>> type(complex)
<#class# 'type'>

Parmi les attributs, on en distingue 2 types :

  1. les attributs de classe → ce sont des attributs qui sont partagés par tous les objets de la classe ⇒ la modification d’un de ces attributs sera répercutée dans l’ensemble des objets construits à partir de cette classe

  2. les attributs d’instance → ce sont des attributs propres à chacun des objets ⇒ ils peuvent donc avoir des valeurs différentes selon l’objet

La librairie Python standard propose déjà un nombre important de classes (c’est-à-dire de types) pré-définies. Cependant, il est tout à fait possible — et même recommandé — d’en définir de nouvelles pour s’adapter à nos besoins de traitement.

Une 1ère classe

Ci-dessous figure le code d’un script Python qui définit et fait appel à une classe Fraction qui va permettre de manipuler des fractions mathématiques.

import math

# Définition de la classe
class Fraction :
    # Déclaration d'attributs d'instance
    nName = "numérateur"
    dName = "dénominateur"

    # Méthode spéciale Python prédéfinie qui est appelée automatiquement lors
    # de la création d'un objet du type de cette classe
    def __init__(self, num, den) :
        # Déclaration et initialisation d'attributs d'instance : 'n' et 'd'
        self.n = num
        self.d = den

    # Méthode utilisateur qui renvoie la valeur de l'attribut d'instance 'n'
    def getNum(self) :
        return self.n

    # Méthode utilisateur qui renvoie la valeur de l'attribut d'instance 'd'
    def getDen(self) :
        return self.d

    # Méthode utilisateur qui redéfinit la valeur des 2 attributs d'instance 'n' et 'd'
    def setFrac(self, num, den) :
        self.n = num
        self.d = den

    # Méthode spéciale Python correspondant à l'opérateur '+'
    def __add__(self, other) :
        # On calcule les numérateur et dénominateur de la somme des fractions
        # après les avoir mises sur le même dénominateur
        nSum = self.n * other.d + other.n * self.d
        dSum = self.d * other.d
        # On cherche le plus commun diviseur
        pgcd = math.gcd(nSum, dSum)
        # On simlifie le numérateur et dénominateur du résultat
        nSum /= pgcd
        dSum /= pgcd
        # On retourne la somme sous la forme d'un nouvel objet Fraction
        return Fraction(nSum, dSum)

    # Méthode spéciale Python qui est appelée par la fonction 'print()'
    def __repr__(self) :
        return f"{int(self.n)}/{int(self.d)}"

if __name__ == "__main__":

    # On instancie 2 fois la classe Fraction
    f1 = Fraction(3,4)
    f2 = Fraction(1,2)

    # On affiche le détail des fractions à partir de la valeur de ses attributs
    # de classe et d'instance
    print(f"f1 -> {f1.nName} / {f1.dName} = {f1.getNum()} / {f1.getDen()}")
    print(f"f2 -> {f2.nName} / {f2.dName} = {f2.getNum()} / {f2.getDen()}")

    # On change la valeur des attributs de classe
    # => La modification se répercute dans 'f1' et 'f2'
    Fraction.nName = "numerator"
    Fraction.dName = "denominator"

    # On affiche le détail des fractions à partir de la valeur de ses attributs
    # de classe et d'instance pour vérifier la bonne prise en compte de la valeur
    # des attributs de classe
    print(f"f1 -> {f1.nName} / {f1.dName} = {f1.getNum()} / {f1.getDen()}")
    print(f"f2 -> {f2.nName} / {f2.dName} = {f2.getNum()} / {f2.getDen()}")

    # On calcule la somme de 'f1' et 'f2' (<- appel à la méthode '__add__()' de 'Fraction')
    f = f1 + f2

    # On affiche le résultat via l'appel à la méthode '__repr__()' de 'Fraction'
    print(f"{f1} + {f2} = {f}")
Résultat :
f1 -> numérateur / dénominateur = 3 / 4
f2 -> numérateur / dénominateur = 1 / 2
f1 -> numerator / denominator = 3 / 4
f2 -> numerator / denominator = 1 / 2
3/4 + 1/2 = 5/4
Le mot clé self

Le premier argument d’une méthode est toujours le mot clé self. Cet argument dénote l’objet sur lequel on est en train de travailler. Ainsi, lorsqu’on écrit :

def setFrac(self, num, den) :
    self.n = num
    self.d = den

le sens de ce code est le suivant :

  • on définit la méthode setFrac() de la classe qui aura 2 arguments (il ne faut pas compter self qui est un argument spécial)

  • lorsque l’on appelle cette méthode avec une syntaxe du type f1.setFrac(4, 5), ceci a pour effet de modifier la valeur des attributs d’instance n et d de l’objet f1 c’est-à-dire celui sur lequel la méthode est appelée.

🞄  🞄  🞄