Finalement, on se rend compte que toutes nos bulles partagent pas mal de choses :
Ce sont donc toutes des "individus" sortis du même "moule" mais dont on a adapté certaines de leurs caractéristiques ( taille, couleur,...).
D'où l'idée qui est derrière la POO : "fabriquons" un "moule", duquel nous tirerons ensuite tous les "objets" dont nous auront besoin en adaptant au passage leurs caractéristiques, et en précisant de plus la manière dont ces objets vont interagir avec leur environnement.
Vous allez voir que le code devient alors beaucoup plus simple, lisible et compréhensible.
Bulle
Le moule duquel sortiront tous nos objets est appelé une classe en POO.
Une classe est un ensemble d'instructions qui ne décrivent pas un objet précis ( ici une bulle particulière ), mais permettent de spécifier comment en créer un ( ici avec une certaine taille, couleur, comment la faire bouger, etc...) ( quand vous avez une poële, de la pâte et la recette, vous n'avez pas encore les crèpes...)
En Python, pour définir une classe, on utilise le mot-clé class
suivi du nom de la classe avec, par convention, une majuscule :
class Bulle :
Les instructions contenues dans une classe auront deux rôles :
class Bulle :
def __init__(self, x, taille) :
self.x = x # un attribut de l'objet
self.taille = taille # un autre attribut
...........................
def afficher(self) : # une méthode de l'objet
...........................
def monter(self) : # une autre méthode
..........................
self
: c'est un terme conventionnel pour désigner un objet quelconque qui sera issu de la classe.self
sera alors automatiquement remplacé par le nom de la variable contenant l'objet en question ( cf ci dessous ).Ce mot clé self
doit préfixer le nom des attributs à chaque fois que l'on y fait référence dans la définition de la classe.
Si par exemple ici vous écrivez dans les autres méthodes simplement taille
au lieu de self.taille
, Python vous
donnera la même erreur que vous obtenez lorsque vous n'avez pas initialisé une variable ( 'Object has no attribute XX', en gros "je ne vois pas de quoi vous parlez..." 😉)
__
qui encadrent son nom ) qui est automatiquement appelée lorsqu'on crée un objet à partir de cette classe; c'est donc
ici par exemple que l'on initialisera tous les attributs d'une bulle lors de sa création.
On remarque que cette méthode a ici des paramètres : il faudra donc lui passer des arguments, ce qui sera fait lors de la création d'un objet à partir de la classe ( voir plus bas ).
Le code d'une classe est donc constitué de la définition de toutes les méthodes associées à elle.
On notera bien que ce code est indenté par rapport au mot clé class
pour indiquer qu'il fait bien partie de la classe.
Le travail que vous allez faire maintenant va être une réécriture du précédent en utilisant la POO.
Dans un premier temps, vous allez compléter la classe Bulle
en écrivant le code de ses différentes méthodes :
afficher
, les instructions qui permettront de dessiner la bulle en ses coordonnées avec sa taille et sa couleur monter
, les instructions qui permettront de déplacer la bulle, mais aussi de tester si elle arrive en haut de la fenêtre ( et dans ce cas la réinitialiser...)On remarque que le constructeur prend plusieurs paramètres, dont les valeurs ( non définies pour l'instant, comme pour toute fonction... ) lui seront passées par le programme principal lors de la création des objets.
La création d'un objet à partir d'une classe s'appelle son instanciation : on dit alors que l'objet est une instance de la classe.
L'instanciation d'un objet se fait bien sûr en dehors de la définition de la classe.
Un objet est une structure de données complexes à laquelle on accédera par un nom de variable associée à l'objet.
La syntaxe est la suivante :
nom = Classe() # noter les parenthèses : créer un objet revient en fait à appeler la méthode constructeur de la classe
C'est à cette étape que le mot self
va être remplacé par le nom de la variable associée.
Ici, pour instancier un objet à partir de la classe Bulle
et l'associer au nom de variable bulle1 ( par exemple ), on écrira :
bulle1 = Bulle(20, 50)
Comme instancier un objet revient en fait à appeler la méthode constructeur, c'est à ce moment qu'on passe donc à celle-ci les éventuels arguments qu'elle attend ( ici, une position et une taille ).
Pour appeler une méthode associée à l'objet bulle1, la syntaxe est la suivante :
bulle1.affiche()
bulle1.monte()
On notera l'utilisation du point .
dans la syntaxe de la POO, qui indique l'appel d'une méthode à partir d'un objet précis :
nom_de_l_objet.methode()
Mais vous avez remarqué ?
self
est obligatoirement indiqué en paramètre dans la définition des méthodesself
comme argument !En effet, self
est passé implicitement ( c'est à dire, sans que l'on ait besoin de le préciser ) comme argument aux méthodes, c'est pourquoi il n'est pas nécessaire de l'indiquer lors de l'appel.
Vous avez en fait déjà souvent rencontré cette notation en Python ( ou en Javascript ), par exemple pour utiliser des méthodes sur des tableaux :
tab1.append(x) # ajoute l'élément x au tableau tab1
tab2.sort() # trie le tableau tab2
Et oui, les tableaux en Python sont des objets ( en fait, tout est objet en Python...).
Compléter le code précédent de façon à instancier un objet-bulle dans le programme principal, et à animer son déplacement dans la fonction draw
.
Vous avez remarqué ? Plus de "global
" dans la fonction draw
! En effet, les objets ont par nature une portée globale, et sont donc accessibles de partout dans le code;
c'est aussi un des avantages du concept de POO...
Faire le même travail avec deux, trois,....bulles.
Facile maintenant, non ? Mais il manque encore un petit quelque chose...
Effectivement, le code précédent devient vite lourd quand le nombre de bulles augmente...
En fait, une classe est un nouveau type de donnée : tous nos objets peuvent donc très bien être lors de leur création rangés comme élément d'un tableau; il n'est alors plus besoin de leur donner un nom particulier.
Ainsi, si on définit un tableau bulles[]
qui contiendra tous nos objets, on désignera chacun de ces objets avec la syntaxe habituelle des tableaux par : bulles[0]
, bulles[1]
, ....
Réécrire le code précédent en créant un tableau d'un nombre quelconque de bulles ( 50 c'est déjà pas mal...) et en les animant.
Vous utiliserez dans la fonction draw
une boucle pour parcourir tous les éléments du tableau et réaliser l'animation.
Pour les "objets-bulles" que vous venez de créer n'apparaissait pas un des autres concepts majeurs en POO, le fait que les objets peuvent interagir entre eux à travers certaines de leur méthodes.
Pour illustrer ceci, voila une application "classique" de la POO : l'animation de deux balles qui rebondissent entre elles et sur les parois de la "boîte" dans laquelle elles sont enfermées.
Balle
Dans le code ci-dessous :
move
de la classe Balle
pour gérer les rebonds d'une balle sur les parois de la "boîte".
tester votre classe en instanciant un seul objet Balle
( bien observer le constructeur ! ), puis deux. Dans un premier temps, ne vous préoccupez pas des collisions entre balles, elles pourront se "passer dessus".
Une aide pour le mouvement des balles
A la différence des bulles de "Champagne !", les balles peuvent se déplacer dans toutes les directions/dans tous les sens, pas seulement verticalement vers le haut.
Pour coder cela, la vitesse devra donc :
vx
et une vitesse verticale vy
pour distinguer les deux directions possibles de déplacement ( verticalement ou horizontalement )Par exemple : vx = +3
et vy = -5
signifie : "déplacement vers la droite et vers le haut".
Réfléchissez alors aux modifications à apporter à vx
et/ou à vy
pour "faire rebondir" une balle sur chaque côté de la "boîte" dans laquelle elles évoluent...
Vous allez maintenant rajouter à la classe Balle
la méthode ( appelez la par exemple rebonds
😛) qui permettra la gestion des collisions entre les balles.
L'idée est que cette méthode soit appelée après le déplacement de chacune des deux balles, qu'elle teste alors si les deux balles sont "en contact" en comparant leurs positions respectives,
et si oui, qu'elle modifie les coordonnées vx
et/ou vy
des balles pour les faire "se rebondir" l'une sur l'autre...
ça devient compliqué...la méthode devra "travailler" sur deux objets en même temps ???
Alors oui, chaque objet "dispose" de cette méthode, mais elle ne sera pas utilisée de la même façon à chaque instant. Il faut distinguer :
Oui, une classe est en fait un nouveau type de donnée, un objet peut donc être passé comme argument à une fonction ou une méthode :
objet1.methode(objet2)
Il faut bien entendu que la méthode ait été codée pour traiter cet argument...
rebonds()
pour qu'elle permette de gérer les rebonds d'une balle contre l'autre.Pour la détection du rebond : il ne se fera que si la distance entre le centre des deux balles est inférieure à une distance donnée par la relation ci-contre ( oui, il va falloir faire un peu de maths...)
Pour créer un rebond d'une balle sur l'autre, vous pouvez vous contenter de simplement inverser le signe de vx
et vy
des deux balles. Ce n'est
pas très réaliste, mais si ça marche, c'est déjà bien ! 😌
Attention dans cette méthode à bien distinguer l'objet qui correspond à self
( = l' objet à partir duquel la méthode sera appelée ) de celui passé en argument.
Et attention là aussi, self
doit quand même être précisé dans les paramètres de la méthode, même si lors de l'appel de cette dernière, on ne le passera pas comme argument :
def rebonds(self, autre_balle): # paramètre 'self' obligatoire
........
Mais :
une_balle.rebonds(autre_balle) # pas d'argument 'self'
Pour la gestion des rebonds, il faudra comparer chaque balle à toutes les autres ! Comment faire ??