Aller au contenu

T2.1 POO (part two)⚓︎

2.1.4 Des inconvénients⚓︎

Mauvaise utilisation⚓︎

Reprenons l'exemple de la classe Voiture de l'exercice 2 et imaginons l'utilisation suivante:

>>> dmc12 = Voiture(0, 20)
>>> dmc12.remplir(25000)
>>> dmc12.avance(-500)
>>> dmc12.affiche()
La voiture a parcouru -500 kilomètres et il y a 25100.0 litres d'essence dans le réservoir.

Quels problèmes illustre cet exemple?

Différentes implémentations⚓︎

Revenons maintenant sur la classe Chrono de l'exercice 3. L'objectif de cette classe est de manipuler un chronomètre, et donc d'utiliser exclusivement les méthodes affiche et avance. On aurait donc très bien pu ne gérer qu'un seul attribut temps donnant le temps en secondes, et de calculer les heures et minutes à partir de la valeur de cet attribut. Par exemple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Chrono:
    def __init__(self, t):
        self.temps = t

    def affiche(self):
        h = self.temps // 3600
        m = self.temps // 60 - 60*h
        s = self.temps - 3600*h - 60*m
        return f'Il est {h} heures, {m} minutes et {s} secondes.'

    def avance(self, s):
        self.temps += s

2.1.5 Un remède: l'encapsulation⚓︎

Privé ≠ Public

Dans la philosophie de la POO, les attributs doivent être privés, c'est à dire qu'ils ne doivent pas être modifiables directement. Leur manipulation doit se faire uniquement par des méthodes. L'utilisateur ne doit pas avoir besoin de connaître ces attributs, cela reste dans les choix d'implémentation.

Les méthodes, elles, sont publiques: elles constituent ce qu'on appelle l'interface de la classe.

Pour accéder ou modifier les valeurs des attributs, on passe donc par des méthodes dédiées : c'est le principe de l'encapsulation.

Exemple: accesseurs et mutateurs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Voiture:
    def __init__(self, k, conso):
        self.kilometrage = k
        self.consommation = conso
        self.carburant = 0

    def get_kilometrage(self):
        return self.kilometrage

    def set_kilometrage(self, k):
        if k > self.get_kilometrage():
            self.kilometrage = k

▶ La méthode get_kilometrage est ce qu'on appelle un accesseur (getter en anglais). Sa seule et unique vocation est de donner la valeur de l'attribut correspondant, celui-ci n'étant pas accessible puisque privé.

▶ La méthode set_kilometrage est ce qu'on appelle un mutateur (setter en anglais). Il permet de modifier la valeur de l'attribut, en permettant d'effectuer tous les contrôles éventuels sur cette valeur.

Exercice 6

  1. Écrire un accesseur et un mutateur pour l'attribut carburant.
  2. Réécrire la méthode avance de la classe Voiture en utilisant les accesseurs et mutateurs.

Exercice 7

Reprendre la classe Chrono (implémentation ci-dessus, avec un seul attribut) en ajoutant les méthodes getter et setter (avec contrôle sur l'argument) et en modifiant les méthodes existantes.

2.1.6 Compléments (Hors programme)⚓︎

Méthodes spéciales⚓︎

Les méthodes spéciales (parfois appelées méthodes magiques) sont encadrées par des __. Ces méthodes (il en existe environ une centaine) sont appelées dans des contextes particuliers (par exemple __init__ est appelée après que l’objet a été alloué, pour initialiser ses attributs). Utiliser __init__ est un passage obligé.

Un autre passage presque obligé est l’obtention d’un affichage human-friendly d’un objet, ce qu'on a fait avec nos fonctions affiche dans les exemples/exercices précédents.

  • __str__ : donne une représentation de l'objet en chaîne de caractères dès que Python en a besoin, par exemple pour print.

  • __repr__ : est utilisée pour afficher l'objet lors de son évaluation, en console par exemple. S’il n’y a pas de méthode __str__ c’est __repr__ qui est utilisée lors d’un affichage avec print.

  • __len__ est appelée automatiquement si on demande la taille d’un objet avec la fonction len.

Exemples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Chrono:
    def __init__(self, t):
        self.temps = t

    def get_h(self):
        return self.temps // 3600

    def get_m(self):
        return self.temps // 60 - 60*self.get_h

    def get_s(self):
        return self.temps - 3600*self.get_h - 60*self.get_m

    def affiche(self):
        return f'Il est {self.get_h} heures, {self.get_m} minutes et {self.get_s} secondes.'
Utilisation en console
>>> c = Chrono(1978)
>>> c
<__main__.Chrono object at 0x7fc8986c7cd0>
>>> print(c)
  <__main__.Chrono object at 0x7fc8986c7cd0>
>>> c.affiche()
'Il est 0 heures, 32 minutes et 58 secondes.'
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Chrono:
    def __init__(self, t):
        self.temps = t

    def get_h(self):
        return self.temps // 3600

    def get_m(self):
        return self.temps // 60 - 60*self.get_h

    def get_s(self):
        return self.temps - 3600*self.get_h - 60*self.get_m

    def __str__(self):
        return f'Il est {self.get_h} heures, {self.get_m} minutes et {self.get_s} secondes.'
Utilisation en console
>>> c = Chrono(1978)
>>> c
<__main__.Chrono object at 0x7f701d943460>
>>> print(c)
  Il est 0 heures, 32 minutes et 58 secondes.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Chrono:
    def __init__(self, t):
        self.temps = t

    def get_h(self):
        return self.temps // 3600

    def get_m(self):
        return self.temps // 60 - 60*self.get_h

    def get_s(self):
        return self.temps - 3600*self.get_h - 60*self.get_m

    def __repr__(self):
        return f'Il est {self.get_h} heures, {self.get_m} minutes et {self.get_s} secondes.'
Utilisation en console
>>> c = Chrono(1978)
>>> c
Il est 0 heures, 32 minutes et 58 secondes.
>>> print(c)
  Il est 0 heures, 32 minutes et 58 secondes.

Avec cette méthode spéciale, on décide de ce que signifie la taille (longueur) de l'objet (si cela signifie quelque chose, n'est-ce pas... ). Si elle n'est pas définie dans la classe, appeler la fonction len entraînera une erreur:

TypeError: object of type 'Chrono' has no len()
1
2
3
4
5
6
7
8
class Parcours:
    def __init__(self, spes, options):
        self.trc = ['Philo', 'HG', 'LVA', 'LVB', 'ES', 'EPS']
        self.spes = spes
        self.options = options

    def __len__(self):
        return len(self.trc) + len(self.spes) + len(self.options)
>>> mpi = Parcours(['Maths', 'NSI'], ['Maths expertes'])
>>> len(mpi)
9

Propriétés⚓︎

En POO, Python permet de combiner:

  • le respect des getters et setters (problème général à la POO);
  • la souplesse syntaxique de la manipulation des attributs.

On utilise pour cela des propriétés. Pour l'illustrer, on observe une classe Point qui représente un point par une abscisse et une ordonnée (comme c'est original) positives

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Point:
    def __init__(self, x, y):
        self._x = x
        self._y = y

    def get_x(self):
        return self._x

    def set_x(self, x):
        if x >=0:
            self._x = x
    # idem pour y

    x = property(get_x, set_x)
    y = property(get_y, set_y)
Dans le code qui précède, _x est un attribut, get_x et set_x sont des méthodes (qui se trouvent être un getter et un setter), et x est une propriété.

Si on écrit à présent :

>>> p = Point(1, 0)
>>> p.x
1
>>> p.x = -2
>>> p.x
1
>>> p.x += 3
>>> p.x
4

En utilisant la propriété x, Python utilise automatiquement le getter ou le setter selon le contexte. Noter que dans p.x += 3, le getter et le setter sont utilisés.

Décorateurs⚓︎

Voici une autre syntaxe possible, mais on n'a plus accès directement aux getter et setter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Point:
    def __init__(self, x, y):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, x):
        self._x = x

Héritage⚓︎

Un des piliers de la POO (mais hors-programme) est le concept d'héritage. En bref, cela signifie qu'une classe peut être écrite à partir d'une classe parent déjà existante, et donc héritera de ses attributs et méthodes.

Par exemple, si on veut des points colorés, il suffit d'ajouter un atttibut de couleur à la classe précédente.

1
2
3
4
class PointColore(Point):
    def __init__(self, x, y, couleur):
        super().__init__(x, y)
        self.couleur = couleur

On signale que la classe PointColore hérite de la classe parent Point (ligne 1).

Ensuite, la méthode __init__ a été redéfinie. On notera l’appel à la méthode __init__ de la classe parent en utilisant super pour les attributs de la classe Point.

Toute instance de la classe PointColore bénéficie des méthodes dejà existantes dans Point, sans avoir besoin de les redéfinir.