Voici une vidéo qui explique la philosophie de la la programmation orientée objet :
Jusqu’ici, les programmes que vous avez créés l'ont été en programmation impérative (c'est-à-dire une succession d'instructions) et en programmation procédurale (c’est-à-dire que chaque programme a été décomposé en plusieurs fonctions réalisant chacune des tâches simples).
À partir des années 1960, un autre type de programmation a été rendu nécessaire par le fait que, lorsque plusieurs
programmeurs travaillent simultanément sur un projet, il faut éviter les conflits entre les fonctions :
la programmation orientée objet est née.
Le premier langage de programmation initiant une forme de programmation orientée objet fut Simula, créé à
partir de 1962. Ce langage servait à faciliter la programmation de logiciels de simulation.
Le premier
langage de programmation réellement fondé sur la programmation orientée objet fut Smalltalk 71, créé au
début des années 70.
La programmation orientée objet est un paradigme de programmation, c'est-à-dire une autre manière de voir les notions
en programmation.
En programmation procédurale, ce sont les fonctions qui sont au cœur du programme ; elles
s'appliquent à modifier des données.
En programmation orientée objet, ce sont les données qui sont au cœur du programme : celles-ci vont désormais être
protégées et c'est le développeur qui décide comment elles seront créées, accessibles, modifiées,
... Ah ! pouvoir désormais définir ses propres objets informatiques en précisant leurs types, leurs propriétés et les
fonctionnalités agissant dessus. Que suffit-il ? Un fiat lux ?
Non, il suffit de travailler le cours qui suit ! Cela s'appelle faire ses classes !
Voici une vidéo qui introduit le vocabulaire de la programmation orientée objet au travers de deux exemples :
Voici un premier exemple concret :
Un chien domestique quelconque peut être vu comme un représentant de l'espèce Canis lupus familiaris..
Médor, Fido ou Pupuce, sont des représentants de cette espèce Canis lupus familiaris.
De plus, les chiens domestiques diffèrent les uns des autres : il existe des éléments variables entre eux qui
permettent de les caractériser.
Par exemple : la race, l'âge, la taille, la masse, la couleur du pelage,
la longueur du pelage, un propriétaire, ...
Sur chaque chien, on peut appliquer plusieurs actions : vacciner, tatouer, tondre, ...
On peut réécrire les phrases précédentes avec le vocabulaire de la programmation objet :
Cette espèce Canis lupus familiaris peut être appelée classe.
N'importe quel chien, par exemple Médor, Fido ou Pupuce, sont des représentants de cette espèce Canis lupus familiaris : ils peuvent être appelés objets de cette classe. (Attention ! Le mot objet est un terme spécifique à l'informatique, il n'y a pas ici de chosification d'un animal !)
Les caractéristiques variables peuvent être appelées attributs d'un chien.
Enfin, il est possible de connaître ou de modifier certains de ces caractéristiques (=attributs ). Par exemple :
"tatouer" permet de retrouver le propriétaire d'un chien : c'est un attribut d'un chien domestique.
"tondre" revient à modifier l'attribut "longueur du pelage".
Ces actions possibles sur n'importe quel représentant de la classe seront appelées méthodes.
Voici un second exemple, cette fois-ci dans le domaine informatique :
Au chapitre précédent sur les structures de données, vous avez découvert la notion de pile, c'est un type de données.
Pour rappel, voici une représentation d'une pile non vide :
Vous aviez vu comme exemple de pile :
Cet exemple de pile peut être vu comme un objet du type pile.
L'élément au sommet d'une pile (non vide), c'est-à-dire celui du haut de la pile, est une caractéristique propre d'une pile ; cet élément peut être vu comme un attribut d'une pile.
Dans l'exemplaire
de pile représenté ci-dessus, l'attribut haut prend la valeur "Anakin".
Vous avez aussi vu que la structure de données pile possède une interface, c'est-à-dire un ensemble d'opérateurs, c'est-à-dire des actions spécifiques aux piles :
vide()
est_vide(P)
empiler(a,P)
depiler(P)
...
Tous ces opérateurs, peuvent être appelés des méthodes.
Cet ensemble de caractéristiques et de méthodes font que le type de données structurées pile est ce que l'on appelle en programmation objet une classe.
Voici une vidéo présentant le vocabulaire essentiel de la programmation objet avec une analogie concrète pour aider à la compréhension de ce vocabulaire.
Voici un ensemble de définitions, qui vont vous paraître abstraites au départ, mais qui seront clarifiées au fil des exemples de ce cours.
Une classe est une structure de données abstraite regroupant :
les caractéristiques de ces données, caractéristiques appelées les attributs ou variable d'instance,
les actions applicables sur les données, actions appelées les méthodes.
Un objet est un élément issu d'une classe. On parle aussi d'instance de la classe.
Voici quelques exemples :
Un chien peut être vu comme un objet de la classe Canis lupus familiaris, ayant plusieurs attributs : race, âge, taille, masse, propriétaire, ... Sur cet objet, peuvent s'appliquer plusieurs méthodes : vacciner, tatouer, tondre, ...
Médor, Fido et Pupuce sont des instances de cette classe Canis lupus familiaris.
Un pays peut être vu comme un objet de la classe État, ayant plusieurs attributs : nom, superficie, population, capitale, ... Sur cet objet, peuvent s'appliquer plusieurs méthodes : agrandir, se fractionner, fusionner, ...
La France et la Chine sont des instances de cette classe État.
Une voiture particulière peut être vu comme un objet de la classe voiture, ayant plusieurs attributs : marque, prix, propriétaire, ... Sur cet objet, peuvent s'appliquer plusieurs méthodes : acheter, faire le plein, réparer, vendre, ...
La voiture du proviseur ou de votre enseignant sont des instances de cette classe voiture.
Une autre manière de dire est qu'une classe regroupe des attributs et méthodes communs à un ensemble d'objets :
Les attributs peuvent être vus comme des variables représentant l'état de l'objet,
Les méthodes peuvent être vues comme des opérations applicables représentant le comportement de l'objet.
Encore quelques définitions :
La création d'un objet d'une classe s'appelle une instanciation de cette classe.
La création d'un nouveau pays revient à une nouvelle instanciation de la classe État. Ce nouveau pays est une instance de cette classe État. Cela nécessite de préciser les attributs de ce nouveau pays : donner un nom, spécifier un territoire, une superficie, préciser la capitale, ...
Concrètement, l'instanciation revient à réserver un espace mémoire puis à le remplir par du contenu.
Il existe trois principaux types de méthodes différentes :
celles qui permettent de construire un objet,
celles qui permettent d'accéder à un attribut,
celles qui permettent de modifier un objet.
Découvrons-les !
Un constructeur est une méthode qui permet l'instanciation. Cette méthode initialise (on dit aussi instancie) l'ensemble des attributs de l'objet.
La déclaration d'indépendance est un constructeur de la classe État. En effet, lors de cette déclaration, les attributs de ce nouveau pays sont précisés : quel nom, quel territoire, quelle superficie, quelle capitale, ...
On considère un jeu dans lequel des cartes sont manipulées. On s'intéresse à l'objet carte à jouer.
Justifier que l'on peut définir une classe nommée JeuDeCartesEnMain en précisant deux attributs de
cette classe.
Proposer des méthodes qui peuvent s'appliquer à cette classe. (Seuls des verbes d'action sur des cartes à jouer sont attendus ici).
Proposer deux instances de cette classe.
Comme ces définitions sont assez abstraites de prime abord, voici une analogie pour se faire une image plus concrète de la notion de classe :
Une classe peut être vue comme un moule.
Le moule n'est pas un objet : il sert à créer des objets.
Les instances (ou objets) peuvent être vues comme les pièces sortant du moule.
Le procédé de fabrication d'un objet à partir du moule correspond à une méthode de type constructeur.
La couleur est un exemple d'attribut de l'objet (ou instance). Chaque instance a sa propre valeur pour cet attribut.
Repeindre est un exemple de méthode s'appliquant sur l'objet.
Se briser est une autre méthode s'appliquant sur l'objet.
Une classe est une structure de données abstraite regroupant des données :
munies de caractéristiques appelées attributs,
sur lesquelles peuvent s'appliquer des actions appelées méthodes.
La création d'un objet d'une classe est appelée instanciation.
Celle-ci initialise l'ensemble des attributs de cet objet.
L'instanciation se réalise à l'aide d'une méthode s'appelant un constructeur.
Si ces notions vous paraissent encore confuses, c'est normal. Mais sachez que vous les manipuler depuis le début sans le savoir car en Python, tout est objet : les fonctions, les types, ...
Vous faites partie d'une société qui crée des jeux vidéo. Dans le projet d'un nouveau jeu, vous devez gérer les personnages. Afin d'éviter tout conflit dans le code produit par les autres collaborateurs, vous écrivez votre code en utilisant le paradigme de la programmation objet.
Vous allez devoir commencer par créer la classe Personnage.
Une classe est définie en Python par le mot-clé class suivi du NomDeLaClasse (par convention, contrairement à une variable, l'initiale est en majuscule et la suite en CamelCase) puis de deux-points : :
class NomDeLaClasse:
"""
Documentation
"""
Voici le début de la classe Personnage, avec une succincte documentation :
# pas dans la console mais dans l'éditeur
class Personnage:
"""
Un personnage du jeu vidéo
"""
Pour reprendre l'analogie vue à la fin du 1.3 (pour y retourner), vous êtes en train de "définir" le moule général d'un personnage. Reste à définir les attributs d'un personnage.
Pour simplifier, pour l'instant, nous supposons que les personnages ont deux attributs :
Pour construire un personnage à partir (du moule général) de la classe, il vous faut utiliser un constructeur.
En Python, le constructeur est la méthode :
toujours notée __init__ (utiliser de chaque côté deux tirets du soulignement),
définie (comme pour les fonctions) par le mot-clé def et se termine par deux-points :,
Les paramètres seront toujours :
le paramètre self (qui désigne l’objet auquel s’appliquera la méthode : self représente l’objet dans la méthode en attendant qu’il soit créé.),
suivi des paramètres correspondant aux différentes valeurs assignées aux attributs lors de l'instanciation de l'objet.
class NomDeLaClasse:
...
def __init__(self, val_attribut1, val_attribut2, ...):
self.nom__attribut1 = val_attribut1
self.nom__attribut2 = val_attribut2
...
Voici le code de la classe Personnage précédente augmentée du constructeur :
# pas dans la console mais dans l'éditeur
class Personnage: # Définition de la classe
"""
Un personnage du jeu vidéo # Documentation
"""
def __init__(self, genre, experience): # Définition du constructeur
self.genre = genre # premier attribut : le genre (féminin, masculin, autre)
self.experience = experience # deuxième attribut : l'expérience (évaluée par un nombre entier)
A-t-on maintenant le personnage ? Pas encore, il faut appeler (implicitement) le constructeur pour que l'ordinateur alloue une place au futur personnage dans sa mémoire pour y caser l'objet et ses attributs.
Pour créer un personnage,
nommé ici Alex, c'est-à-dire une instance de la classe Personnage, il suffit, non pas d'appeler le constructeur __init__ mais directement la classe par son nom
en précisant comme arguments la valeur spécifique de chaque attribut.
Pour créer le personnage nommé Alex, de genre masculin et sans
expérience, il vous suffit de saisir :
# dans l'éditeur
Alex = Personnage("masculin", 0)
Vous pouvez visualiser le code de la définition de la classe Personnage et l'instanciation de l'objet Alex ci-dessous en ramenant le curseur au début si besoin puis en faisant défiler étape par étape (avec le
bouton "next >") :
Pour créer une instance d'une classe, il suffit en Python d'utiliser la syntaxe :
nouvel_objet = NomClasse(nom_attribut1, nom_attribut2, ...)
Votre personnage Alex est désormais créé et a sa place dans l'ordinateur (si ce n'est dans le futur jeu !) comme vous pouvez le voir avec :
# dans l'éditeur
>>> print(Alex)
<__main__.Personnage object at 0x032214D0>
__main__.Personnage object signifie que Alex est un objet de la classe Personnage : vous avez dès lors son type.
0x032214D0 correspond à la notation hexadécimale (d'où le 0x du début) de l'adresse où est stockée l'objet dans la mémoire. L'exécution du code sur votre ordinateur conduira sûrement à une autre adresse.
Les attributs du personnage créé sont désormais toujours accessibles à l'aide de l'opérateur d'accessibilité point . :
Pour accéder aux valeurs des attributs du personnage Alex :
# dans l'éditeur
>>> Alex.genre
'masculin'
>>> Alex.experience
0
Pour accéder aux attributs d'une instance, il suffit en Python d'utiliser avec l'opérateur . en suivant la syntaxe :
valeur = nom_objet.nom_attribut
Créer deux personnages nommés :
Berenice une femme d'expérience 1254.
Bob de genre 'autre' et d'expérience 0.
Vérifier la valeur des attributs de Berenice.
Comme a priori, tout nouveau personnage doit commencer avec une expérience nulle, il est possible de définir la valeur par défaut de cet attribut :
class Personnage:
"""
Un personnage du jeu vidéo
"""
def __init__(self, genre, experience=0): # initialisation
self.genre = genre
self.experience = experience
Alors l'appel au constructeur peut se faire avec un seul argument : le genre .
Création simplifiée d'un personnage avec une expérience banale :
# dans l'éditeur
>>> Duc = Personnage('masculin')
>>> Duc.experience
0
On peut encore créer un personnage avec une autre experience en rajoutant un second argument :
# dans l'éditeur
>>>Elsa = Personnage('feminin', 42)
>>>Elsa.experience
42
Résumé des bases vues au 2.1 de la programmation d'une classe en Python :
On définit une classe en suivant la syntaxe Les méthodes se définissent comme des fonctions, avec le mot clé On construit une instance de classe grâce à son constructeur, une méthode appelée Les méthodes prennent en premier paramètre On définit les attributs d'une instance dans le constructeur de sa classe, en suivant cette syntaxe : On peut accéder aux attribut d'un objet avec l'opérateur
class NomClasse: (nom en CamelCase).def, sauf qu'elles se trouvent dans le corps de la classe.__init__.self, l'instance de l'objet manipulé.self.nom_attribut = valeur.. en suivant cette syntaxe : valeur = nom_objet.nom_attribut
Les personnages du jeu vidéo géreront des outils au cours de leur aventure. Ces outils possèdent différents attributs :
un niveau d'expérience minimal du personnage pour qu'il puisse le manipuler (codé sous forme d'un nombre entier),
une masse (le personnage ne peut pas porter trop de charge) (codé sous forme d'un flottant),
le nombre de mains nécessairement libres pour pouvoir l'utiliser (de 1 à 4 : une coopération avec d'autres personnages peut être nécessaire).
Définissez une nouvelle classe nommée Outil.
Créez le constructeur de cette classe permettant de définir les trois attributs cités pour un outil. Par défaut, le constructeur initialise à 1 le nombre de mains nécessaires pour l'utilisation d'un objet.
Créez deux outils au choix dont un nécessite plus d'une main.
Pour chacun de vos outils, vérifiez la valeur de l'attribut correspondant au nombre de main.
Content de vous, vous montrez le début de votre travail à votre chef d'équipe. Il est horrifié ! Si l'utilisateur a accès à la représentation interne des classes, il pourrait facilement tricher en donnant par exemple à un personnage une experience de 1000000 !
Il vous parle alors de la notion d'encapsulation :
L'encapsulation est un principe qui consiste à regrouper des données avec un ensemble de méthodes permettant de les lire ou de les manipuler dans le but de cacher ou de protéger certaines de ces données.
Les méthodes et données internes (celles plus ou moins "cachées" à l'utilisateur) sont dites privées.
Les méthodes et données accessibles à tout utilisateur (celles que les utilisateurs de la classe connaissent)
sont dites publiques.
Certains langages, comme le PHP ou le Javascript par exemple, définissent de manière stricte cette visibilité en fournissant des mots-clés pour caractériser si chaque élément d’une classe est privé ou public.
En Python, il y a une convention de nommage : un attribut privé est toujours préfixé (c'est-à-dire précédé) de deux espaces soulignés (tiret du bas, celui du 8).
attrib1 et attrib2 sont deux attributs publics.
__attrib3 et __attrib4 sont deux attributs privés.
En Python, lorsque le nom d'un attribut commence par "__", celui-ci est automatiquement renommé ainsi : _nomClasse__nomAttribut. Étant ainsi renommé, il n'est plus aussi aisément accessible depuis l'extérieur de la classe.
Modifiez le code suivant pour rendre l'attribut experience privé :
class Personnage:
"""
Un personnage du jeu vidéo
"""
def __init__(self, genre, experience=0):
self.genre = genre
self.experience = experience
Créez un nouveau personnage au choix.
Essayez d'accéder directement à son expérience avec la syntaxe : valeur = nom_objet.nom_attribut.
Que remarquez-vous ?
Il existe un niveau intermédiaire entre privé et public que l'on nomme protégé. En Python, il suffit de préfixer l'attribut (ou la méthode) d'un espace souligné (tiret du bas ou du 8). Par exemple : _attrib5 # attribut protégé.
Contrairement à d'autres langages, en Python, les données protégées sont accessibles normalement et le préfixe a pour seul objectif d’informer sur leur nature. Nous n'en parlerons pas plus dans ce cours.
Pour l'interface graphique, le niveau d'expérience doit être accessible mais le joueur ne doit pas pouvoir modifier la valeur directement. Pour pouvoir accéder à la valeur de l'attribut, on créé dans la classe une méthode appelée accesseur.
Par convention, un accesseur commence par le verbe anglais get (to get = obtenir = récupérer).
Comme toute méthode, son appel se fera suivant la syntaxe suivante :
valeur = nom_objet.nom_accesseur()
Voici la classe précédente augmentée de la méthode get_experience qui permet de récupérer le niveau d'expérience (mais pas de modifier ce niveau) :
class Personnage:
"""
Un personnage du jeu vidéo
"""
def __init__(self, genre, experience=0):
self.genre = genre
self.__experience = experience
def get_experience(self): # self toujours pour désigner l'objet auquel s'appliquera cette méthode
return self.__experience # return ici pour récupérer la valeur souhaitée
Écrivez le code précédent (dans un éditeur ou dans un Jupyter).
Créez un nouveau personnage Freeda ; vous pouvez lui donner le niveau d'expérience que vous voulez.
Obtenez son niveau d'expérience en utilisant le code suivant : Freeda.get_experience(). Retrouvez-vous la valeur que vous aviez saisie ?
Reprenez la classe que vous avez créé à l'exercice 3 (ici) :
Rajoutez un accesseur permettant de récupérer la masse d'un objet.
Utilisez cette méthode afin de récupérer la masse d'un des objets que vous avez créé lors de l'exercice 3.
Lors du jeu, le niveau d'expérience du personnage doit évoluer : cette expérience doit être accessible en interne mais pas en externe.
Pour pouvoir modifier la valeur de l'attribut d'un objet, on créé dans la classe une méthode appelée
mutateur.
Par convention, un mutateur commence par le verbe anglais set (to set = modifier).
Comme toute méthode, son appel se fera suivant la syntaxe suivante :
nom_objet.nom_mutateur()
Voici ci-dessous la classe précédente augmentée de la méthode set_experience qui permet de modifier le niveau d'expérience.
Pour l'instant, cette méthode est publique afin que vous puissiez l'utiliser dans la console. Il est possible de rendre cette méthode privée en la nommant __set_experience. Dans ce cas, vous pourrez modifier le niveau d'expérience
dans le programme (de l'éditeur) mais pas y accéder depuis la console (cf. le programme de l'exercice 9 du 2.5 accès direct).
class Personnage:
"""
Un personnage du jeu vidéo
"""
def __init__(self, genre, experience=0):
self.genre = genre
self.__experience = experience
def get_experience(self): # self toujours pour désigner l'objet auquel s'appliquera cette méthode
return self.__experience # return ici pour récupérer la valeur souhaitée
def set_experience(self, valeur): # valeur sera le niveau de l'experience
self.__experience = valeur
Remarquez qu'aucun return n'est nécessaire ici pour le mutateur ; la valeur de l'expérience est changée sans être renvoyée. Un peu comme si vous modifiiez une variable globale.
Écrivez le code précédent (dans un éditeur ou dans un Jupyter).
Créez un nouveau personnage Garou ; vous pouvez lui donner le niveau d'expérience que vous voulez.
Vérifier son niveau d'expérience en utilisant l'accesseur get_experience().
Modifier son niveau d'experience en le mettant à 10, par exemple, en utilisant la syntaxe suivante :
Garou.set_experience(10).
Vérifier son niveau d'expérience en utilisant la méthode adéquate.
Reprenez la classe que vous avez créé à l'exercice 3 (ici) :
Rajoutez un mutateur permettant de diminuer de 1 le nombre de mains nécessaire à la manipulation d'un objet, si ce nombre n'est pas 1.
Créez un outil dont l'utilisation nécessite initialement 3 mains.
Utilisez le mutateur pour modifier le nombre de mains nécessaire pour la manipulation de cet outil.
Vérifiez, avec une nouvelle méthode à créer, le nombre de mains nécessaires désormais nécessaire à la manipulation de cet objet.
Résumé des informations vues du 2.2 au 2.4 de la programmation d'une classe en Python :
L'encapsulation est un principe qui consiste à cacher ou à protéger certaines données des objets. Un attribut ou une méthode peut être :
public : c'est-à-dire accessible à tout utilisateur, privé : c'est-à-dire accessible "seulement" dans le code de la classe. Un accesseur est une méthode de la classe qui retourne la valeur d’un attribut d'un objet. Un accesseur est défini dans la classe, par exemple en suivant cette syntaxe : Un mutateur est une méthode de la classe qui modifie la valeur d’un attribut d'un objet. Un mutateur est défini dans la classe, par exemple en suivant cette syntaxe :
Un attribut ou une méthode privée s'obtient avec deux soulignements __ en suivant cette syntaxe :
__nom_attribut ou __nom_methode()
def get_attribut(self):.def set_attribut(self, nouvelle_valeur):.
Il est possible d'insérer dans une classe toute méthode jugée utile.
Dans notre exemple, il serait intéressant :
d'insérer une méthode rencontre qui fait progresser l'expérience du personnage en fonction des rencontres qu'il vit,
faire en sorte que l'utilisateur ne puisse pas modifier l'expérience de son personnage par simple appel de la méthode set_experience()
On peut supposer que chaque rencontre augmente l'expérience d'un nombre aléatoire compris entre 10 et 20. En Python, cela nécessitera l'utilisation de la bibliothèque random. Ainsi, le début du programme devra désormais commencer
par l'habituel :
from random import *
La méthode rencontre conduit au tirage aléatoire du gain d'expérience puis à l'appel de la méthode désormais rendue privée __set_experience.
from random import *
class Personnage:
... # le début n'est pas modifié : il est à reprendre des exemples précédents par copier-coller.
def __set_experience(self, valeur): # méthode désormais privée car seul le programme y accède pour modifier la valeur : d'où le nom commençant par __
self.__experience = valeur
# Autre méthode :
def rencontre(self):
""""fait évoluer aléatoirement l'expérience lors d'une """
n = randint(10, 20) # tirage aléatoire d'un entier entre 10 et 20 (inclus)
self.__set_experience(self.get_experience() + n) # appel de la méthode __set_experience, agissant sur l'objet sur lequel elle s'appliquera (self).
# Le nouveau niveau d'expérience est l'ancien (obtenu avec self.get_experience() ) augmenté de n.
Complétez puis exécuter le programme complet définissant la classe Personnage.
Créez un nouveau personnage sans expérience initiale puis faites lui vivre une première rencontre.
Observez l'évolution de son expérience.
Faites vivre au personnage une seconde rencontre et observez l'évolution de son expérience.
Pouvez-vous par un appel direct au mutateur __set_experience() modifier l'expérience de ce personnage ?
Voici un exercice à faire en autonomie pour tester votre maîtrise de la syntaxe sur la programmation objet.
Cet exercice est issu du site collaboratif de la forge.
Cliquer sur ce lien pour accéder à l'exercice pour partir à l'aventure avec Guybrush.
Voici un exercice à faire en autonomie pour tester votre maîtrise de la création de classe en programmation objet.
Cet exercice est issu du site collaboratif de la forge.
Cliquer sur ce lien pour accéder à l'exercice à trous à compléter.
nous avons déjà vu précédemment que lorsque l'on essaie d'afficher un objet créé
à l'aide de la fonction print, on obtient en affichage surtout l'adresse mémoire en hexadécimal de l'objet.
Pour redéfinir le comportement de la fonction print, il suffit d'utiliser la méthode
native __str__.
Cette méthode doit :
comporter comme seul paramètre self qui désigne l'objet concerné par cet appel,
renvoyer une chaîne de caractères.
Nous avons déjà vu qu'après avoir créé un personnage Alex comme objet
de la classe Personnage, alors print(Alex) affiche que c'est un objet d'une certaine classe
ainsi que son emplacement mémoire.
Nous voulons désormais que cette fonction print affiche une chaîne de caractères précisant le genre et
le niveau d'expérience du personnage.
Par exemple : après avoir créé le personnage Alex avec :
Alex = Personnage("masculin", 0)
On obtient l'affichage suivant avec print(Alex)
Personnage de genre masculin et de niveau d'expérience 0
Dans la classe Personnage, créer la méthode __str__ permettant l'affichage voulu.
Tester cette méthode.
Le but est de définir une classe CompteBancaire qui permette :
d’instancier des objets tels que compte1,compte2, ...
ces objets "compte" auront deux attributs : nom (=titulaire du compte) et solde (=argent sur le compte),
de connaître le nom du titulaire d'un compte, ainsi que la somme présente sur ce compte,
de gérer les transferts d'argent sur le compte en "sécurité".
et même de créer une méthode "protégée" pour pirater un compte.
tout en disposant d'une documentation précise pour la classe et chaque méthode.
Pour la classe et chaque méthode créée dans la suite de cet exercice, pensez à rajouter une documentation claire.
De plus, à partir de la question 2, pensez à tester la méthode nouvellement créée ou modifiée.
Créez la classe CompteBancaire.
Créez le constructeur de cette classe en faisant en sorte qu'un nouveau compte s'ouvre par défaut avec 0 euro dessus.
Créez les accesseurs à chacun des deux attributs des objets de la classe.
Créez un mutateur qui permet de faire évoluer la somme placée sur un compte de variation euros.
Rajoutez à ce mutateur un test qui permet d'afficher un message si le compte est à découvert à l'issue de la modification.
Rendre ce mutateur privé.
Créez une méthode depot qui utilise le mutateur privé précédent pour ajouter une certaine somme sur un compte bancaire.
Créez une méthode retrait qui utilise le mutateur privé précédent pour retirer une certaine somme sur un compte bancaire.
Testez cette méthode retrait de sorte qu'un compte passe dans le négatif.
Est-ce que l'affichage prévu dans le mutateur privé en cas de solde négatif sur le compte s'affiche lors de l'utilisation de cette méthode publique
?
Créez une méthode afficher qui affiche le nom du titulaire et le solde de son compte.
Créez un mutateur privé qui permette de changer le nom du titulaire.
Créez une méthode protégée _pirater qui utilise le mutateur précédent et qui permet de changer le nom du titulaire d'un compte bancaire.
Utilisez cette méthode _pirater pour vous attribuer un compte bancaire. Oh ! Ce n'est pas bien du tout !
Utiliser l'instruction help(CompteBancaire) afin de visualiser l'ensemble des méthodes visibles facilement par l'utilisateur du code. Un client peut-il facilement être au courant de la présence de cette
méthode protégée _pirater ?
Voici un exercice à faire en autonomie pour tester votre maîtrise de la création de classe en programmation objet.
Cet exercice est issu du site collaboratif de la forge.
Cliquer sur ce lien pour accéder à l'exercice à trous à compléter.
Voici un exercice à faire en autonomie pour tester votre maîtrise de la création et l'utilisation de classes en programmation objet.
Cet exercice est issu du site collaboratif de la forge.
Cet exercice est proche d'un exercice 2 apparaissant dans un des sujets de l'Épreuve Pratique de l'an dernier.
Cliquer sur ce lien pour accéder à l'exercice à trous à compléter.
Par groupe, vous devez développer un projet libre dans lequel vous utiliserez la programmation objet.
Un jeu de 52 cartes est constitués de
Carte, qui contient le constructeur et les accesseurs nécessaires à modéliser un jeu de 52 cartes.
Par quel mot-clé débute toute classe ?
Voici le début d'un script pour cette classe. De nombreuses méthodes sont incomplètes.
class Carte:
"""
Un jeu de 52 cartes
"""
def __init__(self, valeur, couleur): # toujours self pour désigner l'objet auquel s'appliquera cette méthode
"""constructeur de la classe"""
# À compléter # penser qu'il y a deux attributs
# ensembles des accesseurs
def get_valeur(self):
# À compléter # penser à utiliser un return afin de récupérer la valeur souhaitée
def get_couleur(self):
# À compléter # penser à utiliser un return afin de récupérer la valeur souhaitée
Complétez le constructeur de la classe en écrivant une ligne pour chaque attribut.
Compléter les deux méthodes correspondant aux accesseurs liés à chaque attribut.
Testez votre code en construisant une première carte, nommée carte_en_main, qui est une instance de
la classe Carte et qui devra être ici le 7 de carreau.
Rajouter au code précédent deux mutateurs nommés set_couleur et set_valeur ; ces méthodes
ont deux paramètres : self et nouvelle.
On suppose que désormais la carte en main n'est plus le 7 de carreau mais le 10 de cœur. Utiliser les deux
mutateurs créés afin de changer les attributs de l'instance carte_en_main.
On veut construire une méthode trier_hasard dans la classe Carte ayant pour seul paramètre
self qui permet de modifier la couleur et la valeur d'une carte pour simuler le tirage aléatoire
d'une carte du
jeu.
Pour cela :
Rajouter en première ligne de code : from random import *.
Voici ci-dessous le début de la méthode trier_hasard à insérer dans la classe Carte :
def tirer_hasard(self):
"""
choix au hasard de la couleur et de la valeur
"""
# pour la couleur
coul = choice(['carreau', 'coeur', 'pique', 'trefle']) # choix au hasard d'un élément de la liste
# ligne à rajouter pour modifier la couleur de la carte (utilisez le mutateur set_couleur())
En utilisant le mutateur set_couleur(), compléter la dernière ligne de la méthode
trier_hasard.
On modélise le tirage aléatoire de la valeur par le tirage aléatoire d'un nombre entier entre 1 et 13 où :
le 1 correspond à l''As',
le 11 correspond au 'Valet',
le 12 correspond à la 'Dame',
le 13 correspond au 'Roi'.
On simule le tirage aléatoire grâce à randint(1, 13).
Voici la suite de cette
méthode trier_hasard (à ajouter en dessous des lignes précédentes) :
# pour la valeur
n = randint(1, 13) # tirage aléatoire de la valeur
if n == 1:
# ligne à rajouter pour modifier la valeur de la carte en 'As' (utilisez le mutateur set_valeur())
elif n == 11:
# poursuivre en traitant chaque cas
Compléter la fin de la méthode trier_hasard.
Utiliser cette méthode trier_hasard pour modifier carte_en_main.
Après chaque nouveau lancer, découvrez la nouvelle valeur et la nouvelle couleur en utilisant les accesseurs,
Modifier le code de la méthode trier_hasard afin de remplacer le traitement des différents par
des if,
elif et else par l'utilisation d'un dictionnaire.
Libre à vous de développer davantage ce projet en rajoutant votre propres idées.
Pour l'instant deux classes ont été créées de manière indépendante : la classe Personnage et celle Outil. L'objectif est désormais de faire un lien entre ces deux classes.
Pour cela, nous allons utilisé ces classes comme un module simple utilisable par différents programmes, tout comme vous utilisiez des bibliothèques déjà construites à l'intérieur de programme. On parle alors de modularité.
En pratique, vous pouvez :
créer un fichier par classe.
modifier ainsi une classe (donc un fichier) sans avoir à modifier les autres.
partager le travail en équipe en se répartissant les classes (donc les fichiers) à réaliser.
Pour lier les deux classes déjà construites, nous allons successivement :
Construire la classe Outil à l'intérieur d'un fichier spécifique,
Importer cette classe Outil dans un nouveau programme qui correspondra à la classe Personnage précédente modifiée.
Ce programme correspondra à un nouveau fichier,
Créer un programme principal qui importera la seule classe Personnage.
Ce programme correspondra grosso modo au programme accessible à l'utilisateur.
Voici le détail de ce qui est à faire :
Créez un fichier nommée outil.py qui contient le code de la classe Outil. Il vous suffit d'y mettre comme contenu le code obtenu à la fin de l'exercice 8 (cf. lien direct).
Créez un nouveau fichier nommé personnage_avec_outil.py.
Y mettre le contenu de la classe Personnage obtenu à la fin de l'exercice 9 (cf. lien direct).
Comme quelques modifications sont nécessaires pour lier les deux classes, afin d'éviter les confusions, renommer la classe comme
PersonnageAvecOutil.
Rajouter en deuxième ligne le code suivant :
from outil import *
Ce code permet d'importer les fonctions présentes dans le fichier outil.py, c'est-à-dire d'utiliser les méthodes de la classe Outil. Désormais, les deux classes sont
liées.
Créez un fichier main.py. Ce fichier sera le programme que l'utilisateur exécutera directement.
Dans ce fichier main.py, coller le code suivant :
# importation des fonctions présentes dans le fichier personnage_avec_outil.py, c'est-à-dire des méthodes de la classe PersonnageAvecOutil
from personnage_avec_outil import *
# gestion de l'affichage de la saisie du genre par l'utilisateur
genre = input("""
Saisir la lettre correspondant au genre désiré pour votre personnage :
M : si vous voulez une héros de genre masculin,
F : si vous voulez une héroïne de genre féminin,
A : si vous désirez un personnage sans genre déterminé
""")
genre = genre.upper() # pour s'assurer que la lettre saisie est en majuscule
# Création du nouveau personnage, appelé ici par défaut hero
if genre == 'F':
hero = PersonnageAvecOutil('feminin')
elif genre == 'M':
hero = PersonnageAvecOutil('masculin')
elif genre == 'A':
hero = PersonnageAvecOutil('autre')
else :
print("erreur de saisie dans le choix du genre")
# Affichage d'une caractéristique du personnage créé :
print("Votre personnage a comme niveau d'expérience {}.".format(hero.get_experience()))
Remarquez que la méthode get_experience() de la classe PersonnageAvecOutil est directement utilisable.
L'utilisateur peut ainsi utiliser les méthodes
sans savoir comment elles ont été écrites.
En exécutant le programme de ce fichier, vous devez voir apparaître dans la console :
>>>
Votre personnage a comme niveau d'expérience 0.
Voici une vidéo pour aider à la réalisation de l'exercice précédent ainsi que du suivant :
Maintenant que les deux classes sont liées, il est possible de modifier ou de créer des méthodes.
On veut désormais que tout personnage possède un objet, unique pour simplifier pour l'instant.
Pour cela, on considère que le personnage possède un nouvel attribut, nommé objet, qui correspond à l'outil en main.
On veut désormais que tout personnage nouvellement créé commence avec un seul outil : un simple bâton de marche, de masse 0.5 kg et que l'on peut tenir à une seule main.
Pour cela, modifiez comme ci-dessous le script de la méthode __init__ de la classe PersonneAvecOutil du fichier personnage_avec_outil.py :
def __init__(self, genre, experience=0):
self.genre = genre
self.__experience = experience
self.objet = Outil(0, 0.5) # ligne à rajouter
Cette ligne rajoutée fait appel au constructeur de la classe Outil, constructeur importé grâce au code de la première ligne
from outil import *.
On désire maintenant obtenir un accesseur pour ce nouvel attribut objet. On veut qu'il nous renvoie la masse et le nombre de mains nécessaires à son utilisation.
Pour le créer dans la classe PersonneAvecOutil (donc dans le fichier fichier personnage_avec_outil.py), il suffit d'utiliser les méthodes déjà existantes dans la classe Outil.
Le script suivant, qui permet d'obtenir un tel accesseur, est à copier en fin de fichier personnage_avec_outil.py :
# méthode faisant appel à la classe Outil :
def get_objet(self):
"""accesseur des caractéristiques masse et nombre de mains de l'objet"""
masse = self.objet.get_masse()
main = self.objet.get_main()
return masse, main
Notez bien que :
les méthodes get_masse() et get_main() sont celles de la classe Outil importée.
elles s'appliquent sur l'instance self.objet qui correspond à l'attribut objet du personnage courant (appelé avec le mot-clef self).
On désire que lors de la création d'un nouveau personnage, s'affiche le nom de l'objet avec la valeur de ces attributs de masse et de nombre de mains nécessaires.
Pour cela, il suffit de rajouter les deux lignes suivantes en fin du programme principal (du fichier main.py) :
# Découverte des caractéristiques de l'objet en main du héros ou de l'héroïne :
print("Vous commencez avec un bâton de marche de masse {} que vous pouvez tenir à {} main.".format(hero.get_objet()[0],hero.get_objet()[1]))
Notez bien que :
bien que la classe Outil ne soit pas directement importée dans la fichier main.py, on peut accéder aux attributs de l'outil.
cet accès se fait grâce à l'accesseur nouvellement créé, qui lui est importé de la classe PersonnageAvecOutil grâce à la ligne déjà présente : from personnage_avec_outil import *.
Si vous exécutez ce programme main.py, vous verrez s'afficher dans la console :
>>>
Votre personnage a comme niveau d'expérience 0.
Vous commencez avec un bâton de marche de masse 0.5 que vous pouvez tenir à 1 main.
Résumé de cette partie :
On peut décomposer un projet en plusieurs classes dont le script est écrit dans des fichiers séparés, On peut utiliser une classe dans une autre classe par simple import de fichier, L'utilisateur aura un accès direct à un fichier (ici L'utilisateur utilise indirectement l'ensemble des classes : en instanciant un personnage de la classe On peut ainsi partager le travail de codage entre plusieurs personnes sans risque de perturbation, une fois l'architecture globale des fichiers conçue.
main.py : il ne voit pas comment est conçue l'architecture globale du projet
ni l'ensemble des classes qui le composent).PersonnageAvecOutil,
il instancie aussi un outil de la classe Outil.
Vous devez créer une nouvelle méthode, nommée decouverte dans la classe PersonnageAvecOutil qui modélise la découverte d'un nouvel objet par le joueur, objet dont le niveau requis, la masse et la
nombre de mains sont donnés comme paramètres de cette méthode.
Cette méthode conduira au remplacement de l'outil en main (celui de l'attribut objet) par le nouveau dans le seul cas où le personnage possède
un niveau d'expérience suffisant pour cela.
De plus, deux affichages différents sont attendus dans la console :
Nouvel objet s'il y a eu changement d'outil,
Dommage, il faut encore progresser en niveau sinon.
Testez votre code en lançant le programme main.py puis en saisissant, par exemple, dans la console :
>>> hero.decouverte(10, 1.23, 2)
Vous devez obtenir comme affichage :
>>> hero.decouverte(10, 1.23, 2)
Dommage, il faut encore progresser en niveau
Testez le cas de la découverte d'un objet que votre personnage peut posséder vu son niveau d'expérience.
Vous pouvez vous amuser à rajouter d'autres méthodes (voire même des attributs) pour poursuivre le jeu.
Vous pouvez avoir un personnage avec plusieurs outils possédés. Pour cela, il vous suffit de modifier ou rajouter des attributs de votre personnage et d'instancier autant de fois la classe Outil que le nombre d'outils
que votre personnage peut avoir. Évidemment, vos aurez sûrement alors à modifier les méthodes liées à ces attributs.
Maintenant que vous maîtrisez la programmation objet, vous allez pouvoir implémenter ces deux notions de piles et de files à l'aide de ce paradigme de programmation.
Dans cet exercice, vous allez créer progressivement une implémentation de la notion de pile accompagnée de son interface
vue au chapitre précédent sous forme de méthode s'appliquant à un objet pile.
La notion de liste Python sera utilisée pour créer la classe Pile.
Voici ci-dessous un début de code, mais erroné, pour cette classe Pile.
Corriger le code proposé afin que le constructeur permette de créer un objet pile en faisant en sorte que
par défaut le contenu est une liste vide.
class Pile:
def __init__(self, valeurs):
self.contenu = valeurs
Pour faciliter les tests par simple affichage, insérer la méthode __str__ suivante dans la classe
en cours de création :
def __str__(self):
"""permet l'affichage des valeurs de la pile
les unes au-dessus des autres, la dernière étant au sommet"""
l = self.contenu.copy() # copie indépendante pour ne pas modifier le contenu de la liste lors de l'affichage
l.reverse()
ch = ""
for valeur in l:
ch = ch + "\n" + str(valeur)
return ch
Tester le constructeur déjà codé.
Créer la méthode est_vide qui renvoie True si la pile est vide,
False sinon
Tester cette méthode.
Créer la méthode empiler qui prend en paramètre un entier elt et qui place
cet élément au sommet de la pile.
Tester cette méthode.
Créer la méthode depiler qui supprime l'élément placé au sommet de la pile sous peu qu'elle
soit non vide et qui renvoie l'élément dépilé.
Utiliser une précondition.
Tester cette méthode.
Nous allons rajouter d'autres méthodes à la classe Pile définie à l'exercice précédent.
Créer l'accesseur sommet qui renvoie la valeur du sommet de la pile, sous peu que celle-ci
soit non vide, sans la retirer.
Créer la méthode hauteur qui renvoie le nombre d'éléments de la pile sur laquelle s'applique
cette méthode.
Créer la méthode vider qui vide la pile sur laquelle s'applique cette méthode.
Il existe d'autres manières d'implémenter des piles en programmation objet.
L'autotest ci-dessous vous en présente une autre.
Attention, dans cette nouvelle implémentation, il y a une persistance des
données constituant chaque pile et la méthode depile ne
renvoie pas l'élément dépilé mais une nouvelle pile soit vide, soit correspondant
à l'ancienne sans son sommet.
Voici un exercice à faire en autonomie pour tester votre maîtrise de la programmation objet
tout en découvrant une autre implémentation possible des files.
Cet exercice est issu du site collaboratif de la forge.
Cliquer sur ce lien pour accéder à l'exercice.
Vous n'avez qu'à compléter les lignes 19 à 27 du script.
Vous verrez au niveau de la classe Pile à compléter
(sans les commentaires d'aide ajoutés) :
class Pile:
def __init__(self, maillon=None):
self.maillon = maillon # None si la pile est vide
def est_vide(self):
return self.maillon is None
def empile(self, elt):
return ... # Lire l'explication en lien avec le dernier schéma
def sommet(self):
if not self.est_vide():
return ... # valeur du sommet
def depile(self):
if not self.est_vide():
return ... # Renvoie d'une nouvelle pile correspondant à celle initiale sans son sommet
Dans cet exercice, vous allez créer progressivement une implémentation de la notion de file accompagnée de son interface
vue au chapitre précédent sous forme de méthode s'appliquant à un objet file.
La notion de liste Python sera utilisée pour gérer le corps de la file.
On suppose dans cet exercice qu'une file a un seul élément a cet élément placé dans la tête et qu'une file a deux éléments a un corps vide.
Lorsque nous aurons vu comme structures de données les listes chaînées, nous pourrons améliorer la classe créée
dans cet exercice, en particulier au niveau du coût de la méthode defiler.
Voici ci-dessous le début de code pour cette classe File.
class File:
def __init__(self, tete, queue=None, corps=None):
self.tete = tete
if corps is None: # presque la même chose que corps == None mais en plus rigoureux
self.corps = []
self.queue = queue
def __str__(self):
"""permet l'affichage des valeurs de la file
les unes à côté des autres, la tête apparaissant en premier,
la queue en dernier"""
ch = "<-"
if self.tete is not None: # presque équivalent à self.tete != None
ch = str(self.tete)
if self.corps != []:
for elt in self.corps :
ch = ch + " | " + str(elt)
if self.queue is not None: # presque équivalent à self.queue != None
ch = ch + " | " + str(self.queue)
return ch + "<-"
Pour permettre l'affichage du contenu de la file, la méthode __str__ a été modifiée.
Quels sont les attributs de la classe File ?
Quel est le type de l'attribut corps ?
Créer la méthode est_vide qui renvoie True si la file est vide,
False sinon
Tester cette méthode.
Créer la méthode enfiler qui prend en paramètre un entier elt et qui place
cet élément à la queue de la file.
Tester cette méthode, entre autre avec une file vide, une file a un élément, une file a deux éléments, une file a plus de deux éléments.
Créer la méthode defiler qui supprime l'élément en tête de la file sous peu qu'elle
soit non vide et qui renvoie l'élément défilé.
Utiliser une précondition.
Penser à tester la méthode avec une file vide, une file a un élément, une file a deux éléments, une file a plus de deux éléments.
Tester cette méthode.
Créer un accesseur get_tete qui renvoie l'élément en tête de la file, sans le supprimer.
Créer une méthode longueur qui renvoie la taille de la file.
L'implémentation réalisée sur les files à l'exercice précédent n'est qu'une implémentation parmi d'autres.
On pourrait aussi, entre autres :
utiliser une liste chaînée,
la classe Pile créé lors de cet exercice pour implémentée une file comme
le couple de deux piles entree et sortie comme vu
lors de cet exercice du chapitre précédent.
Une liste circulaire comme dans l'autotest suivant.
Voici un exercice à faire en autonomie pour tester votre maîtrise de la programmation objet
tout en découvrant une autre implémentation possible des files.
Cet exercice est issu du site collaboratif de la forge.
Cliquer sur ce lien pour accéder à l'exercice.
Vous n'avez qu'à compléter les lignes 31 à 48 du script. Si elles ne sont pas directement visibles, descendez dans les lignes du script. Vous y verrez (sans les commentaires d'aide ajoutés) :
def enfile(self, elt):
if self.est_vide(): # Lire le texte en-dessous du quatrième schéma
self.premier = ... # Attention tout élément de la file est un objet de la classe Maillon
self.premier.suivant = ... # "lien vers lui-même"
else:
maillon = ... # instance de classe où deux attributs sont nécessaires (s'aider du cinquième schéma)
self.premier.suivant = ... # s'aider du septième schéma
self.premier = ... # Quelle est la nouvelle queue de file ?
def defile(self):
if not self.est_vide():
premier_maillon = self.premier
element = ... # Lire le paragraphe précédent le dernier schéma
if premier_maillon == premier_maillon.suivant:
self.premier = ... # Dans quel type de file se trouve-t-on si la condition est remplie ?
else:
premier_maillon.suivant = ... # cf. dernier schéma
return ...
Dans cet exercice, vous allez développer un système simplifié de gestion de comptes utilisateurs sécurisés.
Chaque compte :
doit avoir un identifiant unique, un mot de passe stocké de manière sécurisée et un niveau d'accès,
doit pouvoir accéder à un ensemble de chiffrements et de déchiffrements pour gérer des messages sensibles,
doit pouvoir vérifier l'intégralité des données avec un hash.
Pour cela, trois fichiers python différents vont être considérés :
Un fichier outils.py permettant de valider les mots de passe et de les hacher.
Un fichier classes.py contenant les différentes classes du mini-projet : Utilisateur,
CodageCesar et CodageXOR.
Un fichier principal main.py utilisant les fichiers précédents pour gérer des comptes sécurisés.
Partie A : hachage de mots de passe sûrs
Afin d'augmenter fortement le temps nécessaire pour déchiffrer un mot de passe par une attaque par force brute, la première mesure de sécurité est d'imposée des critères sur le mot de passe choisi par les utilisateurs :
Le mot de passe doit contenir au moins 12 caractères.
Le mot de passe doit contenir au moins 1 lettre majuscule.
Le mot de passe doit contenir au moins 1 lettre minuscule.
Le mot de passe doit contenir au moins 1 chiffre.
Le mot de passe doit contenir au moins 1 caractère spécial de cette chaîne :
"!@#$%^&*()_+-=[]{}|;:,.<>?/".
Dans un fichier à créer nommé outils.py, proposer une fonction
valider_mdp qui prend en paramètre une chaîne de caractères correspondant
à un mot de passe que voudrait prendre un utilisateur et qui renvoie un booléen True
si ce mot de passe rempli les 5 critères précédents et False sinon.
Vous pouvez vous aider du code à trous proposé dans cet autre exercice traitant de la cybersécurité.
Tester la fonction valider_mdp créée avec ce jeu de tests :
def tests_valider_mdp():
assert valider_mdp("1_tropCourt") == False, "erreur sur un mot de passe trop court"
assert valider_mdp("Un_sans_chiffre") == False, "erreur sur un mot de passe sans chiffre"
assert valider_mdp("1_sans_chiffre") == False, "erreur sur un mot de passe sans majuscule"
assert valider_mdp("1_SANS_MINUSCULE") == False, "erreur sur un mot de passe sans minuscule"
assert valider_mdp("1SansCaractereSpecial") == False, "erreur sur un mot de passe sans caractère spécial"
assert valider_mdp("Enf1!T@nt_DAttempte...") == True, "erreur sur un mot de passe convenable"
print("tests sur valider_mdp réussis !")
Afin de ne pas stocker en clair les mots de passe, la technique du hachage
va être utilisée.
Celle-ci consiste à transformer une donnée telle un mot de passe en une chaîne de caractères de taille fixe
appelée empreinte (ou hash en franglais) à l'aide d'une fonction de
hachage.
Vous trouverez dans cet autre exercice de cybersécurité
des détails sur ce qu'est une fonction de hachage.
Dans ce mini-projet, nous travaillerons avec la fonction de hachage la plus utilisée actuellement en cybersécurité : la
SHA-256.
Cependant, utiliser un standard de cybersécurité ne suffit pas pour se prémunir d'attaques !
En effet, comme une fonction de hachage transforme toujours le même mot donné en entrée en la même empreinte, il suffit
à un pirate informatique de calculer en avance l'empreinte de millions de mots de passe "courants", de stocker ces
associations "mot_de_passe": "empreinte" dans un dictionnaire puis de chercher dans ce dictionnaire si
une empreinte volée ou interceptée y apparaît et ainsi pouvoir connaître le mot de passe en clair.
Ce type d'attaque s'appelle table arc-en-ciel
(ou Rainbow table en franglais).
Pour éviter ce type d'attaque, une chaîne de caractères aléatoire, appelée sel,
est rajouté par concaténation au mot de passe choisi avant le hachage.
Comme l'empreinte de deux mots proches donnés en entrée sont très dissemblables, une table arc-en-ciel devient inopérante.
Dans le même répertoire outils.py, recopier et compléter la fonction creer_sel suivante qui prend en paramètre un entier long
et qui renvoie une chaîne de caractères formées de long caractères choisis aléatoirement
dans un ensemble de possibles.
import random
caracteres = "azertyuiopqsdfghjklmwxcvbnAZERTYUIOPQSDFGHJKLMWXCVBN0123456789"
lst = [... for ... in ...] # liste des caractères de la chaîne caracteres
def creer_sel(long: int) -> str:
ch = ""
for _ in range(long):
carac = random.choice(lst)
...
return ch
En cybersécurité, il suffit d'associer à chaque utilisateur un sel fixe, qu'il est même possible de stocker en clair.
Dans le fichier outils.py, proposer une fonction rajout_sel qui
prend en paramètre un dictionnaire et une chaîne de caractères correspondant à un utilisateur, qui
vérifie si cet utilisateur apparaît comme clé dans ce dictionnaire et sinon calcule une clé aléatoire de longueur 10
servant de sel pour cet utilisateur puis rajoute dans ce
dictionnaire l'association "nom_utilisateur": "sel_obtenu".
Cette fonction renvoie finalement le dictionnaire éventuellement modifié.
Au vu de l'importance du hachage, une librairie native de Python contient les fonctions utiles pour
de tels hachages dans la bibliothèque hashlib.
Le script suivant permet de renvoyer l'empreinte en hexadécimal de la concaténation des deux chaînes
de caractères données comme argument en utilisant la fonction SHA-256 :
# Ne pas oublier d'importer d'abord la bibliothèque hashlib !
def saler_hasher_mdp(mot_de_passe: str, sel: str) -> str:
return hashlib.sha256((mot_de_passe + sel).encode()).hexdigest()
Dans le fichier outils.py, à l'aide des fonctions précédentes, créer une
fonction hacher qui a trois paramètres :
nom_utilisateur : une chaîne de caractères identifiant un utilisateur.
dico_sel : un dictionnaire qui à chaque utilisateur connu par son nom associe son sel.
mot_de_passe : chaîne de caractères que l'utilisateur aimerait utiliser comme mot de passe.
Cette fonction vérifie que le mot de passe proposé valide les 5 critères vus au début. Dans ce cas, l'empreinte
de la concaténation du mot de passe avec le sel de l'utilisateur est créée et renvoyée. Sinon, rien n'est renvoyée.
On suppose ici que le nom de l'utilisateur apparaît bien comme clé du dictionnaire, mais écrire une
précondition le vérifiant.
Vous pouvez télécharger à cette adresse
le fichier outils.py complet.
Partie B
Le but est de créer un fichier classes.py dans le même répertoire que le précédent outils.py
est de le compléter au fur et à mesure afin qu'il puisse gérer les instances d'utilisateurs et des instances de textes
à chiffrer ou déchiffrer suivant deux moyens simples.
Rajouter dans le fichier outils.py la ligne suivante donnant en clair le sel associé à deux utilisateurs :
dico_sel = {"utilisateur1" : "AXi8g22CAN", "utilisateurB@c": "3VD7SFitP9"}
Tout au long de cette partie, c'est ce dictionnaire qui sera appelé pour obtenir le sel associé à un utilisateur donné.
On supposera aussi que l'identifiant de l'utilisateur est toujours présent dans ce dictionnaire (pour ne pas avoir
à utiliser la fonction rajout_sel).
Importer en une ligne le contenu du fichier outils.py dans classes.py.
La classe nommée Utilisateur possède trois attributs :
id : une chaîne de caractères identifiant de manière unique un utilisateur.
__hache : chaîne de caractères correspondant à l'empreinte du mot de passe de l'utilisateur considéré
niveau_acces : chaîne de caractères correspondant au statut de l'utilisateur :
"admin" ou "user" ou "". Le statut ""
empêche toute action de l'utilisateur sur le site.
Le constructeur de la classe Utilisateur a trois paramètres particuliers :
id, l'identifiant supposé unique de l'utilisateur
mot_de_passe, la chaîne de caractères correspondant au mot de passe en clair que veut
utiliser l'utilisateur.
role, la chaîne de caractère correspondant au statut de l'utilisateur.
Dans classes.py, créer ce constructeur en faisant en sorte de récupérer le sel associé à l'utilisateur puis de hacher le mot de passe
avec la fonction hacher de outils.py et d'affecter comme statut par défaut à l'utilisateur
"user". Si le mot de passe n'est pas valide alors l'attribut __hache est initialisé tout comme
niveau_acces à "".
Créer une instance de la classe Utilisateur pour l'utilisateur identifié par
"utilisateur1" dont le nom est bien présent dans le dictionnaire d'association du
fichier outils.py et dont le mot de passe voulu est "1mot2passe_PAS_sur?".
De même, créer un second objet pour l'utilisateur "utilisateurB@c" avec comme mot de passe voulu
"Mot2P@asse!".
Créer une méthode droits de la classe Utilisateur qui renvoie les droits sous forme d'une chaîne de caractères,
droits correspondant au statut de l'utilisateur.
Vérifier grâce à cette méthode le statut des deux objets précédemment construits.
Créer une méthode verifier_mdp de la classe Utilisateur qui prend en paramètre
un chaîne de caractère correspond à un mot de passe et renvoie un booléen True dans le cas
où l'empreinte calculée de ce mot de passe correspondant bien à l'empreinte associé à l'utilisateur considéré.
Le renvoi est False sinon.
Vérifier avec cette méthode que le mot de passe de l'utilisateur de pseudo "utilisateur1" est bien
"1mot2passe_PAS_sur?".
Créer une méthode __str__ renvoyant une chaîne de caractères permettant d'obtenir un affichage clair :
pour une instance objet de la classe Utilisateur identifié par "Cmoi!" et de
statut "user", print(objet) conduit à l'affichage suivant :
Utilisateur se nommant Cmoi! et ayant comme statut user.
La classe Caesar a un attribut cle (de type str) et permet de chiffrer ou de déchiffrer un texte
à partir de cette clé.
Dans le fichier classes.py, créer la classe Caesar avec son constructeur.
Dans le fichier classes.py, recopier et compléter la méthode chiffrer suivante en supposant que texte
est une chaîne de caractères écrite entièrement en majuscule :
def chiffrer(self, texte: str) -> str:
resultat = [] # stocke temporairement les caractères obtenus par chiffrement César
for caractere in texte:
resultat.append(chr((ord(...) - ... + ...) % 26 + ...))
return ''.join(resultat) # permet de créer rapidement une chaîne de caractères formée des éléments ordonnés de la liste
Voici différentes aides :
chr(num) renvoie le caractère ASCII se trouvant en position num dans la table ASCII,
num étant un nombre entier écrit en base 10.
chr(65) renvoie le caractère "A".
ord(carac) renvoie la position décimale du caractère ASCII carac dans la table ASCII.
ord("A") renvoie $65$.
Vérifier qu'avec un décalage de $11$, le texte "GAZOPKHJZHHJ" est chiffré en
"RLKZAVSUKSSU".
Le chiffrement de César est un chiffrement symétrique : la même clé est utile pour le
chiffrement et le déchiffrement.
Avec le code César, déchiffrer un texte qui a été chiffré avec une cle revient à chiffrer ce texte avec l'opposé
de la clé de chiffrement.
Rajouter la méthode dechiffrer de la classe Caesar qui prend en paramètre un texte
chiffré de type str et renvoie le texte déchiffré.
Vérifier avec une chaîne quelconque de majuscules qu'avec les méthodes créées le déchiffrement annule bien le chiffrement.
XOR est une opération booléenne qui a déjà été vue dans un exercice de cybersécurité.
Il correspond sur les booléens au OU Exclusif : il ne renvoie True que lorsque les deux booléens sont différents.
Par analogie, entre deux bits, le XOR renvoie 1 si les deux bits diffèrent et 0 sinon.
Voici comme appliqué un XOR indirectement entre une chaîne de caractères et un autre caractère :
Chacun des caractères peut être converti en un entier, celui correspondant à la position de ce
caractère dans la table ASCII. C'est par exemple le rôle de la fonction python ord.
Ensuite, chacun de ces nombres peut être converti en binaire.
Puis le XOR s'applique bit à bit sur ces conversions binaires.
Enfin, le résultat obtenu binaire obtenu et converti par bloc de 7 bits en un nombre entier entre 0 et 127,
nombre qui lui correspond finalement à un caractère ASCII.
C'est ce caractère qui est renvoyé comme transformation des deux caractères initiaux.
En appliquant ce procédé à chaque caractère et en concaténant, on obtient la transformation
d'une chaîne de caractères par un autre caractère avec un XOR interne.
Le XOR est implémenté en Python directement avec l'opérateur ^, sur les entiers et les bits,
mais pas sur les caractères.
Considérons la transformation de "a" par "*" avec un XOR interne :
"a" est le caractère en position $97$, nombre qui s'écrit en binaire sur 7 bits :
1100001.
"*" est le caractère en position $42$, nombre qui s'écrit en binaire sur 7 bits :
0101010.
bit à bit, 1100001 ^ 0101010 donne 1001011 : c'est à ce niveau qu'intervient le XOR.
Ce nombre obtenu est l'écriture binaire de $75$.
Comme cette position correspond au caractère "K", "a" est transformé par "*"
en "K".
Dans le fichier classes.py, créer une classe XOR, avec son constructeur, dont l'unique attribut cle est un entier compris entre 0 et 127.
Cette clé correspond à la position d'un caractère dans la table ASCII.
Créer l'instance liée à la clé 42.
Compléter la méthode xor_1carac qui prend en paramètre une chaîne de
caractères texte et renvoie la chaîne obtenu en appliquant sur chacun de
caractère de texte l'opération XOR avec la caractère correspondant à la clé de l'objet considéré,
en ayant bien en tête que le XOR ^ ne s'applique qu'au niveau des entiers, pas des caractères.
def xor_1carac(self, texte: str ) -> str:
lst = [chr(...) for carac in texte] # liste des caractères ASCII obtenus par XOR
return ''.join(lst) # chaîne créée par concaténation directe des caractères de la liste
Vérifier que lorsque la clé choisie est $42$, la chaîne de caractères "Oasis_@" est transformée
en "eKYCYuj"
Rendre désormais privée la méthode xor_1carac.
Créer une méthode chiffrer de la classe XOR qui prend en paramètre
une chaîne de caractères texte, utilise la méthode privée précédente et renvoie la
chaîne de caractères obtenues.
Le chiffrement par XOR est aussi un chiffrement symétrique : la même clé permet à la fois de chiffrer et de déchiffrer en utilisant la propriété suivante sur les caractères ASCII $a$ et $b$ : $$(a \wedge b) \wedge b = a$$
Créer une méthode dechiffrer qui prend en paramètre une chaîne texte
et renvoie la chaîne obtenue par application du XOR avec la clé de l'instance considérée.
Vérifier sur une chaîne de caractères quelconques que le déchiffrement annule bien le chiffrement.
Partie C
Les deux fichiers déjà créés vont être utilisé dans un troisième fichier main.py qui sert de fichier
de travail.
L'idée est de limiter l'accès direct des utilisateurs à ce seul fichier : toutes les interactions s'y situent.
Pour cela, on commence par modifier les deux fichiers sources afin que les tests présents dans ces fichiers ne soient
pas exécutés au moment de l'importation des fonctions et des classes.
Ensuite, on créé deux nouveaux utilisateurs directement dans le fichier main.py puis on interagit dessus
pour authentifier et dé-chiffrer.
Dans le fichier classes.py, rajouter en toute fin ce bloc :
if __name__ == "__main__":
Déplacer dans ce bloc tous les instances crées dans la partie B ainsi que les tests effectués dessus.
Ce déplacement permet de pouvoir toujours lancer ces tests depuis le fichier classes.py :
si vous exécutez le script directement, vous voyez apparaître les résultats des tests.
Cependant, si vous importez ce script dans un autre fichier, ces tests ne sont pas exécutés.
Dans le fichier outils.py, au niveau du corps principal, rajouter dans le dictionnaire
dico_sel les deux associations suivantes :
"admin123!": "J9ysNu9Pjt", "azerty007": "IDB40mFq7c"
Créer dans le même répertoire que les deux fichiers précédents le fichier main.py.
En une seule ligne, importer dans ce nouveau fichier les trois classes Utilisateur,
Caesar et XOR du fichier classes.py.
Dans ce répertoire main.py, créer deux nouveaux utilisateurs :
admin identifier par le nom "admin123!" voulant comme mot de passe
"A1Z2E3R4t5y6:000!".
user3 identifier par le nom "azerty007" voulant comme mot de passe
"^K1o#R53tFq/c5PWk3".
Proposer deux tests d'authentification :
Une personne se prétendant nommée "admin123!" avec pour mot de passe saisi :
"A1Z2E3R4t5y6:000!".
Quel script utilisant une méthode précédemment écrite permet d'authentifier ou non cette
personne ?
Une personne se prétendant nommée "azerty007" avec pour mot de passe saisi :
"^K1o#R63tFq/c5PWk3".
Quel script utilisant une méthode précédemment écrite permet d'authentifier ou non cette
personne ?
Afficher les informations connues sur l'utilisateur admin.
Quel script permet de chiffrer le texte "BONJOURTOUTLEMONDE" avec un décalage de 17 en César ?
Quel script permet de déchiffrer le texte "uWLg\]ghYKK]g{WUHJWUQKT]g[PYV_]Jg]VgLWML]gMJ_]V[]"
avec un XOR de clé $56$ ?
Voici une liste de sites traitant de la programmation orientée objet :
Voici une video sur le site Lumni qui traite de la programmation objet : programmer-avec-des-objets

Les différents
auteurs mettent l'ensemble du site à disposition selon les termes de la licence Creative
Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0
International