Infoforall

Python 22 : Les objets - présentation

1 - Rappel sur différents mots de vocabulaire

Nous allons aujourd'hui voir comment utiliser les classes d'objets dans le cadre de la programmation. Il s'agit de l'un des piliers de la programmation orientée objet. Mais, vous avez déjà rencontré plus d'une fois les termes suivants et vous devriez commencer à vous en faire une certaine idée : objet, classe, méthode ...

Tentons de remettre tout ça en place :

Vous avez déjà manipulé plusieurs fois des objets, puisque vous avez utilisé des méthodes : une méthode est une fonction qui permet de manipuler un objet.

Mais qu'est-ce qu'un objet par rapport aux entités plus basiques ?

Variables de structure :

Pour rappel, nous avons parfois utilisé des variables dont le type est int (on stocke un entier) ou float (on stocke un réel).

Par exemple :

a = 45

On stocke deux choses en mémoire :

  • l'information integer 45 codé en binaire dans une zone mémoire précise, connue par un identifiant
  • la liaison entre le nom a, l'identifiant précédent dans un tableau qu'on nommme espace des noms et le type de la donnée codée (integer).
variable

a est donc une variable "simple" qui ne contient que la valeur d’un seul type de donnée. On parle de variable de structure car la structure des données est prédéfinie. Elle ne peut contenir qu'un nombre bien précis et prédéterminé de données. Un integer, un float, un string...

Lorsque l'interpréteur tombe sur a, il va donc pouvoir vous afficher la valeur à l'intérieur de cette "case-mémoire".

Si on supprime les références, on obtient la vision des "boites" :

objet simplifié

Variables de référence :

Il existe néanmoins des objets plus complexes, comme les images : une image est une entité beaucoup plus complexe qu’un entier : elle est définie par :

  • Son nombre de couches : RGB, L, avec éventuellement une couche Alpha en transparence.
  • Son nombre de pixels en hauteur (height)
  • Son nombre de pixels en largeur (width)
  • L’intensité ou les intensités RGB de chaque pixel.

mon_image = Img.open("linux.jpg")

De plus, les objets-image Pillow possèdent des méthodes bien spécifiques (l'activité sur les images devrait vous en rappeler quelques unes : show, putpixel, getpixel, save).

Ici l'utilisation de la variable mon_image ne renvoie pas directement une valeur mais une référence (une adresse) vers les données de l'objet. Cette référence va ensuite être utilisée pour obtenir la bonne valeur parmi toutes celles stockées pour l'objet.

objet

On peut donc voir un objet comme :

  • Un regroupement de plusieurs variables (attributs)
  • pour lequel il existe des fonctions particulières (méthodes)

Si on supprime les références, l'imbrication devient plus facile à visualiser :

objet simplifié

Dans ce cas, les variables-objets renvoient la référence à leur objet lorsqu'on les note dans un programme.

Une classe correspond au type d'objets, au moule à partir duquel l'objet est décrit. Ainsi, vous pouvez avoir plusieurs objets-images mais ils dérivent tous de la même classe img définie dans Pillow.

Des types de variables pour les variables "simples" : si x = 45 alors

  • x est une variable
  • de type int (integer) qui
  • renvoie une valeur.

Des classes pour les objets : si x = Img.open("linux.jpg") alors

  • x est une variable
  • renvoyant une référence vers l'objet
  • de classe Img.

2 - Définitions des classes

Pour créer un objet, il faut utiliser le moule à savoir la class. Pour créer une classe, nous allons utiliser l'instruction class qui s'utilise un peu comme le def des fonctions :

Partons par exemple sur le principe de vouloir faire un RPG futuriste. Il nous faut définir ce qu'est un personnage.

Majuscule : Souvenez-vous de l'activité sur Tkinter : les noms des classes doivent commencer par une majuscule, pour différencier justement les classes des autres entités.

Il nous faut donc créer une classe nommée Personnage et pas "personnage".

#!/usr/bin/env python

# -*- coding: utf-8 -*-


# - - - - - - - - - - - - - - - - - -

# Déclarations des classes

# - - - - - - - - - - - - - - - - - -


class Personnage:

    "Ceci est une classe permettant de créer un personnage dans mon super RPG"


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


bob_skywalker = Personnage()

print(bob_skywalker)

print(type(bob_skywalker))

01° Lancer le code dans IDLE (rajouter sinon une pause à la fin pour vous permettre de voir l'affichage) :

Vous devriez obtenir quelque chose du type :

<__main__.Personnage object at 0x0025ACD0>

<class '__main__.Personnage'>

En gros, il vous dit que c'est un objet issu de la classe Personnage, créé depuis le programme principal (__main__) et que son adresse est 0025ACD0 en hexadécimal (0x).

Pour l'instant, on ne fait pas grand chose par contre : on crée juste un objet à partir d'une classe en utilisant sa méthode-constructeur qui porte le même nom que la classe : Personnage.

D'ailleurs, cette classe ne fait rien en soit, elle ne contient que sa propre description. Essayons de voir si on peut y stocker des informations.

#!/usr/bin/env python

# -*- coding: utf-8 -*-


# - - - - - - - - - - - - - - - - - -

# Déclarations des classes

# - - - - - - - - - - - - - - - - - -


class Personnage :

    "Ceci est une classe permettant de créer un personnage dans mon super RPG"


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


bob_skywalker = Personnage()

bob_skywalker.nom = "Skywalker"

bob_skywalker.prenom = "Bob"

bob_skywalker.niveau = 1

bob_skywalker.classe = "Jedi raté"


print("Le nom du personnage est ",bob_skywalker.nom)

print("Le niveau du personnage est ",bob_skywalker.niveau)

02° Où se trouve l'instruction qui permet de stocker l'information sur le nom ? Où se trouve l'instruction qui permet d'afficher le contenu du nom du personnage ?

...CORRECTION...

On affecte un nom à l'objet bob_skywalker à l'aide de

bob_skywalker.nom = "Skywalker"


On affiche le nom à l'aide de

print("Le nom du personnage est ",bob_skywalker.nom)

Comme vous le voyez, l'accès aux variables d'un objet se fait en utilisant un point entre le nom de l'objet et le nom de la variable. Comme avec les méthodes.

On notera d'ailleurs que vous pouvez avoir une variable niveau dans le reste du programme : Python ne va pas mélanger une variable niveau avec la variable propre (attribut) niveau de bob_skywalker car on accède à cette dernière en utilisant le point : bob_skywalker.niveau. Pas de confusion possible.

accès aux variables

03° Créer un second personnage en utilisant personnage2 = Personnage(). Lui affecter également un nom et un niveau.

Nous aimerions maintenant que deux personnages puissent se combattre.

Pour savoir qui gagne, nous allons additionner leur niveau et un dé à 6 faces. Le plus haut résultat gagne. En cas d'égalité, on retire le dé.

Nous allons créer une fonction combat qui renvoie la référence du perdant lorsqu'on lui fournit les références de deux combattants.

#!/usr/bin/env python

# -*- coding: utf-8 -*-


import random


# - - - - - - - - - - - - - - - - - -

# Déclarations des classes

# - - - - - - - - - - - - - - - - - -


class Personnage:

    "Ceci est une classe permettant de créer un personnage dans mon super RPG"


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


def combat(p1,p2) :

    """Ceci est une fonction qui renvoie la référence de l'objet Personnage qui perd le combat

    Les paramètres p1 et p2 doivent être les références des deux objets de la classe Personnage

    On renvoie donc soit p1, soit p2"""

    perdant = ''

    test = True

    while test :

        valeur1 = random.randint(1,6)+p1.niveau

        valeur2 = random.randint(1,6)+p2.niveau

        if valeur1>valeur2 :

            perdant = p2

            test = False

        elif valeur2>valeur1 :

            perdant = p1

            test = False

    return(perdant)


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


bob_skywalker = Personnage()

bob_skywalker.nom = "Skywalker"

bob_skywalker.prenom = "Bob"

bob_skywalker.niveau = 1

bob_skywalker.classe = "Jedi raté"


perso2 = Personnage()

perso2.nom = "De nom je n'ai pas"

perso2.prenom = "Yoda"

perso2.niveau = 3

perso2.classe = "Vieux Jedi"


print( combat(bob_skywalker,perso2).nom )

print( combat(bob_skywalker,perso2).nom )

Comme on veut afficher le nom du perdant du combat et pas l'objet qui a perdu, on affiche combat(...).nom puisque la fonction combat renvoie la référence d'un objet de classe Personnage.

03° Regarder les deux dernières lignes : quel est le type de bob_skywalker et perso2 ? A-t-on visiblement le droit de fournir un objet comme argument à une fonction ?

...CORRECTION...

Oui, on peut transmettre un objet en tant qu'argument de fonction puisque bob_skywalker et perso2 sont des objets, instances de la classe Personnage.

Si, on regarde les arguments transmis et les paramètres en attente :

p1 reçoit bob_skywalker et p2 reçoit perso2.

04° Peut-on accéder au contenu d'un objet depuis une fonction ?

...CORRECTION...

Oui, car on utilise :

valeur1 = random.randint(1,6)+p1.niveau

05° Que se passe-t-il si les deux jets niveau+d6 donnent le même résultat ?

...CORRECTION...

Le test reste True. On reste donc dans la boucle while : on relance ect ...

Imaginons maintenant que nous ayons oublié de fournir le niveau de perso2.

06° Supprimer perso2.niveau=3 et relancer.

Et oui, ça plante car pour perso2 :

AttributeError: 'Personnage' object has no attribute 'niveau'

Or, la programmation objet a pour but (entre autres raisons) de fournir des codes solides et stables. Pas terrible. Comment faire pour éviter cette erreur ?

On peut rajouter un try except, mais c'est lourd. Non, le mieux, c'est de fournir des valeurs par défaut dès la CREATION de l'objet.

Python 3 vs Python 2

class Personne(object): ou class Personnage ?

Si vous cherchez des codes dans une documentation, vous trouverez surement des classes définies à l'aide de class Personnage(object):. Il s'agit d'une ancienne façon de déclarer les classes en Python 2. Ce terme object fut rajouté suite à une mise à jour de façon à permettre aux classes de Python 2 de se comporter réellement comme de vraies classes. Comme cette déclaration est rétrocompatible en Python 3, on trouve beaucoup de codes qui incluent cette façon de faire. Mais en Python 3, si vous ne notez rien entre les parenthèses, Python réagira comme si vous aviez rajouté object.

En résumé, en Python 3, que vous notiez class Personnage: ou class Personnage(object): vous obtiendrez la même chose.

Nous expliquerons d'où vient cet object dans la troisième activité sur les objets.

3 - Instanciation et autres méthodes

L'instanciation (avec un c) veut dire création d'un objet à partir d'une classe. On dit qu'on crée une nouvelle instance de la classe (du "moule").

Cette instanciation se fait visiblement automatiquement. Mais, on peut également l'écrire et ainsi donner des valeurs par défaut à certaines variables.

Comme ces variables seront contenues dans la définition même de la Classe, on parle de variables de variables propres, de variables de classes ou de variables d'instance. Nous verrons les différences entre ces termes plus tard.

Pour cela, nous allons définir DANS la définition de la classe la définition d'une fonction qui fera cela. Il s'agit donc d'une méthode puisque la fonction ne pourra s'appliquer qu'aux objets de cette classe.

La méthode qui crée un nouvel objet (on dit une nouvelle instance de la Classe) est nommée la méthode constructeur. La méthode constructeur en Python est la méthode spéciale __new__(class). Nous ne traiterons ici que d'une partie de la construction : la méthode qui permet d'initialer les attributs d'une instance.

Son nom est nécessairement __init__.

Et pourquoi mettre deux underscores devant le nom new et init ?

C'est simple : c'est la codification utilisée dans Python pour indiquer qu'il s'agit de méthodes spéciales.

En quoi sont-elles spéciales ?

Encore une fois, c'est simple : ce sont des méthodes que Python appelle lui même lorsque certaines événements ont lieu. Ainsi __init__ est utilisée dès qu'on crée une nouvelle instance. Mais ce n'est pas vous qui allez lancer son appel. Le simple fait de créer une nouvelle instance avec Personnage va faire s'activer cette méthode "spéciale".

Voici le début pour notre classe Personnage :

class Personnage:

    "Ceci est une classe permettant de créer un personnage dans mon super RPG"

    def __init__(self) :

        self.nom = 'nobody'

        self.prenom = 'nobody'

        self.niveau = 0

        self.classe = 'aucune classe'

07° Rajouter la méthode constructeur. Vérifier que cette fois-ci, on puisse omettre de déclarer certaines choses sans que cela ne provoque une erreur.

Une solution générale aux questions posées ici se trouve en fin d'activité si vous bloquez.

08° Créer une nouvelle méthode avec def presentation(self): qui affiche une présentation détaillée du personnage, dont le niveau.

09° Faire l'appel à cette méthode depuis le programme principal avec par exemple bob_skywalker.presentation(). Vérifier que tout fonctionne correctement.

accès aux méthodes

Imaginons maintenant que nous voulions enlever un niveau à chaque fois qu'un personnage perd un combat.

10° Faire la modification de la fonction combat de façon à faire perdre un niveau au perdant.

...CORRECTION...

def combat(p1,p2) :

    "Ceci est une fonction qui renvoie le nom du combattant qui perd"

    perdant = ''

    test = True

    while test :

        valeur1 = random.randint(1,6)+p1.niveau

        valeur2 = random.randint(1,6)+p2.niveau

        if valeur1>valeur2 :

            perdant = p2

            test = False

        elif valeur2>valeur1 :

            perdant = p1

            test = False

    perdant.niveau = perdant.niveau - 1

    return(perdant)


On aurait pu également taper :

    perdant.niveau -= 1

Et ça, c'est le mal en programmation objet : vous venez normalement de permettre à une fonction externe à votre objet de modifier les variables propres de votre objet. Cela veut dire que vous permettez à n'importe qui de venir mettre le bazar dans votre beau personnage.

Règle à respecter : en programmation objet, seules les méthodes de la Classe doivent permettre aux variables propres d'être modifiées. De cette façon, on est presque certain que le code sera solide : les modifications ne pourront être assurées que via votre code. En gros, vous allez créer une nouvelle méthode qui va enlever un niveau plutôt que de laisser le programme principal le faire tout seul.

11° Créer la méthode perdre_un_niveau, propre à la classe Personnage. N'oubliez pas le self obligatoire en paramètre.

...CORRECTION...

    def perdre_un_niveau(self) :

    "Ceci est une méthode qui diminue le niveau du personnage de 1"

        self.niveau -= 1

Votre méthode devra bien entendu être placée dans votre Classe, sous la méthode __init__ par exemple.

12° Modifier le programme pour que le perdant perde un niveau suite à l'action de la méthode plutôt que suite à la modification directe du niveau.

...CORRECTION...

def combat(p1,p2) :

    "Ceci est une fonction qui renvoie le nom du combattant qui perd"

    perdant = ''

    test = True

    while test :

        valeur1 = random.randint(1,6)+p1.niveau

        valeur2 = random.randint(1,6)+p2.niveau

        if valeur1>valeur2 :

            perdant = p2

            test = False

        elif valeur2>valeur1 :

            perdant = p1

            test = False

    perdant.perdre_un_niveau()

    return(perdant)

Mais on peut même aller plus loin : dans le programme principal, on affiche le perdant en allant rechercher à la main son nom :

print( combat(bob_skywalker,perso2).nom )

print( combat(bob_skywalker,perso2).nom )

C'est mal aussi : on verra par la suite, qu'on devrait faire autrement. Mais gardons cela pour l'instant.

La fonction combat n'a pas vraiment sa place non plus : pourquoi ne pas créer une méthode de la classe Personnage qui attend un adversaire en paramètre et qui gère automatiquement le combat entre le personnage sur lequel on agit et son adversaire qu'on fournit ?

Depuis le programme principal, son appel donnerait quelque chose comme bob_skywalker.combat(perso2).

13° A vous de faire. Vous aurez certainement besoin de faire quelque recherche. La solution est en fin d'activité si vous ne trouvez pas.

Résumons :

Un objet est créé à partir d'un "moule" qu'on nomme une Classe dont le nom commence par une majuscule, pour différencier la classe des autres types d'entités.

Un objet est une entité qui comporte des variables propres ( attributs ) et des fonctions propres (méthodes ). Ces éléments sont décrits lors de la définition de la Classe. Tous les objets d'une même classe possèdent donc des variables internes et des méthodes similaires.

Lorsqu'on crée un nouvel objet, on dit qu'on a une nouvelle instance de la classe.

Pour lancer l'exécution de la méthode afficher sur l'objet contenu dans la variable mon_truc, il faut utiliser un point entre les deux sous la forme objet.methode : mon_truc.afficher().

Par contre, pour faire référence à l'instance en cours d'utilisation depuis l'intérieur même du code de défintion de la Classe, on utilise le mot-clé self : self.afficher().

D'ailleurs, contrairement aux fonctions, toutes les méthodes doivent comporter un premier argument qu'on notera self par convention.

En gros, lorsque vous tapez mon_truc.afficher() et que la méthode est définie par def afficher(self), Python va faire comme si on avait taper afficher(self=mon_truc) pour une fonction classique. On recoit donc automatiquement l'objet mon_truc dans le paramètre self.L'avantage du point est qu'on sait immédiatement qu'on fait référence à une méthode issue d'une classe.

La méthode constructeur porte le même nom que la classe qu'on a créer. Elle va faire appel à def __init__ en Python.

En programmation objet, on ne laissera jamais quelqu'un agir directement sur l'une des variables internes. On préférera créer une méthode mutateur qui agira sur la variable : ainsi, pour agir sur le niveau de perso2, on crée une méthode perdre_un_niveau de façon à taper : perso2.perdre_un_niveau() plutôt que perso2.niveau +=-1 depuis le programme principal.

Pourquoi ? Quel est l'intérêt ? C'est l'un des thèmes de la prochaine activité Objet !

Dans le monde réel, nous pourrions prendre le cas des voitures.

La classe est définie par le fichier informatique contenant la modélisation 3D, les circuits électroniques ...

Si vous lancez la chaîne de production, vous obtenez un objet, une instance de la Classe : une voiture réelle.

Vous pourriez utiliser la même chaîne de production et obtenir une seconde voiture, identique mais indépendante.

Par contre, une fois créés, les objets peuvent être peints, ou peut leur rajouter des nouveaux éléments ... Bref, ils sont issus du même moule mais sont bien des objets différents.

Solution proposée :

#!/usr/bin/env python

# -*- coding: utf-8 -*-


import random


# - - - - - - - - - - - - - - - - - -

# Déclarations des classes

# - - - - - - - - - - - - - - - - - -


class Personnage:

    "Ceci est une classe permettant de créer un personnage dans mon super RPG"


    def __init__(self) :

        self.nom = 'nobody'

        self.prenom = 'nobody'

        self.niveau = 0

        self.classe = 'aucune classe'


    def presentation(self) :

        "Affiche quelques unes des valeurs stockées dans le personnage"

        print(self.nom, ' ',self.prenom)

        print(self.classe, ' de niveau ',self.niveau)


    def perdre_un_niveau(self) :

        "Cette méthode permet de réduire le niveau de 1"

        self.niveau +=-1


    def combat(self,adversaire) :

        "Ceci est une méthode qui renvoie le nom du combattant qui perd"

        perdant = ''

        test = True

        while test :

            valeur1 = random.randint(1,6)+self.niveau

            valeur2 = random.randint(1,6)+adversaire.niveau

            if valeur1>valeur2 :

                perdant = adversaire

                adversaire.presentation()

                test = False

            elif valeur2>valeur1 :

                perdant = self

                self.presentation()

                test = False

        perdant.perdre_un_niveau()

        return(perdant)


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -


# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


bob_skywalker = Personnage()

bob_skywalker.nom = "Skywalker"

bob_skywalker.prenom = "Bob"

bob_skywalker.niveau = 1

bob_skywalker.classe = "Jedi raté"


perso2 = Personnage()

perso2.nom = "De nom je n'ai pas"

perso2.prenom = "Yoda"

perso2.niveau = 3

perso2.classe = "Vieux Jedi"


print(bob_skywalker.combat(perso2).nom, " vient de perdre un combat")

print(bob_skywalker.combat(perso2).nom, " vient de perdre un combat")

Si la fin du code vous semble compliqué à comprendre, c'est normal : l'écriture est condensée. Ce n'est pas forcément un peu positif !

Voilà, ce qu'on aurait pu écrire pour tenter d'être plus clair :

persoAyantPerdu = bob_skywalker.combat(perso2)

print(persoAyantPerdu.nom, " vient de perdre un combat")

persoAyantPerdu2 = bob_skywalker.combat(perso2)

print(persoAyantPerdu2.nom, " vient de perdre un combat")

4 - Mini-projet

Nous allons maintenant appliquer ceci à une animation Tkinter.

Nous allons créer une classe Balle qui contiendra le code d'un item destiné à être intégré dans un Canvas.

Nous allons créer un item balle_1 à l'aide du constructeur Balle pour constater qu'on crée bien une balle qui rebondit sur les bords du Canvas.

Bref, vous aurez besoin d'avoir vu les activités suivantes :

14° Lancer le code suivant pour vérifier qu'il fonctionne.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

import random


# - - - - - - - - - - - - - - - - - -

# Déclarations des classes

# - - - - - - - - - - - - - - - - - -


class Balle:

    """Ceci est une classe permettant de créer un item rond en transmettant

    * un Canvas qu'on transmet en paramètre :

    * la coordonnée initiale xc du centre

    * la coordonnée initiale yc du centre

    Les balles ont de base un rayon de 5 pixels

    La référence de l'item est stockée dans leItem

    """


    def __init__(self,leCanvasMaitre,xc,yc) :

        self.rayon = 5

        self.couleur = 'black'

        self.leCanvas = leCanvasMaitre

        x0 = xc - self.rayon

        y0 = yc - self.rayon

        x1 = xc + self.rayon

        y1 = yc + self.rayon

        self.leItem = self.leCanvas.create_arc(x0,y0,x1,y1,fill=self.couleur,start=0,extent=359)

        # On génére les deux vitesses absolues initiales

        self.vitX0 = random.randint(2,6)

        self.vitY0 = random.randint(2,6)

        # On génére aléatoirement le sens de déplacement initial

        signeX = 1

        if random.randint(0,1)==1 :

            signeX = -1

        signeY = 1

        if random.randint(0,1)==1 :

            signeY = -1

        self.vitX = signeX * self.vitX0

        self.vitY = signeY * self.vitY0


    def animation(self):

        # on recupère les 2 références (canvas et item) pour plus de clarté

        leCanvas = self.leCanvas

        laBalle = self.leItem

        # liste_coord est une liste contenant les coordonnées du carré contenant la balle

        liste_coord = leCanvas.coords(laBalle)

        # liste_items est une liste contenant les items en contact avec notre balle, notre balle comprise

        liste_items = leCanvas.find_overlapping(liste_coord[0],liste_coord[1],liste_coord[2],liste_coord[3])


        if liste_coord[2]>500 :

            self.vitX = -self.vitX0

        elif liste_coord[0]<0 :

            self.vitX = self.vitX0

        elif liste_coord[1]<0 :

            self.vitY = self.vitY0

        elif liste_coord[3]>600 :

            self.vitY = -self.vitY0


        leCanvas.move(laBalle,self.vitX,self.vitY)


        fen_princ.after(50, self.animation)


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -

def vide():

    print("Rien")

# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


# Création de la fenetre

fen_princ = Tk()

fen_princ.title("DES BALLES, DES BALLES, DES BALLES")

fen_princ.geometry("600x620")


# Création du Canvas

monCanvas = Canvas(fen_princ, width=500, height=600, bg='ivory', bd=0, highlightthickness=0)

monCanvas.grid(row=0,column=0, padx=10,pady=10)


# Création des balles dans le Canvas

balle_1 = Balle(monCanvas, 50,100)

balle_1.animation()

balle_2 = Balle(monCanvas, 200,200)

balle_2.animation()


# Création d'une zone sur la droite avec des boutons

zone2 = Frame(fen_princ, bg='#777777')

zone2.grid(row=0,column=1,ipadx=5)


Label(zone2, text="Controles",width=7).pack(fill=X)


but_A = Button(zone2, text="A", fg="yellow", bg="black", command=vide)

but_B = Button(zone2, text="B", fg="yellow", bg="black", command=vide)

but_C = Button(zone2, text="C", fg="yellow", bg="black", command=vide)

but_D = Button(zone2, text="D", fg="yellow", bg="black", command=vide)

but_A.pack(fill=X)

but_B.pack(fill=X)

but_C.pack(fill=X)

but_D.pack(fill=X)


# - - - - - - - - - - - - - - - - - -

# Surveillance de la fenetre

# - - - - - - - - - - - - - - - - - -

fen_princ.mainloop()

La création (instanciation) des balles se fait ici :

# Création des balles dans le Canvas

balle_1 = Balle(monCanvas, 50,100)

balle_1.animation()

balle_2 = Balle(monCanvas, 200,200)

balle_2.animation()


Comme vous le voyez, le lancement du déplacement de la balle se fait "manuellement" : on doit appliquer la méthode animation à l'objet-Balle.

15° Créer une troisième balle sans la faire bouger. Refaire alors le même programme mais en faisant bouger la balle dès le départ (comme les deux premières).

...CORRECTION...

Création simple :

balle_3 = Balle(monCanvas, 400,100)


Création et déplacement :

balle_3 = Balle(monCanvas, 400,100)

balle_3.animation()

Comme vous l'avez vu, nul besoin de connaitre le code interne des objets pour savoir les manipuler : il suffit de connaitre les méthodes permettant de les manipuler. Ici, il s'agit du constructeur Balle et de la méthode animation.

On peut même aller plus loin et ne pas garder les références : on peut simplement créer des objets à l'aide d'une boucle :

16° Remplacer les créations de balles par les lignes suivantes où on crée 10 balles situées en 250,250 à l'aide d'une boucle. Comme on ne stocke pas la référence, on lance du coup immédiatement la méthode animation. Tester :

for compteur in range(10) :

    Balle(monCanvas, 250,250).animation()

17° Remplacer les coordonnées par une génération aléatoire entre 100 et 400 par exemple du point de départ.

...CORRECTION...

    Balle(monCanvas, random.randint(100,400),random.randint(100,400)).animation()

Par contre, si on veut modifier une classe, il faut aller voir son code. C'est ce que nous allons faire. J'aimerai que les boules disparaissent si elles se percutent.

Commençons par analyser la méthode spéciale __init__ qui est lancée automatiquement lorsqu'on utilise la méthode constructeur Balle :

    def __init__(self,leCanvasMaitre,xc,yc) :

        self.rayon = 5

        self.couleur = 'black'

        self.leCanvas = leCanvasMaitre

        x0 = xc - self.rayon

        y0 = yc - self.rayon

        x1 = xc + self.rayon

        y1 = yc + self.rayon

        self.leItem = self.leCanvas.create_arc(x0,y0,x1,y1,fill=self.couleur,start=0,extent=359)

        # On génére les deux vitesses absolues initiales

        self.vitX0 = random.randint(2,6)

        self.vitY0 = random.randint(2,6)

        # On génére aléatoirement le sens de déplacement initial

        signeX = 1

        if random.randint(0,1)==1 :

            signeX = -1

        signeY = 1

        if random.randint(0,1)==1 :

            signeY = -1

        self.vitX = signeX * self.vitX0

        self.vitY = signeY * self.vitY0

La première ligne nous indique qu'on doit fournir la référence du canvas dans lequel on doit dessiner (qui sera mémorisée dans le paramètre leCanvasMaitre). Lors de l'appel, on doit fournir également les coordonnées x et y initiales (que seront placées les paramètres xc et yc).

    def __init__(self,leCanvasMaitre,xc,yc) :

On voit ensuite qu'on commence à stocker des choses dans des variables d'instance nommées : rayon, couleur, leCanvas :

        self.rayon = 5

        self.couleur = 'black'

        self.leCanvas = leCanvasMaitre

18° Comment accéder à la référence du Canvas à l'intérieur de la méthode __init__ (deux façons à trouver) ? A l'intérieur du code de la classe ? (une façon à trouver) ? Comment connaitre cette référence si on est dans le programme principal ?

...CORRECTION...

Dans la méthode __init__ :

leCanvasMaitre

self.leCanvas


Dans le code de la Classe :

self.leCanvas


A l'extérieur de la Classe (corps du programme par exemple) :

Il faut connaitre la référence de l'objet Balle. Si la référence est stockée dans la variable balle_1 :

balle_1.leCanvas

19° Dans quelle variable interne a-t-on stocké la référence de l'item lui-même (le rond qu'on dessine dans le Canvas) ?

...CORRECTION...

On stocke la référence dans la variable d'instance qui se nomme leItem.

self.leItem = self.leCanvas.create_arc(x0,y0,x1,y1,fill=self.couleur,start=0,extent=359)

La suite du code de la méthode n'est pas inutile mais nous ne l'étudierons pas ici.

20° Je compte désormais toujours animer les balles directement. Comment modifier le code pour ne plus avoir à devoir activer la méthode animation après avoir créer la Balle ? Rajouter la ligne de code nécessaire à la fin de votre méthode __init__ et supprimer les lignes inutiles dans le corps du programme.

...CORRECTION de __init__...

    def __init__(self,leCanvasMaitre,xc,yc) :

        self.rayon = 5

        self.couleur = 'black'

        self.leCanvas = leCanvasMaitre

        x0 = xc - self.rayon

        y0 = yc - self.rayon

        x1 = xc + self.rayon

        y1 = yc + self.rayon

        self.leItem = self.leCanvas.create_arc(x0,y0,x1,y1,fill=self.couleur,start=0,extent=359)

        # On génére les deux vitesses absolues initiales

        self.vitX0 = random.randint(2,6)

        self.vitY0 = random.randint(2,6)

        # On génére aléatoirement le sens de déplacement initial

        signeX = 1

        if random.randint(0,1)==1 :

            signeX = -1

        signeY = 1

        if random.randint(0,1)==1 :

            signeY = -1

        self.vitX = signeX * self.vitX0

        self.vitY = signeY * self.vitY0

        self.animation()

...CORRECTION de la création des balles...

Plus besoin de faire référence à la méthode qui lance l'animation. Le code suivant suffit :

for compteur in range(10) :

    Balle(monCanvas, random.randint(100,400), random.randint(100,400))

Il nous reste à gérer les collisions. Pour cela, j'ai créer dans la méthode animation, la liste liste_items.

        liste_items = leCanvas.find_overlapping(liste_coord[0],liste_coord[1],liste_coord[2],liste_coord[3])

Pour rappel, cette liste créée avec la méthode find_overlapping contient les références des items qui sont en contact avec le rectangle dont on fournit les coordonnées du point haut-gauche et bas-droit.

21° Faire changer changer en rouge la couleur de la balle une fois qu'elle a touché une autre balle. Il suffit de rajouter quelques lignes de code sous la ligne précédente. Il va falloir utiliser la méthode itemconfig :

...CORRECTION ...

        liste_items = leCanvas.find_overlapping(liste_coord[0],liste_coord[1],liste_coord[2],liste_coord[3])

        if len(liste_items) > 1 :

            leCanvas.itemconfig(laBalle, fill='red')


Ou alors, si vous ne voulez pas utiliser les variables locales qu'on a créé dans la fonction pour que le code soit plus clair, ca devient rapidement moins sympa :


        liste_items = leCanvas.find_overlapping(liste_coord[0],liste_coord[1],liste_coord[2],liste_coord[3])

        if len(liste_items) > 1 :

            self.leCanvas.itemconfig(self.leItem, fill='red')

Dernière chose et on ferme : je ne veux plus mettre la Balle en rouge, je veux la détruire avec la méthode delete des Canvas :

leCanvasATraiter.delete(referenceDeItemAFaireDisparaitre)

22° Utiliser la méthode de la façon la plus simple qui soit. Tester : vous devriez constater qu'il n'y a que la Balle ayant executé la méthode a ce moment qui disparait et qu'en plus cela génére des erreurs dans le reste du script.

...CORRECTION ...

        liste_items = leCanvas.find_overlapping(liste_coord[0],liste_coord[1],liste_coord[2],liste_coord[3])

        if len(liste_items) > 1 :

            leCanvas.delete(laBalle)

Pour finir, voilà une façon de faire : on va faire disparaitre tous les items, pas uniquement celui de l'objet en cours :

        liste_items = leCanvas.find_overlapping(liste_coord[0],liste_coord[1],liste_coord[2],liste_coord[3])

        if len(liste_items) > 1 :

            for item_balle in liste_items :

                leCanvas.delete(item_balle)

Si vous lancez ceci, vous pourrez constater qu'il y a encore des erreurs : on ne détruit pas l'objet mais l'item (le rond noir dessiné dans le Canvas).

On continue donc à suivre le code dans l'objet actuel et les autres objets relancent aussi leur méthode animation. Même si l'item qu'ils stockent normalement a disparu !

Comme on ne garde pas les références des balles, on ne peut pas simplement supprimer le contenu de la variable d'instanciation leItem. Une solution consiste :

C'est loin d'être parfait en terme de gestion de la mémoire, mais encore une fois, il fallait y réflechir AVANT : c'est plus facile de détruire réellement les références aux objets si on garde leurs références.

#!/usr/bin/env python

# -*- coding: utf-8 -*-

from tkinter import *

import random


# - - - - - - - - - - - - - - - - - -

# Déclarations des classes

# - - - - - - - - - - - - - - - - - -


class Balle:

    """Ceci est une classe permettant de créer un item rond en transmettant

    * un Canvas qu'on transmet en paramètre :

    * la coordonnée initiale xc du centre

    * la coordonnée initiale yc du centre

    Les balles ont de base un rayon de 5 pixels

    La référence de l'item est stockée dans leItem

    """


    def __init__(self,leCanvasMaitre,xc,yc) :

        self.rayon = 5

        self.couleur = 'black'

        self.leCanvas = leCanvasMaitre

        x0 = xc - self.rayon

        y0 = yc - self.rayon

        x1 = xc + self.rayon

        y1 = yc + self.rayon

        self.leItem = self.leCanvas.create_arc(x0,y0,x1,y1,fill=self.couleur,start=0,extent=359)

        # On génére les deux vitesses absolues initiales

        self.vitX0 = random.randint(2,6)

        self.vitY0 = random.randint(2,6)

        # On génére aléatoirement le sens de déplacement initial

        signeX = 1

        if random.randint(0,1)==1 :

            signeX = -1

        signeY = 1

        if random.randint(0,1)==1 :

            signeY = -1

        self.vitX = signeX * self.vitX0

        self.vitY = signeY * self.vitY0

        self.animation()


    def animation(self):

        # on recupère les 2 références (canvas et item) pour plus de clarté

        leCanvas = self.leCanvas

        laBalle = self.leItem

        # liste_coord est une liste contenant les coordonnées du carré contenant la balle

        liste_coord = leCanvas.coords(laBalle)

        if liste_coord :

            # liste_items est une liste contenant les items en contact avec notre balle, notre balle comprise

            liste_items = leCanvas.find_overlapping(liste_coord[0],liste_coord[1],liste_coord[2],liste_coord[3])

            if len(liste_items) > 1 :

                for item_balle in liste_items :

                    leCanvas.delete(item_balle)

            else :

                if liste_coord[2]>500 :

                    self.vitX = -self.vitX0

                elif liste_coord[0]<0 :

                    self.vitX = self.vitX0

                elif liste_coord[1]<0 :

                    self.vitY = self.vitY0

                elif liste_coord[3]>600 :

                    self.vitY = -self.vitY0


                leCanvas.move(laBalle,self.vitX,self.vitY)


                fen_princ.after(50, self.animation)


# - - - - - - - - - - - - - - - - - -

# Déclarations des fonctions

# - - - - - - - - - - - - - - - - - -

def vide():

    print("Rien")

# - - - - - - - - - - - - - - - - - -

# Corps du programme

# - - - - - - - - - - - - - - - - - -


# Création de la fenetre

fen_princ = Tk()

fen_princ.title("DES BALLES, DES BALLES, DES BALLES")

fen_princ.geometry("600x620")


# Création du Canvas

monCanvas = Canvas(fen_princ, width=500, height=600, bg='ivory', bd=0, highlightthickness=0)

monCanvas.grid(row=0,column=0, padx=10,pady=10)


# Création des balles dans le Canvas

for compteur in range(30) :

    Balle(monCanvas, random.randint(100,400),random.randint(100,400))


# Création d'une zone sur la droite avec des boutons

zone2 = Frame(fen_princ, bg='#777777')

zone2.grid(row=0,column=1,ipadx=5)


Label(zone2, text="Controles",width=7).pack(fill=X)


but_A = Button(zone2, text="A", fg="yellow", bg="black", command=vide)

but_B = Button(zone2, text="B", fg="yellow", bg="black", command=vide)

but_C = Button(zone2, text="C", fg="yellow", bg="black", command=vide)

but_D = Button(zone2, text="D", fg="yellow", bg="black", command=vide)

but_A.pack(fill=X)

but_B.pack(fill=X)

but_C.pack(fill=X)

but_D.pack(fill=X)


# - - - - - - - - - - - - - - - - - -

# Surveillance de la fenetre

# - - - - - - - - - - - - - - - - - -

fen_princ.mainloop()

Voilà. C'est encore grandement améliorable. On pourrait faire mieux sur la gestion des collisions : les rectangles c'est bien mais la forme est circulaire en réalité... Et vous avez plus de boutons qui ne servent à rien sur le côté : vous pourriez les utiliser pour générer de nouvelles balles, tenter de changer les couleurs ... Et vous risquez de vous rendre compte qu'il faudra un moment ou à un autre stocker ou récupérer les références des objets-Balle qu'on crée.