Fichier de rejeu Close

Indication Close

A propos de... Close

Commentaire Close

Téléchargements

Aide

Encapsulation

Objectif :

L’objectif de ce chapitre est d’aborder
  • la notion de contrôle d’accès aux attributs et aux méthodes.
  • les règles d’écritures et d’encapsulation que nous suivrons pour définir des classes
  • les notions d’attributs et de méthodes statiques

Contrôle d’accès, visibilité

Les logiciels produits par l’ingénierie informatique sont par essence complexes. Ils doivent composer avec les contraintes techniques imposées par le matériel, manipuler des données volumineuses ou hétérogènes, mettre en œuvre un ensemble de fonctionnalités qui évoluent au cours du temps, interagir avec des utilisateurs humains ou avec tout un écosystème d’autres logiciels eux mêmes complexes... Le plus souvent ces applications sont développées pas plusieurs personnes. Elles sont étendues, corrigées, versionnées!

La taille des codes et notre capacité limité à prendre en compte leur complexité a conduit à les découper en unités plus petites qui présentent une apparente simplicité : les classes. Une classe est responsable d’une partie du logiciel (single responsability principle) et collabore avec les autres classes. Les mécanismes d’encapsulation permettent de “cacher” la mécanique interne d’une classe pour ne présenter via sont interface que les mécanismes utiles à son utilisation.

Le contrôle de l’accès aux variables et fonctions membres permet de choisir ce qui est visible ou non depuis l’extérieur d’un objet :

  • visibilité publique : Si on possède une référence vers un objet, on peut accéder à toutes ses propriétés et services publics définis par sa classe. En python, c’est la visibilité par défaut si on ne précise rien.
  • visibilité privée : Les propriétés et services privés ne sont visibles qu’à l’intérieur de l’espace de nommage de la classe. Seul le code de la classe peut les manipuler. En python, pour déclarer un élément privé, on fait précéder son nom de 2 tirets.

Prenons le code d’une classe qui possède un attribut publique:

class MyClass:
    def __init__(self):
        self.public_prop = 42

obj=MyClass()

#code valid
x=obj.public_prop

Nous pouvons rendre cet attribut privé en ajoutant “__” devant sont nom. L’accès à l’attribut depuis un code non membre de la classe provoquera une erreur.

class MyClass:
    def __init__(self):
        self.__private_prop = 42

obj=MyClass()

#ERROR!!
x=obj.__private_prop

On pourra toujours accéder à l’attribut depuis une méthode de sa classe.

class MyClass:
    def __init__(self):
        self.__private_prop = 42

    def public_method(self):
        #can acces to private property
        x = self.__private_prop
        return x

obj=MyClass()

#code valid
x=obj.public_method()

On peut également rendre une méthode privée. Elle ne sera plus accessible depuis l’extérieur de la classe.

class MyClass:
    def __init__(self):
        self.__private_prop = 42

    def __private_method(self):
        #can acces to private property
        x = self.__private_prop
        return x

obj=MyClass()

#ERROR!!
x=obj.__private_method()

Mais on pourra toujours y accéder depuis une autre méthode.

class MyClass:
    def __init__(self):
        self.__private_prop = 42

    def __private_method(self):
        return self.__private_prop

    def public_method(self):
        #can acces to private methode
        x= self.__private_method()
        return x

obj=MyClass()

#code valid
x=obj.public_method()
python : code_run80.py
Sorties

            

Reglès d’encapsulation

Invariant

Soit un disque qui peut être posé sur une surface rectangulaire. On souhaite éviter que le disque ne sorte de cette surface. Si non voulons coder ces informations :

class Disc:
    def __init__(self,x,y,r,x_max,y_max):
        self.x=x
        self.y=y
        self.r=r
        self.x_max=x_max
        self.y_max=y_max

d = Disc(50.,50.,10.,800.,600.)

#wrong value!
d.x=5.
d.y=60.
On souhaite imposer que :
  • x > r/2
  • x < xmax - r/2
  • y > r/2
  • y < ymax - r/2

L’ensemble des ces contraintes forme ce qu’on appel un invariant. On souhaite s’assurer que cet invariant soit respecté au cours de l’exécution du programme.

Malheureusement l’instruction d.x=5. a pour effet de briser cet invariant. En rendant les attributs privés et en n’y permettant l’accès qu’à travers des méthodes, on peut s’assurer du maintient de l’invariant.

class Disc:
    def __init__(self,x,y,r,x_max,y_max):

        self.__x_max=x_max
        self.__y_max=y_max
        self.__r=r

        x,y=self.__adjust_position(x,y)

        self.__x=x
        self.__y=y

    def __adjust_position(self,x,y):

        if x < self.__r/2. :
            x = self.__r/2.
        elif x > (self.__x_max - self.__r / 2.) :
            x = self.__x_max - self.__r /2.

        if y < self.__r/2. :
            y = self.__r/2.
        elif y > (self.__y_max - self.__r / 2.) :
            y = self.__y_max - self.__r /2.

        return x,y

    def move(self,x,y):

        x,y=self.__adjust_position(x,y)

        self.__x = x
        self.__y = y


d = Disc(50.,50.,10.,800.,600.)

d.move(5.,60.)

Pour réduire les risques et simplifier l’utilisation d’une classe, nous nous fixons la règle de tout rendre privé par défaut et de ne rendre visible que ce qui doit nécessairement l’être.

Accesseurs & mutateurs

Pour permettre le maintient des invariants qui portent sur les attributs, nous utiliserons des accesseurs et des mutateurs.

class MyClass:
    def __init__(self,value):
        self.__private_value=value
        self.__public_value=value

    #accessor
    def get_public_value(self):
        return self.__public_value

    #mutator
    def set_public_value(self,value):
        self.__public_value=value

Ainsi pour rendre un attribut public, nous le doterons d’accesseur et mutateur publics, mais nous le coderons quand même comme un attribut privé.

Discussion sur l'encapsulation systématique des attributs publics.

Par défaut tous les attributs sont privés. Seulement lorsque c’est nécessaire, nous les rendons publics.

Même si tous les attributs publics ne sont pas soumis à des invariants, nous choisissons tout de même de systématiser l’utilisation d’accesseur/mutateur. Ce choix est discutable car il alourdit l’écriture du code et rajoute des indirections pour accéder aux attributs, ce qui ralentit l’exécution du code.

Nous choisissons tout de même ce compromis car :

  • la perte de performance (très minime) n’est pas gênante dans la mesure où python n’est pas un langage qu’on utilise lorsqu’on recherche à optimiser les temps d’exécution.
  • le code produit sera homogène. L’utilisateur de la classe n’aura pas à distinguer les attributs soumis à invariant des autres. Pas besoin de se demander si on accède comme ça : obj.x ou comme ça : obj.get_x().
  • l’écriture de messages par les accesseurs/mutateurs en phase de débogage est très intéressante
  • on n’a pas toujours conscience à priori des invariants à respecter quand on commence à coder. Il peut être coûteux de changer l’interface d’une classe en cours de développement pour passer d’un accès direct à un accès par accesseur (le descripteur property, ci-après, annule cet argument en python).
  • vous êtes à l’école, et il est intéressant d’apprendre à respecter une règle de codage! pourquoi pas celle-ci?
Descripteur property.
Python3 offre la possibilité d’alléger l’écriture en utilisant porperty.
class MyClass:
    def __init__(self,value):
        self.__public_value=value

    #accessor
    def get_public_value(self):
        print("access value")
        return self.__public_value

    #mutator
    def set_public_value(self,value):
        print("set vallue")
        self.__public_value=value

    public_value = property(get_public_value,set_public_value)

mc=MyClass(12)
mc.public_value = 12
print(mc.public_value)

Grâce à property, lorsqu’on manipule l’attribut public_value on appelle automatiquement les accesseurs et mutateurs de __public_value. Malheureusement l’ENIBook ne supporte pas cette fonctionnalité.

Cette autre écriture est possible :

class MyClass:
    def __init__(self,value):
        self.__public_value=value

    @property
    def public_value(self):
        print("access vallue")
        return self.value

    @public_value.setter
    def public_value(self,value):
        print("set vallue")
        self.__value=value

mc=MyClass(12)
mc.public_value = 12
print(mc.public_value)

L’utilisation de property a le grand intérêt de pouvoir introduire un invariant sans modifier le code client. En effet, l’instruction mc.public_value = 12 peut soit manipuler un attribut public soit appeler sans le savoir un accesseur.

Attributs avec multiplicité

Un attribut est une donnée définie par son type (entier, réel, chaîne de caractères...) et par une multiplicité.

En général, sa multiplicité vaut 1 :
“l’attribut x est 1 entier”
Mais il peut y avoir une multiplicité supérieur:
“l’attribut position se défini par deux réels”

Lorsque la multiplicité vaut plus de 1, il convient d’utiliser un conteneur. Pour ce faire en python, deux conteneurs semblent appropriés : list et tupple. Le tupple étant non mutable, on utilisera plutôt ce conteneur pour les attributs avec multiplicité qui ne sont pas modifiés. On utilisera plutöt une liste si la valeur et la multiplicité de l’attribut sont amenés à évoluer au cours de la vie de l’objet.

python : code_run83.py
Sorties

            

Ensuite, si on souhaite accéder à la valeur des attributs, il faut ajouter un accesseur :

python : code_run84.py
Sorties

            

Pour respecter l’encapsulation, afin de garantir la possibilité de maintenir un invariant sur un attribut avec multiplicité, l’accesseur doit renvoyer une copie du conteneur de l’attribut et non le conteneur lui même. Ici, nous avons choisit de recopier la liste dans un tuple :

  • return tuple(self.__students)

Si nous avions renvoyé directement la liste des noms d’étudiants :

  • return self.__students

Il aurait été possible de modifier n’importe comment la liste des noms d’étudiants du groupe depuis l’extérieur de l’objet. Le maintient de l’invariant : “aucun doublon dans la liste”, deviendrait impossible à vérifier.

On aurait également pu renvoyer une copie de la liste avec l’instruction :

  • return self.__students[:]

Attributs, méthodes statiques

Attribut de classe

La définition d’une classe fournit un espace de nommage dans lequel il est possible de déclarer des fonctions membres, des fonctions qui prennent comme premier paramètre self. Il est également possible de déclarer dans cet espace des variables de classe :

class MyClass:

    public_class_var =12
    __private_class_var =12

    def doit(self):
        print MyClass.__private_class_var

print MyClass.public_class_var
#print MyClass.__private_class_var : not allowed here

Ces variables de classe peuvent être de visibilité privée comme les méthodes en les faisant précéder de __. Notons également qu’il n’est pas nécessaire d’instancier un objet pour manipuler une variable de classe.

Cela permet d’encapsuler dans la classe des informations partagées par tous les objets plutôt qu’avoir recours à des variables globales... ce qui rend le code un peu plus “propre”. Voici deux exemples d’utilisation :
  • un compteur d’instance (ici on ne compte que les instances rouges)
  • une table associative, pour traduire des chaînes de caractères en un code couleur entier
python : code_run85.py
Sorties

            

Méthodes statiques

Il est également possible de déclarer dans la classe une fonction qui ne sera pas une méthode d’instance. C’est à dire une fonction qui ne prend pas comme premier paramètre self.

Par exemple, nous pouvons écrire un accesseur pour nos variables de classe.

class ColoredPoint :

    __cpt_red_instances = 0

    __color_name_table={'red':0,'green':1,'blue':2,'rouge':0,'vert':1,'bleu':2,0:0,1:1,2:2}

    def __init__(self,x,y,color):
        self.__x,self.__y = x,y
        self.__color = ColoredPoint.__color_name_table[color]

        if self.__color == 0:
            ColoredPoint.__cpt_red_instances += 1
        print ColoredPoint.__cpt_red_instances, ' red created'

    def get_cpt_red_instances():
        return ColoredPoint.__cpt_red_instances


cp1 =  ColoredPoint(0,0,"rouge")
cp2 =  ColoredPoint(0,0,"blue")
cp3 =  ColoredPoint(0,0,"red")

print ColoredPoint.get_cpt_red_instances()

Cependant, il faut avoir conscience que l’écriture du code : cp1.get_cpt_red_instances() provoquera une erreur, puisqu’il n’y a pas de paramètre self``disponible dans la fonction ``get_cpt_red_instances. Pour permettre l’appel d’une telle fonction depuis la référence d’un objet, python a prévu une solution. Il s’agit d’ajouter le décorateur : @staticmethod devant la fonction.

class ColoredPoint :

    ...

    @staticmethod
    def get_cpt_red_instances():
        return ColoredPoint.__cpt_red_instances


cp1 =  ColoredPoint(0,0,"rouge")

print cp1.get_cpt_red_instances()

On parle alors de méthode statique.

Attention l’ENIBook ne sait pas gérer les décorateurs.

Méthodes de classe

Pour information, il existe un autre décorateur classmethod. Il permet de remplacer la référence sur l’objet par une référence sur la classe elle même :

class ColoredPoint :

    ...

    @staticmethod
    def get_cpt_red_instances():
        return ColoredPoint.__cpt_red_instances

    @classmethod
    def print_cpt_red_instances(cls):
        print cls.get_cpt_red_instances()

cp1 =  ColoredPoint(0,0,"rouge")

print cp1.get_cpt_red_instances()

Ainsi, nous avons remplacé self``par ``cls car ce premier paramètre prendra pour valeur une référence vers la classe au lieu d’une référence vers l’instance. On parle alors de méthodes de classes plutôt que de méthodes d’instance.

Bien que le décorateur classmethod présentera sont réel intérêt lorsque nous aurons abordé l’héritage nous pouvons donner un exemple d’utilisation. Si nous voulons créer une factory nommée red() pour créer uniquement des points rouges.

class ColoredPoint :

    __cpt_red_instances = 0

    __color_name_table={'red':0,'green':1,'blue':2,'rouge':0,'vert':1,'bleu':2,0:0,1:1,2:2}

    def __init__(self,x,y,color):
        self.__x,self.__y = x,y
        self.__color = ColoredPoint.__color_name_table[color]

        if self.__color == 0:
            ColoredPoint.__cpt_red_instances += 1
        print ColoredPoint.__cpt_red_instances, ' red created'

    @staticmethod
    def get_cpt_red_instances():
        return ColoredPoint.__cpt_red_instances

    @classmethod
    def print_cpt_red_instances(cls):
        print cls.get_cpt_red_instances()

    @classmethod
    def red(cls,x,y):
        return cls(x,y,'red')

#using factory to instanciate
red_point = ColoredPoint.red(10.,10.)

red_point.print_cpt_red_instances()

Par abus de langage...

Nous venons de découvrir 3 nouvelles notions :
  • variables de classes
  • méthodes statiques
  • méthodes de classes python (facultatif)
qui complètent les précédentes notions :
  • attributs ou variables d’instance
  • méthodes d’instance
Par abus de langage ou par convention, et parce que la plupart des autres langages de la POO ne sont pas aussi dynamiques de python, nous parlerons :
  • d’attribut statiques pour désigner les variables de classe
  • de méthodes statiques pour désigner les méthodes de classe python et les méthodes statiques
  • d’attribut pour désigner les variables d’instance.
  • de méthodes pour désigner les méthodes d’instance

Règles d’écriture

La Python software fundation fournit un certain nombre de préconisations pour améliorer les codes écrits en python : Python Enhancement Proposals (PEP). La PEP8 est la plus connue. Elle propose un style de programmation. Vous êtes invités à consulter ce document. Autant que possible, ce cours essaie de respecter la PEP8.

La PEP256, quant à elle, propose de commenter le code en utilisant les docstrings. Pour nous, il s’agira de commencer les modules, classes et méthodes par une chaîne de caractères entre triple guillemets : """ comment """. Ce commentaire sera accessible en consultant la propriété .__doc__ pour chaque élément commenté.

Python est un langage très souple qui permet de programmer de plein de manières différentes. De plus, il n’y a pas de fichier entête pour connaître le contenu des classes. Pour y voir clair il est essentiel de produire un code structuré, documenté et homogène. Pour atteindre cet objectif, ce cours impose quelques les règles d’écriture de code :

  • Nommage :
    • Le nom des classes est en CamelCase
    • Le nom des modules, des variables, méthodes et attributs est en minuscule séparé par de _
    • Le nom des constantes est en majuscule
    • Les noms de variables et les commentaires sont en anglais
  • S’il n’y a qu’une seule classe dans un module, le module porte le nom de la classe.
  • Les modules et les classes sont commentées comme dans l’exemple proposé.
  • Les imports de modules se font dans l’ordre suivant :
    • Modules internes à python
    • Modules externes, logiciels tiers
    • Modules internes à l’application
  • A l’intérieur de la classe on trouvera dans l’ordre :
    • Les variables statiques
    • L’initialisateur
    • Le finaliseur
    • Les accesseurs et mutateurs
    • Les méthodes publiques
    • Les méthodes privées
  • Enfin après l’instruction suivante, on place les tests unitaires du module : if __name__ == '__main__':

Exemple de fichier : my_class.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""Provides MyClass class
illustrates the way python script should be structured
"""

# Built-In Imports
import os
import sys

# Third part Libs
import pygame

# Own modules
import my_other_class

#we may add properties to module
__author__ = "Gireg Desmeulles"
__copyright__ = "Copyright 2019, ENIB"
__credits__ = ["Gireg Desmeulles", "Elisabetta Brevacqua"]
__license__ = "GPL"
__version__ = "1.0.1"
__maintainer__ = "Gireg Desmeulles"
__email__ = "desmeulles@enib.fr"
__status__ = "Production"


MY_CONSTANT = 42

class MyClass(list):
    """here comment the MyClass purpose"""

    #----------class attributs----------

    __MY_CLASS_CONSTANT = 42
    __my_class_attribute = "value"

    #----------special methods----------

    def __init__(self, first_parameter=0.0):
        self.__public_attr = first_parameter
        self.__private_attr = True

    def __del__(self):
        print "MyClass instance destruction"

    #----------public operations----------

    def get_public_attr(self):
        return self.__public__attr

    def set_public_attr(self, value):
        self.__public_attr = value

    public_attr = property(fget=get_public_attr, fset=set_public_attr)

    def public_method(self):
        """ doctring comment for public_method"""
        pass

    @staticmethod
    def my_class_constant():
        return __MY_CLASS_CONSTANT

    #----------private operations----------

    def __private_method(self):
        pass


if __name__ == '__main__':    #code to execute if called from command-line

    #unit testing

    my_class = MyClass('toto')

    print __doc__
    print MyClass.__doc__

Résumé

Ce chapitre nous donne quelques règles pour coder les classes :

  • rendre toutes les propriété des classes privées par défaut et ne rendre public que ce qui doit nécessairement l’être
  • pour rendre un attribut public, le doter d’accesseurs et mutateurs publics, mais conserver une visibilité privée pour cet attributs
  • respecter les règles de codage, de commentaires et de structuration du code.

Par ailleurs nous avons appris qu’il est possible de définir des méthodes et attributs statiques qui sont liés à la classe et non aux objets.

Excercices

  1. Véhicule

    1- Définir une classe Vehicle avec les attributs privés suivants : type, brand, color et price

    2- Créer un constructeur qui accepte les 4 paramètres

    3- Créer les accesseurs

    4- Créer une méthode show permettant d’afficher les caractéristiques d’un véhicule

    5- Instancier deux véhicules de votre choix et afficher leurs caractéristiques

    Votre réponse :
    python : code_run89.py
    Sorties
    
                
    Une solution possible :
    python : code_run91.py
    Sorties
    
                
  2. Point

    1- Définir une classe Point avec les attributs privés suivants : a (abscisse) et o (ordonnée) qui représentent l’abscisse et l’ordonnée d’un point dans un plan cartésien.

    2- Créer un constructeur qui accepte les deux paramètres

    3- Nous souhaitons modifier la valeur par défaut de ce point. Créer les accesseurs et les mutateurs convenables pour effectuer cette tâche.

    4- Créer une méthode move pour déplacer ce point sur le plan.

    5- Créer une méthode show permettant d’afficher les valeurs de l’abscisse et de l’ordonnée après déplacement.

    6- Créer une instance p

    7- Afficher les valeurs de a et o avant et après modifications.

    8- Afficher les valeurs du point p après déplacement

    Votre réponse :
    python : code_run94.py
    Sorties
    
                
    Une solution possible :
    python : code_run96.py
    Sorties
    
                

Labo 2:

  1. Domino

    Définissez une classe Domino qui permet d’instancier des objets simulant les pièces d’un jeu de dominos. 1. Le constructeur de cette classe initialisera les valeurs des points présents sur les deux faces A et B du domino (valeurs par défaut = 0). 2. Deux autres méthodes seront définies :

    • une méthode show_points() qui affiche les points présents sur les deux faces
    • une méthode value() qui renvoie la somme des points présents sur les 2 faces.

    Exemples d’utilisation de cette classe :

    >>> d1 = Domino ( 2 , 6 )
    >>> d2 = Domino ( 4 , 3 )
    >>> d1.show_points( )
    face A : 2 face B : 6
    >>> d2.show_points( )
    face A : 4 face B : 3
    >>> print "total des points : " , d1.value() + d2.value()
    15
    >>> dominos_list = [ ]
    >>> for i in range(7) :
    dominos_list.append(Domino(6,i))
    >>> for j in dominos_list :
    j.show_points()
    
    Votre réponse :
    python : code_run99.py
    Sorties
    
                
    Une solution possible :
    class Domino:
        """ Class Domino represent domino pawn"""
    
        def __init__(self,a=0,b=0):
            self.__a=a
            self.__b=b
    
        def __del__(self):
            print "destroy domino"
    
        def show_points(self):
            msg =  "face A : "+ str(self.__a)+" face B : "+ str(self.__b)
            print(msg)
    
        def value(self):
            return self.__a+self.__b
    
    if __name__ == '__main__':
    
        domino = Domino(1,4)
        domino.show_points()
        print domino.value()
    
  2. Compte bancaire

    Définissez une classe BankAccount, qui permette d’instancier des objets tels que account_1,``account_2``, etc...

    1. L’initialisateur de cette classe déclarera deux attributs name et balance, avec les valeurs par défaut 'Dupont' et 1000. 2. Trois autres méthodes seront définies :

    • ``deposit(amount)` permettra d’ajouter une certaine somme au solde
    • withdraw(amount) permettra de retirer une certaine somme du solde
    • show() permettra d’afficher le nom du titulaire et le solde de son compte.
    Exemples d'utilisation de cette classe :
    >>> account_1 = BankAccount('Duchmol ', 800)
    >>> account_1.deposit(350)
    >>> account_1.withdraw(200)
    >>> account_1.show()
    Le solde du compte bancaire de Duchmol est de 950 euros.
    >>> account_2 = BankAccount()
    >>> account_2.deposit(25)
    >>> account_2.show()
    Le solde du compte bancaire de Dupont est de 1025 euros.
    
    Votre réponse :
    python : code_run103.py
    Sorties
    
                
    Une solution possible :
    class BankAccount :
        """ define a bank account """
        def __init__(self, name = "Dupont", balance = 1000. ):
            self.__name = name
            self.__balance = balance
    
        def __del__(self):
            print("destroy account ")
    
    
        #public method
        def deposit(self, amount):
            self.__balance += amount
    
    
        def whithdraw(self, amount):
            self.__balance -= amount
    
    
        def show(self):
            print("Le solde du compte bancaire de", self.__name, "est de", self.__balance, "euros.")
    
    
    if __name__ == '__main__':
        account_1 = BankAccount('Duchmol', 800.)
        account_1.deposit(350)
        account_1.whithdraw(200)
        account_1.show()
    
        account_2 = BankAccount()
        account_2.deposit(25)
        account_2.show()
    
  3. Voiture

    Définissez une ``Car``qui permette d’instancier des objets reproduisant le comportement de voitures automobiles.

    1. Le constructeur de cette classe initialisera les attributs d’instance suivants, avec les valeurs par défaut indiquées : mark = 'Ford', color = 'red', driver = 'no_one', speed = 0. Lorsque l’on instanciera un nouvel objet Car(), on pourra choisir sa marque et sa couleur, mais pas sa vitesse, ni le nom de son conducteur. 2. Les méthodes suivantes seront définies :

    *choose_driver(name) permettra de désigner (ou de changer) le nom du conducteur * speed_up(acceleration, duration) permettra de faire varier la vitesse de la voiture.

    La variation de vitesse obtenue sera égale au produit : acceleration * duration. Par exemple, si la voiture accélère 1.3m.s^-2 pendant 20 secondes, son gain de vitesse doit être égal à 26 m/s. Des accélérations négatives seront acceptées (ce qui permettra de décélérer). La variation de vitesse ne sera pas autorisée si le conducteur est ‘no_one’.

    • show() permettra de faire apparaître les propriétés présentes de la voiture, c’est-à dire sa marque, sa couleur, le nom de son conducteur, sa vitesse.
    Exemples d'utilisation de cette classe :
    >>> a1= Car('Peugeot', 'bleue')
    >>> a2= Car(color='verte')
    >>> a3= Car('Mercedes')
    >>> a1.choose_driver('Romeo')
    >>> a2.choose_driver('Juliette')
    >>> a2.speed_up(1.8,12)
    >>> a3.speed_up(1.9,11)
    Cette voiture n'a pas de conducteur !
    >>> a2.show()
    Ford verte pilotee par Juliette, vitesse=21.6m/s.
    >>> a3.show()
    Mercedes rouge pilotee par personne, vitesse=0m/s.
    
    Votre réponse :
    python : code_run107.py
    Sorties
    
                
    Une solution possible :
    python : code_run109.py
    Sorties