• 6 heures
  • Facile

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Mis à jour le 04/02/2019

Comprenez l'encapsulation

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

 Je ne vous ai pas encore parlé d'un principe fondamental en POO : celui de l'encapsulation.

What else?

Pour bien le comprendre, prenons l'exemple des capsules à café. (Oui oui, nous allons bien parler d'encapsulation grâce à des capsules... J'ai un humour bien particulier mais je me soigne.)

Prenez un café, Emile

Comment faisait-on le café dans les temps anciens quand l'électricité n'était pas si répandue ? On prenait du café en grain, on en faisait de la poudre grâce à un moulin à café, puis on le mettait dans la cafetière, on tassait bien, on ajoutait l'eau et on posait sur le feu. 

Que les amateurs de café me pardonnent : cela demande du temps que la plupart des consommateurs préfèrent allouer à d'autres activités. La majorité des gens n'a en réalité pas envie de passer du temps à faire un café. En fait, ils ne veulent même pas savoir comment il est fait. Connaître vaguement le principe des machines leur suffit. Ce qu'ils veulent est le résultat : un café pour se réveiller.

C'est pourquoi une entreprise, incarnée par un ancien acteur d'Urgences, a inventé le principe des capsules et de la machine. C'est tout simple : pour obtenir un café, vous n'avez qu'à insérer une capsule et à appuyer sur un bouton. Et à mettre de l'eau froide. Point.

Toute la logique de la conception du café est déléguée à la machine. Cette capsule renferme du café déjà moulu, conservé à bonne température, prêt à être dégusté.

C'est la même logique avec les classes. Une classe est comme une capsule : elle contient en elle-même tout ce dont elle a besoin pour fonctionner. Lorsque nous l'utilisons, une partie de son fonctionnement nous est caché afin de ne nous donner accès qu'aux fonctions essentielles pour nous. 

Si notre machine était un programme informatique...

Continuons notre exemple. Imaginons une classe CoffeeMachine. Lorsque nous appuyons sur le bouton Tasse de Café, la machine s'allume, fait chauffer l'eau puis nous verse un café. Nous pouvons donc dire que la méthode __init__() va faire appel à trois autres méthodes : demarrer_machine()bouillir_eau() et faire_cafe().

Notre machine ne nous donne pas de bouton pour faire bouillir l'eau. La méthode bouillir_eau() est donc complètement secrète, accessible uniquement de l'intérieur du programme et pas par nous, pauvres mortels que nous sommes. Nous appelons cela des éléments privés.

Si nous regardons derrière la machine, nous voyons un bouton pour l'allumer. Je suppose qu'en appuyant dessus cela va déclencher la méthode demarrer_machine(). Il a fallu que je cherche un peu pour la trouver, elle n'est pas évidente. En soi, on ne l'utilise pas vraiment de l'extérieur puisqu'on appuie plutôt sur le bouton Tasse de Café pour faire un café. Mais la machine nous donne quand même la possibilité de le faire, au cas où. Nous appelons cela des éléments protégés. Vous pouvez y accéder en dehors de la classe, mais on l'utilise plutôt à l'intérieur de la classe.

Démonstration !

Créons une classe CoffeeMachine qui a trois méthodes :  start_machine()boil_water() et make_coffee() :

class CoffeeMachine():

  def start_machine(self):
    # Start the machine

  def boil_water(self):
    # Boil water

  def make_coffee(self):
    # Make a new coffee!

Son niveau d'eau maximum est de 100. Imaginons que la machine ne démarre que si son niveau d'eau est supérieur à 20 :

class CoffeeMachine():
  WATER_LEVEL = 100

  def start_machine(self):
    # Start the machine
    if self.WATER_LEVEL > 20:
        return True
    else:
        print("Please add water!")
        return False

  def boil_water(self):
    return "boiling..."

  def make_coffee(self):
    # Make a new coffee!
    if self.start_machine():
        self.WATER_LEVEL -= 20
        print(self.boil_water())
        print("Coffee is ready!")

machine = CoffeeMachine()
for i in range(0, 5):
    machine.make_coffee()

La console affiche :

boiling...
Coffee is ready!
boiling...
Coffee is ready!
boiling...
Coffee is ready!
boiling...
Coffee is ready!
Please add water!

 

Méthodes privées ou protégées

Bien. Vous serez d'accord avec moi pour dire que je ne dois pas pouvoir exécuter la méthode boil_water() hors de la classe. Comment faire ? Nous allons ajouter des underscores pour transformer nos méthodes en méthodes privées ou protégées.

Ainsi : 

class CoffeeMachine:
    ...    
    def _start_machine(self):
        # Start the machine. 
        if self.water_level > 20:
            return True
        else:
            print("Please add water!")
            return False

Si vous l'appelez sans le underscore, cela ne fonctionnera plus :

Traceback (most recent call last):
 File "private.py", line 26, in <module>
 print("Start Machine: Protected", machine.start_machine())
AttributeError: 'CoffeeMachine' object has no attribute 'start_machine'

Pour continuer à utiliser cette méthode, ajoutez le underscore.

class CoffeeMachine:
    ...
    def make_coffee(self):
        # Make a new coffee!
        if self._start_machine():
            self.water_level -= 20
            print(self.boil_water())
            print("Coffee is ready!")

Et pour les méthodes privées ? Vous ajoutez deux underscores ! Ainsi :

class CoffeeMachine
... 
    def make_coffee(self):
        # Make a new coffee!
        if self._start_machine():
            self.water_level -= 20
            print(self.__boil_water())
            print("Coffee is ready!")

 

Tout est public

Je ne sais pas pour vous, mais lorsque je lis cela, je me dis : si j'essaie d'accéder à ces méthodes hors de la classe, je devrais avoir une erreur. Mon ordi va exploser ou Python va me dire de rentrer chez moi et de me faire une tisane. Mais j'aime le danger ! Je vais essayer.

Je vais commenter la boucle et ajouter trois nouvelles lignes :

...

machine = CoffeeMachine()
# for i in range(0, 5):
#    machine.make_coffee()

print("Make Coffee: Public", machine.make_coffee())
print("Start Machine: Protected", machine._start_machine())
print("Boil Water: Private", machine.__boil_water())

Normalement, la première devrait bien afficher "boiling..." et "Coffee is ready". La seconde devrait afficher "Start Machine" et la troisième ligne devrait renvoyer une erreur. Résultat :

boiling...
Coffee is ready!
Make Coffee: Public None
Start Machine: Protected True
Traceback (most recent call last):
 File "private.py", line 28, in <module>
 print("Boil Water: Private", machine.__boil_water())
AttributeError: 'CoffeeMachine' object has no attribute '__boil_water'

Seule la troisième ligne renvoie bien une erreur ! La méthode "protégée" est bien accessible de l'extérieur. Top. :)

Mais je vais vous montrer une petite astuce : on peut bel et bien accéder à cette méthode privée. Tout est question de syntaxe ! En écrivant _MaClasse__methode_privee(), je peux quand même l'exécuter. Démonstration :

print("Boil Water: Private", machine._CoffeeMachine__boil_water())

Résultat :

boiling...
Coffee is ready!
Make Coffee: Public None
Start Machine: Protected True
Boil Water: Private boiling...

Certains langages sont très stricts au sujet de l'encapsulation. Vous ne pouvez vraiment, vraiment pas accéder à des méthodes privées en dehors de la classe. Python est plus laxiste. La philosophie du langage est plus axée sur la confiance : si nous voulons accéder à un élément protégé ou privé c'est que nous avons une bonne raison de le faire.

D'ailleurs, une phrase du fondateur de Python, Guido van Rossum, est fréquemment citée à ce propos : "We are all consenting adults here". Elle signifie : "Nous sommes tous des adultes consentants". Sous entendu : si vous voulez vous tirer une balle dans le pied, allez-y, vous êtes adulte après tout.

C'est pourquoi Python utilise cette notion d'encapsulation et d'éléments privés mais à travers une convention de syntaxe. Tout est public en Python, néanmoins il vous sera plus difficile d'accéder à des éléments considérés comme privés. Il faut vraiment, vraiment le vouloir !

Sans entrer dans le détail, cette fonctionnalité du langage est très pratique lorsque vous importez un module et que vous souhaitez légèrement l'adapter à vos besoins.

L'encapsulation appliquée à notre programme

À présent que vous avez bien compris le principe d'encapsulation, revenons à notre programme d'origine et modifions-le légèrement.

L'initialisation des zones devrait se faire de l'intérieur de la classe, et non de l'extérieur. Néanmoins, je dois pouvoir quand même le faire en cas de besoin. Je vais donc "protéger" la méthode de la classe initialize_zones(cls) et l'enlever de notre fonction main().

class Zone
    ...
    
    @classmethod
    def _initialize_zones(cls):
        for latitude in range (cls.MIN_LATITUDE_DEGREES, cls.MAX_LATITUDE_DEGREES, cls.HEIGHT_DEGREES):
            for longitude in range(cls.MIN_LONGITUDE_DEGREES, cls.MAX_LONGITUDE_DEGREES, cls.WIDTH_DEGREES):
                bottom_left_corner = Position(longitude, latitude)
                top_right_corner = Position(longitude + cls.WIDTH_DEGREES, latitude + cls.HEIGHT_DEGREES)
                zone = Zone(bottom_left_corner, top_right_corner)
                cls.ZONES.append(zone)

def main():
    for agent_attributes in json.load(open("agents-100k.json")):
        latitude = agent_attributes.pop("latitude")
        longitude = agent_attributes.pop("longitude")
        position = Position(longitude, latitude)
        agent = Agent(position, **agent_attributes)
        zone = Zone.find_zone_that_contains(position)

Après tout, nous n'avons pas envie de savoir s'il faut initialiser la grille ou pas hors de la zone. C'est une vérification qui devrait être faite à l'intérieur. Étant donné que nous exécutons la fonction find_zone_that_contains(position), je vais vérifier si les zones existent déjà au tout début :

class Zone
    ...
    
    @classmethod
    def find_zone_that_contains(cls, position):
        if not cls.ZONES:
            # Initialize zones automatically if necessary
            cls._initialize_zones()

        # Compute the index in the ZONES array that contains the given position
        longitude_index = int((position.longitude_degrees - cls.MIN_LONGITUDE_DEGREES)/ cls.WIDTH_DEGREES)
        latitude_index = int((position.latitude_degrees - cls.MIN_LATITUDE_DEGREES)/ cls.HEIGHT_DEGREES)
        longitude_bins = int((cls.MAX_LONGITUDE_DEGREES - cls.MIN_LONGITUDE_DEGREES) / cls.WIDTH_DEGREES) # 180-(-180) / 1
        zone_index = latitude_index * longitude_bins + longitude_index

        # Just checking that the index is correct
        zone = cls.ZONES[zone_index]
        assert zone.contains(position)
  
        return zone

Bravo ! ‌

Réfléchissons dans le prochain chapitre aux différents moyens de calculer l'agréabilité d'une zone. 

Code du chapitre

Retrouvez tout le code de ce chapitre ici : https://github.com/OpenClassrooms-Student-Center/la_poo_avec_python/tree/06_encapsulation 

Exemple de certificat de réussite
Exemple de certificat de réussite