python objets

Identification

Infoforall

38 - Création d'objets avec Python


Nous allons aujourd'hui voir comment utiliser les classes d'objets dans le cadre de la programmation. Vous les avez déjà rencontrées plus d'une fois sans forcément vous en rendre compte. En réalité, dans Python, tout est objet ou presque.

Nous allons donc définir aujourd'hui les termes suivants : objet, Classe, attribut, méthode...

La P.O.O c'est Classe
La Programmation Orienté Objet, car vous avez la Classe !

Prérequis : savoir utiliser Python :o)

Logiciel nécessaire pour l'activité : Python 3 : Thonny, IDLE ...

Evaluation : 8 questions

  question 02

  question 03-04-07-09-12-13-15

Exercices supplémentaires 🏠 : oui

Documents de cours : open document ou pdf

Documents uniquement : open document ou pdf

1 - Rappel pour percevoir ce qu'est un objet

Cette partie est destinée à être lue chez vous avant le cours lui-même.

Résumons les variables et types de données associés que nous avons vu pour le moment :

RAPPEL : référence, adresse ou identifiant

Rappelons qu'on peut localiser une zone mémoire de plusieurs façons :

  1. Avec une adresse réelle : une vraie adresse-mémoire
  2. Avec une adresse symbolique : un identifiant ou une référence permettant de trouver une vraie adresse-mémoire

Dans le cadre de Python, les contenus sont caractérisés par un identifiant. Néanmoins, j'utiliserai "adresse", plus simple et explicite. Mais en Python, il s'agit bien d'identifiants en réalité.

1.1 Variable et type simple

A - Modèle simple de la boîte

La variable "simple" fait référence à une donnée de type simple : un entier (type int), ou un flottant (type float). Le modèle de la "boîte" est suffisant pour comprendre leur fonctionnement apparent.

>>> a = 5 >>> b = 9
représentation en boîte
B - Principe d'organisation interne

En Python, nous avons vu que cette sorte de variable se comporte bien comme cela mais que l'implémentation réelle est différente et comporte un espace des noms et un espace mémoire.

  • L'espace des noms fait le lien entre le nom de la variable et une adresse
  • L'espace mémoire contient des données rangées dans des zones identifiées par leurs adresses.
espace des noms et mémoire

C'est la raison pour laquelle, en Python, l'adresse d'une variable change lorsqu'on lui donne une nouvelle affectation. Attention, les variables ne fonctionnent pas nécessairement sous ce principe dans d'autres langages.

C - Comportement public visible vu de l'extérieur

Connaître l'adresse d'une variable est rarement utile à un humain.

Conséquence : par défaut, Python affiche le contenu vers lequel mène l'adresse plutôt que l'adresse elle-même lorsqu'on lui demande d'afficher juste a.

>>> a = 5 >>> a 5

Si vous voulez connaître l'adresse, il en faire la demande explicitement (pour annuler le comportement par défaut) : il faut utiliser la fonction native id().

>>> id(a) 1485

Comme vous pouvez vous en douter à cause du nom de la fonction, Python gère plutôt l'espace des noms à l'aide d'identifiants qu'à l'aide d'adresses réelles.

D - Différence entre réel privé et visible public

Le fonctionnement interne d'un système n'a pas forcément à voir avec son comportement visible. La mécanique interne n'est pas visible de l'extérieur.

1.2 Variable et type construit ("conteneur")

A - Modèle simple de l'armoire-conteneur

Une variable "conteneur" fait référence à une structure de données qui contient plusieurs "casiers".

Ce type de variable correspond donc à une sorte d'armoire comportant des casiers. Dans le cas des tableaux, des tuples et des strings, les casiers sont numérotés et on peut donc les identifier à l'aide d'un numéro d'indice. Dans le cas du dictionnaire, les casiers sont identifiés par une clé.

Dans tous les cas en Python, on accède au contenu avec des crochets et la syntaxe nom[i] ou nom[clé].

B - Principe d'organisation interne
  • La variable d'un conteneur contient l'adresse de la structure et pas son contenu directement
  • La syntaxe des crochets est à comprendre de cette façon :
    • t[i] : va à l'adresse du tableau et ramène le contenu de la case d'indice i.
    • d[cle] : va à l'adresse du dictionnaire et ramène le contenu de la valeur associée à la clé.
  • Sur cet exemple fictif, j'ai pris un tableau de caractères, tous encodés sur 1 octet. L'adresse d'une case correspond donc à l'adresse du tableau plus la valeur de l'indice (adr = A + i*1).

Exemple

t[0] mène vers l'adresse 1485 qui mène à l'adresse 68 qui fait référence à 'a'
t[1] mène vers l'adresse 1486 qui mène à l'adresse 48 qui fait référence à 'b'
t[2] mène vers l'adresse 1487 qui mène à l'adresse 24 qui fait référence à 'c'.

B - Comportement visible vu de l'extérieur

Connaître l'adresse de la variable t est rarement utile à un humain.

Conséquence : par défaut, Python affiche le contenu de la structure plutôt que l'identifiant.

>>> t = ['a', 'b', 'c'] >>> t ['a', 'b', 'c']

Pour connaître l'"adresse", il faut utiliser la fonction native id().

>>> id(t) 1485
C - Différence réel privé et visible public

Comme avec les types simples. Ne confondez pas ce que l'interface vous montre et le fonctionnement réel privé de Python.

Exemple 1 de comportement extérieur incompréhensible avec le modèle simple des boîtes

La variable t étant bien l'adresse de la structure-tableau (et pas son contenu), on peut l'utiliser pour aller sur place et modifier le contenu des cases sans modifier l'adresse du tableau lui-même : vous pouvez modifier la page 5 d'un classeur sans devoir changer de classeur à chaque fois.

Ce qu'on voit via l'interface :

>>> t[0] = 'z' >>> t ['z', 'b', 'c'] >>> id(t) 1485

Ce qui se passe en interne : l'ordinateur a juste modifié vers où aller à partir de l'adresse 1485 : on a maintenant Adr.1485 -> Adr.100, alors qu'avant nous avions Adr.185 -> Adr.68.

Autre exemple de comportement extérieur incompréhensible avec le modèle simple des boîtes

Le système (espace des noms - mémoire réelle) permet de comprendre la différence entre :

  • La création d'un alias d'un tableau (deux variables menant vers la même adresse et donc la même structure interne). Si on modifie l'une, on touche à l'autre puisqu'en réalité, il s'agit juste de deux noms menant à la même zone mémoire.
  • >>> v = t >>> id(t) 1485 >>> id(v) 1485 >>> t ['z', 'b', 'c'] >>> v ['z', 'b', 'c'] >>> t[0] = 'A' >>> t ['A', 'b', 'c'] >>> v ['A', 'b', 'c']
  • La création d'une copie d'un tableau (deux variables menant vers deux adresses différentes et donc deux structures internes dont le contenu est initialement identique). Si on modifie l'une, on ne touche pas à l'autre puisqu'il s'agit juste de deux zones mémoire différente.
  • >>> v = [e for e in t] >>> id(t) 1485 >>> id(v) 2000 >>> t ['z', 'b', 'c'] >>> v ['z', 'b', 'c'] >>> t[0] = 'A' >>> t ['A', 'b', 'c'] >>> v ['z', 'b', 'c']
1.3 Variable et objet

Les objets sont des structures qui peuvent contenir à la fois des données internes et des fonctions internes.

Vous avez déjà manipulé plusieurs fois des objets, puisque vous avez utilisé des méthodes : une méthode est une fonction particulière, une fonction qui est intégrée dans un objet.

Objet Turtle

Voici comment on trace un trait rouge avec le module de dessin Turtle :

1 2 3 4 5
import turtle as trt crayon = trt.Turtle() # Crée l'objet et stocke son adresse dans crayon crayon.pencolor("red") # Va à cette adresse et tu y trouvera une fonction pencolor() crayon.forward(200) # Va à cette adresse et tu y trouvera une fonction forward()

En ligne 2, on crée un nouveau objet de classe Turtle en utilisant le constructeur nommée Turtle() contenu dans le module connu sous le nom trt.

Les "fonctions" utilisées sont particulières puisqu'elles sont directement présentes dans l'objet et qu'on y accède avec la syntaxe objet.methode().

Objet Fichier

Voici comment on ouvre un fichier pour afficher son contenu ligne par ligne :

1 2 3 4 5
obj_fichier = open('le_fichier_voulu.txt', 'r', encoding="utf-8") for ligne in obj_fichier: print(ligne) obj_fichier.close()

En ligne 1, on crée un nouveau objet-fichier en utilisant la fonction native nommée open(). La variable obj_fichier contient alors l'adresse de notre nouvel objet.

En ligne 3, on crée une boucle en générant une variable de boucle ligne sur tous les éléments qu'on va trouver à l'adresse de notre objet obj_fichier.

En ligne 5, on ferme notre objet-fichier en allant cherche à son adresse la méthode se nommant close().

Encore une fois, on retrouve la syntaxe objet.methode().

Objet Tkinter

Voici comment on crée une fenêtre d'application graphique à partir du module Tkinter :

1 2 3 4 5 6
import tkinter as tk fenetre = tk.Tk() # Crée l'objet et stocke son adresse dans fenetre fenetre.geometry("600x300") # Va à cette adresse et tu y trouvera une fonction geometry() fenetre.title("Fait maison") # Va à cette adresse et tu y trouvera une fonction title() fenetre.configure(bg=FOND) # Va à cette adresse et tu y trouvera une fonction configure()

Encore une fois : la syntaxe objet.methode() est présente.

Si on veut résumer cela de façon très synthétique (et un peu fausse)

  • Les types simples font référence à un contenu unique
  • Les types construits sont des conteneurs stockant plusieurs contenus
  • Les objets sont des conteneurs stockant plusieurs variables (nommées attributs) et plusieurs fonctions (nommées méthodes)

Cette année, nous allons voir comment utiliser ces objets mais également comment les créer.

En réalité, en Python, tout n'est qu'objet. Même les integers sont des objets car on peut utiliser des méthodes sur les integers !

A titre d'exemple, appliquons la méthode bit_length() qui permet d'obtenir le nombre de bits nécessaires pour encoder un entier. Pour 5, c'est 3 puisqu'on a 5 10 = 101 2.

>>> n = 5 >>> n.bit_length() 3

Comme quoi, même les choses qu'on pouvait croire simples, peuvent être complexes sous le capot.

simplicité jusqu'à ce qu'on regarde les codes importés

Maintenant que vous voyez qu'un objet est une structure de données contenant d'autres variables (ses attributs) et des fonctions (ses méthodes), voyons comment créer des objets.

2 - Instanciation (création d'un objet)

Lorsqu'on veut créer un objet dans la vraie vie, on a besoin d'une notice, d'un plan, d'une recette.

Si vous avez la Recette pour faire un gâteau, vous serez capable d'en créer autant que vous le voulez.

C'est pareil en informatique.

2.1 - Classe et instance d'une classe

Les instructions d'une Recette permettent de réaliser de vrais gâteaux, tous un peu similaires mais potentiellement différents : tous les gâteaux n'auront pas les mêmes caractéristiques (forme, couleur, taille, goût...).

Réalisation de gateaux différents à partir d'une même recette

Les instructions d'une Classe permettent de réaliser de vrais objets : ils auront au départ des attributs (des variables) dont les contenus sont identiques mais pourront être personnalisés.

2.2 Création d'une Classe vide

A - Déclaration d'une Classe (le moule)

Créer une Classe en Python nécessite l'utilisation

  • du mot-clé class suivi
  • du nom de la Classe qui commence par une Majuscule par convention
  • et on finit par : (comme toute déclaration en Python)

On placera une docstring d'une ligne décrivant la Classe.

Les instructions à réaliser sont décalées de 4 espaces comme avec def, for, while, if... Ici, l'instruction en ligne 3 est juste pass : ne rien faire de particulier.

1 2 3
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" pass
B - Informations sur la Classe
  • Une Classe possède un type <class 'type'>.
  • Une classe possède une adresse.
  • Par défaut, la console affiche son lieu de création et son nom.
>>> type(Personnage) <class 'type'>

Avec cette réponse, Python signale qu'il s'agit d'une Classe configurée manuellement, pas d'un type natif.

>>> id(Personnage) 21741560

La classe est bien en mémoire quelque part.

>>> Personnage <class '__main__.Personnage'>

La classe Personnage est définie dans le programme principal (__main__), c'est à dire pas depuis un module.

Maintenant que nous avons notre moule, nous allons pouvoir l'utiliser pour créer des objets-personnages.

2.3 - Création d'objets

A - Instanciation

Définition Instanciation : création d'un nouvel objet à partir du Constructeur d'une Classe.

Définition Instance : synonyme d'objet. La différence est qu'on indique la classe lorsqu'on parle d'instance. On dira instance de Personnage alors qu'on parlera juste d'objet.

Définition Constructeur : pour créer un objet à partir d'une Classe, on utilise une fonction un peu particulière qu'on nomme le Constructeur. La syntaxe est simple : on utilise le nom de la Classe suivi des parenthèses !

Classe : Personnage

Constructeur : Personnage()

Objet ou Instance de Personnage : x = Personnage()

Cette fonction Constructeur renvoie l'adresse du nouvel objet.

Pas d'entrée particulière  ⇒   Constructeur   ⇒  Adresse d'un nouvel objet

Ici, la variable x contient donc la réponse de Personnage(), à savoir l'adresse d'un nouvel objet basé sur la classe Personnage.

C'est donc exactement la même chose que lorsqu'on crée un tableau list-Python : la variable contient l'adresse du conteneur.

Exemple  on crée deux objets issus du constructeur Personnage() et on les stocke dans alice et bob.

>>> alice = Personnage() >>> id(alice) 139663546393488 >>> bob = Personnage() >>> id(bob) 139663546395728 >>> id(Personnage) 21741560

On voit que les objets alice et alice ne sont pas la même chose que la classe Personnage puisque leurs adresses ne sont pas 21741560.

De la même façon, alice et bob sont deux objets indépendants puisqu'ils ont chacun leur propre adresse.

B - Tester la classe d'un objet

On peut savoir si un objet est issu d'une classe particulière de deux façons.

  • Soit avec la fonction native type() :
  • >>> type(alice) == Personnage True
  • Soit avec la fonction native isinstance() dont voici la syntaxe :
  • >>> isinstance(alice, Personnage) True >>> isinstance(alice, list) False

    Dans le cadre réduit des connaissances vues en NSI, elles auront toujours un comportement identique. Mais elles ne testent pas vraiment la même chose.

C - Comportement public visible vu de l'extérieur

Contrairement aux types natifs (int, float, list...), la console n'affiche pas automatiquement le contenu d'un objet. Si on demande d'afficher alice dans une console, l'interpréteur Python va nous donner le type de sa Classe et l'adresse mémoire de l'objet.

>>> alice <__main__.Personnage object at 0x7f25459ff9b0> >>> Personnage <class '__main__.Personnage'>

On voit bien la différence entre alice qui est une instance de Personnage, et la Classe Personnage qui est définie dans le programme principal.

Pour rappel, voici une demande équivalente basée sur le type natif list-Python :

>>> t = list() >>> t []

On voit bien la différence entre un type natif et une Classe.

Remarque

On voit une similitude avec les fonctions.

  • Déclaration : on prévoit juste une utilisation ultérieure.
  • Instanciation : c'est l'appel, l'utilisation concrète.
Le problème des adresses...

Reste un problème : on signale que l'objet est stocké en 0x7f25459ff9b0 alors qu'avec id(), nous avions obtenu 139663546393488...

La réponse à cette "bizarrerie" ? Il s'agit de la même valeur d'adresse mais

  • la fonction native id() la renvoie en décimal (base 10),
  • l'évaluation directe en console la renvoie en hexadécimal (base 16) (Python l'indique avec le préfixe 0x)
>>> id(alice) 139663546393488 >>> alice <__main__.Personnage object at 0x7f05f4122390> >>> 0x7f05f4122390 == 139663546393488 True >>> int(0x7f05f4122390) 139663546393488 >>> hex(139663546393488) '0x7f05f4122390'

3 - Mauvaise pratique : gestion des attributs à la volée

Pour l'instant, la classe Personnage est un moule vide et donc nos objets sont donc des conteneurs vides.

Voyons comment stocker des attributs, des variables rattachées à notre objet.

3.1 - Création d'attributs à la volée

A - Attributs

Un attribut est une sorte de variable placée à l'intérieur d'un objet.

B - Rajouter des attributs à la volée

On utilise la syntaxe objet.attribut = valeur.
On notera que l'affectation ne se fait pas sur l'objet mais sur son contenu.

>>> alice.agilite = 18

Traduction en français :

On agit sur alice en lui rajoutant un nouvel attribut agilite référençant 18.
Va à l'adresse d'alice et crée et stocke sur place un attribut agilite référençant 18.

Si on veut créer un cousin éloigné de Luke Skywalker, on pourrait donc faire ceci :

1 2 3 4 5 6 7 8 9 10
# Déclaration des classes class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" bob = Personnage() bob.nom = "Skywalker" bob.prenom = "Bob" bob.age = 25

Que fait ce programme ?

Ligne 7 : il crée un objet bob.

Lignes 8-9-10 : on rajoute des attributs en utilisant la syntaxe du point.

C - Accéder aux valeurs des attributs

On peut accéder à la valeur d'un attribut avec la syntaxe objet.attribut.

>>> bob.nom 'Skywalker' >>> bob.prenom 'Bob'
D - Modifier les valeurs des attributs

Les objets sont fortement muables : on peut modifier le contenu sans modifier l'adresse de l'objet lui-même.

On utilise la syntaxe objet.attribut = nouvelle_valeur

>>> bob.nom 'Skywalker' >>> bob.nom = "Bond" >>> bob.nom 'Bond'
3.2 - Principe de la structure d'un objet

Représentation mentale

Un objet possède une adresse.

L'espace des noms fait le lien entre le nom de la variable et l'adresse.

A cette adresse se trouve un espace des noms interne à l'objet lui-même. Nous n'allons pas étudier l'implémentation réelle des objets dans Python, mais vous pouvez travailler avec cette représentation :

Dans l'espace des noms, on voit que bob mène à l'adresse finissant ...680.

A cette adresse ...680, on peut considérer qu'on va trouver un espace local des noms pour cet objet.

structure d'un objet

La codification bob.niveau peut se traduire ainsi en Français : va à l'adresse de cet objet bob et utilise son espace local des noms pour trouver le contenu de l'attribut se nommant niveau.

La différence avec l'espace local des noms d'une fonction est qu'il n'est pas détruit avant la fin du programme.

Classification grossière et temporaire
  • Type simple : la boîte unique.
  • Type structuré : une armoire contenant des "cases".
  • Objet : une armoire contenant un espace des noms privés.

Jamais d'attributs créés à la volée !

Les deux problèmes du rajout d'attributs à la volée sont liés :

  1. il faudrait lire toutes les lignes de code pour retrouver le nom d'un attribut déjà créé. Pas top avec 300 000 lignes.
  2. on pourrait écraser le contenu d'un attribut qui existe déjà en utilisant par mégarde un nom d'attribut qui déjà utilisé.

Conclusion : même si Python permet de rajouter des attributs à la volée n'importe où dans le code, on veillera à ne pas le faire.

On utilise le rajout à la volée lorsqu'on réalise des petits tests ou un prototype. Jamais dans un programme réel.

C'est ici qu'intervient l'initialisation via la méthode __init__() dont l'un des buts est de centraliser la création des attributs.

Vous allez voir qu'elle est assez proche de la fonction immeuble_aleatoire() du projet rue. Nous avions centralisé la création des clés de façon à retrouver facilement leurs existences et leurs noms.

4 - Bonne pratique : gestion des attributs avec la méthode spéciale __init__()

Voyons maintenant plus en détails comment se déroule l'instanciation.

4.1 - Les 3 étapes lors de l'instanciation

En ligne 4, lLe constructeur Personnage() renvoie une nouvelle instance de la Personnage qu'on stocke ici dans alice.

1 2 3 4
class Personnage: """Une classe créant un personnage dans mon super RPG""" alice = Personnage()

Voyons les étapes que va suivre le Constructeur.

1 - Réservation d'une adresse

D'abord, le Constructeur demande la réservation d'une nouvelle place mémoire et génére l'espace des noms correspondant. Après cette étape, l'objet est créé et possède une adresse personnelle.

2 - Initialisation des attributs

Ensuite, le Constructeur fait appel automatiquement à la méthode spéciale __init__() si elle est présente dans la déclaration de la Classe. Elle permettra d'initialiser les attributs de cet instance particulière. Vous allez découvrir __init__() dans la partie suivante.

Pour l'instant, notre Classe ne contient pas de méthode __init__(). Ici, on ne fait donc... rien.

3 - Renvoi de l'adresse

Finalement, le code interne du Constructeur rencontre un return et renvoie l'adresse qu'il a réservé. A qui renvoie-t-il sa réponse ? A l'entité informatique qui a lancé l'appel au constructeur (une fonction, un programme...). Si vous ne mémorisez pas la réponse, l'objet est créé mais vous ne pourrez plus interagir facilement avec lui.

Déroulé du programme
1 2 3 4
class Personnage: """Une classe créant un personnage dans mon super RPG""" alice = Personnage()

L1(déclaration) - L4(appel constucteur) - L1 - L2(même pas) - L4(retour constructeur)

Bilan

  1. Réservation d'une adresse (et donc d'un espace mémoire)
  2. Initialisation du contenu à cette adresse
  3. Renvoi de l'adresse du nouvel objet
4.2 - La méthode spéciale __init__()

A - Rôle de __init__()

Le Constructeur lance automatiquement un appel à la méthode spéciale __init__() (c'est pour cela qu'elle est spéciale).

La méthode spéciale __init__() a pour but de centraliser la création et l'initialisation des attributs.

B - Comment déclarer une méthode dans une Classe ?

Une méthode n'est (presque) rien d'autre qu'une fonction présente directement dans l'espace des noms de l'objet. On utilise donc presque la même codification que les fonctions. La seule différence vient de la présence d'un paramètre self dont nous allons reparler.

1 2 3 4 5 6 7 8
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self): self.nom = 'aucun' self.prenom = 'aucun' self.niveau = 0 self.profession = 'aucune'

Lignes 5-6-7-8 : on comprend qu'on déclare et affecte 4 attributs nommés nom, prenom, niveau et profession.

C - Quelles conséquences lors de la création ?

Cette fois, les instances de Personnage ne sont plus vides au départ.
Ils ont 4 attributs et ces attributs possèdent les valeurs visibles sur les lignes 5 à 8.

>>> p = Personnage() >>> p.nom 'aucun' >>> p.prenom 'aucun' >>> p.niveau 0 >>> p.profession 'aucune' >>> p2 = Personnage() >>> p2.nom 'aucun' >>> p2.prenom 'aucun' >>> p2.niveau 0 >>> p2.profession 'aucune'
D - A quoi ça sert ?

Pour l'instant, à centraliser l'information sur les attributs disponibles. Nous verrons plus tard qu'on peut également calculer les contenus initiaux en fonction d'arguments envoyés au constructeur.

La méthode spéciale __init__() est donc nommée la méthode-initialisateur ou même, parfois, juste l'initialisateur.

Puisque c'est le constructeur qui l'appelle automatiquement, par simplification, on la nomme parfois méthode-constructeur ou même constructeur. C'est un abus de langage courant. Le vrai constructeur est Personnage().

4.3 Le paramètre self

A - La particularité de self

La déclaration de __init__() possède un paramètre nommé self.

Le paramètre self est particulier puisque ce n'est pas vous qui le transmettez au constructeur Personnage() pour qu'il le transmette à __init__().

1 2 3 4 5 6 7 8
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self): self.nom = 'aucun' self.prenom = 'aucun' self.niveau = 0 self.profession = 'aucune'

Voici la création d'alice :

>>> alice = Personnage()

Que contient self alors ?

B - Le contenu de self

Voici un code imaginaire simulant les trois étapes de travail du constructeur Personnage() :

Instructions de Personnage(): adresse = reserver_adresse_pour_un_objet() Personnage.__init__(adresse) return adresse

A l'étape 2, Personnage() lance automatiquement un appel à __init__() en lui envoyant la nouvelle adresse qu'il a obtenu lors de l'étape 1.

CONCLUSION : self référence AUTOMATIQUEMENT l'adresse de l'objet en cours d'utilisation.

BILAN (hyper-important pour comprendre la POO)

Le paramètre self :

  1. est toujours placé en premier dans le prototype de la méthode,
  2. contiendra l'adresse de l'objet qui a lancé l'appel à cette méthode.
  3. n'est pas envoyé par l'utilisateur mais géré à l'interne.

Si vous trouvez pénible de créer l'objet puis de devoir le remplir, c'est normal ! Ce n'est pas comme cela qu'on fait habituellement.

4.4 - Transmission d'arguments lors de la création

A - Savoir suivre le transfert

Tous les arguments envoyés au constructeur sont envoyés automatiquent également à la méthode __init__().

Un exemple :

1 2 3 4 5 6 7 8 9 10
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self, nom, pre, niv, pro): self.nom = nom self.prenom = pre self.niveau = niv self.profession = pro p = Personnage("InBorderLand", "Alice", 20, "Héroïne")

On peut alors regarder ce que contient notre premier objet :

>>> p.prenom 'Alice' >>> p.niveau 20

Si on veut comprendre la transmission d'informations, il faut comparer l'appel au contructeur et la déclaration de la méthode __init__() qui récupère automatiquement ce qu'on envoie au constructeur :

  • l'appel au constructeur Personnage() et
  • 10
    p = Personnage("InBorderLand", "Alice", 20, "Héroïne")
  • la déclaration de la méthode __init__().
  • 4
    def __init__(self, nom, pre, niv, pro):

On voit les transferts suivants :

  1. self : on ne l'a pas envoyé, ce paramètre est géré automatiquement.
  2. "InBorderLand" vers nom.
  3. "Alice" vers prenom.
  4. 20 vers niv.
  5. "Héroïne" vers pro.

Que fait-on ensuite des paramètres ?

On voit ligne 6 que pre sert à initialiser l'attribut prenom par exemple.

1 2 3 4 5 6 7 8
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self, nom, pre, niv, pro): self.nom = nom self.prenom = pre self.niveau = niv self.profession = pro
B - Pratique usuelle à ne pas confondre avec de la magie

C'est un peu pénible de ne pas nommer le paramètre de la même façon que l'attribut. Habituellement, on utilise plutôt un code qui ressemble à cela :

1 2 3 4 5 6 7 8
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self, nom, prenom, niveau, profession): self.nom = nom self.prenom = prenom self.niveau = niveau self.profession = profession

Avantage : on comprend facilement dans quel attribut sera stocké chacun des paramètres.

Désavantage : on pourrait croire que les lignes 5-6-7-8 ne servent à rien, ce qui est totalement faux. Ce sont bien ces lignes qui font le lien entre les paramètres reçus et les attributs de l'objet.

C - Paramètre par défaut

Parfois, la majorité des arguments envoyés au constructeur seront toujours les mêmes. Par exemple, le niveau de départ des personnages à 1. Dans ce cas, on peut donner des valeurs par défaut à certains paramètres.

1 2 3 4 5 6 7 8
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self, nom='Aucun', prenom='Aucun', niveau=1, profession="Humain de base"): self.nom = nom self.prenom = prenom self.niveau = niveau self.profession = profession

Ici, tous les paramètres ont une valeur par défaut. On peut donc lancer des appels de ce type :

>>> alice = Personnage() >>> alice.niveau 1 >>> alice.profession 'Humain de base'

Remarque importante : si vous avez des paramètres sans valeur par défaut et d'autres avec valeur par défaut, il faut impérativement

  • mettre au début du prototype les paramètres normaux (sans valeur par défaut) puis
  • mettre en fin de prototype ceux avec une valeur par défaut.
  • 1 2 3 4
    class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self, nom, prenom, niveau=1, profession="Humain de base"):
D - Paramètres nommés

On peut transmettre les paramètres dans n'importe quel ordre pourvu qu'on nomme tous les paramètres qui n'ont pas de valeur par défaut.

>>> bob = Personnage(prenom="Luke", nom="Skywalker", niveau=5, profession="Jedi") >>> jim = Personnage(profession="Capitaine de l'Enterprise", nom="Kirk", niveau=6, prenom="James")

5 - Exercices

Mauvaise pratique : création d'attributs à la volée

01° Mettre le programme suivant en mémoire puis utiliser les instructions indiquées dans la console.

Le programme permet de déclarer une classe Personnage. Via la console, on parvient à créer plusieurs objets basés sur la même structure.

1 2
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG"""
>>> Personnage <class '__main__.Personnage'> >>> alice = Personnage() >>> alice <__main__.Personnage object at 0x7f25459ff9b0> >>> bob = Personnage() >>> bob <__main__.Personnage object at 0x7f25459ff898>

Questions

  1. Quelle est la particularité du nom de la classe au niveau majuscule/minuscule (cela devrait vous sauter aux yeux) ?
  2. Comment se nomment les deux variables qui contiennent des objets basés sur cette classe ?
  3. Quelles sont les deux instructions qui ont permis de créer des objets basés sur la classe Personnage ?
  4. Les deux objets font-ils référence à la même zone mémoire ?

...CORRECTION...

  • Quelle est la particularité du nom de la classe (qui devrait vous sauter aux yeux) ?
    • Le nom commence par une majuscule. Une majuscule ! Personnage et pas personnage.
  • Comment se nomment les deux variables qui contiennent des objets basés sur cette classe ?
    • On crée visiblement deux variables par affectation : alice et bob
  • Quelles sont les deux lignes qui permettent de créer des objets basés sur la classe Personnage ?
    • On crée des objets basés sur la classe Personnage en utilisant une fonction qui porte le nom de la classe utilisée (avec une majuscule aussi du coup). Par contre, s'agissant d'une fonction, on retrouve des parenthèses.
    • >>> alice = Personnage() >>> bob = Personnage()
  • Les deux objets font-ils référence à la même zone mémoire ?
    • Non, on voit lorsqu'on demande à la console d'afficher ces variables qu'elle ne donne pas visuellement le "contenu" mais juste le type (ici des objets basés sur la classe Personnage) et leurs adresses qui désignent bien deux zones différentes.
    • >>> alice <__main__.Personnage object at 0x7f25459ff9b0> >>> bob <__main__.Personnage object at 0x7f25459ff898>

✎ 02° Rajouter deux attributs à la volée :

  • un attribut profession (contenant la profession du personnage, par exemple "Ninja") et
  • un attribut niveau (contenant par exemple 8).

✎ 03° Donner les instructions (et leurs résultats) de façon à

  1. Afficher le niveau du personnage
  2. Afficher l'adresse du personnage
  3. Incrémenter de 1 le niveau du personnage
  4. Afficher le nouveau niveau du personnage
  5. Afficher l'adresse du personnage

✎ 04° Expliquer à partir des résultats précédents si un objet est muable ou immuable.

05° Dans l'exemple donné ci-dessous,

  • r2d2 est-il un objet, une instance, une classe ou un attribut ?
  • profession est-il un objet, une instance, une classe ou un attribut ?
  • niveau est-il un objet, une instance, une classe ou un attribut ?
>>> r2d2 = Personnage() >>> r2d2.profession = "robot" >>> r2d2.niveau = 5 >>> r2d2.couleur = "blanc"

...CORRECTION...

  • perso est un objet et une instance de Personnage.
  • profession est un attribut.
  • niveau est un attribut.

Bonne pratique : création des attributs via __init__()

06° Mettre la classe ci-dessous en mémoire puis utiliser les instructions dans la console pour visualiser cette initialisation.

1 2 3 4 5 6 7 8
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self): self.nom = 'aucun' self.prenom = 'aucun' self.niveau = 0 self.profession = 'aucune'
>>> bob = Personnage() >>> bob.nom 'aucun' >>> bob.prenom 'aucun' >>> bob.niveau 0 >>> bob.profession 'aucune' >>> bob.prenom = 'bob' >>> bob.prenom 'bob'

Question et réponse : en regardant les attributs déjà créés, serait-il intelligent de vouloir créer un attribut contenant le niveau de magie en le nommant niveau ? Ben non. Un attribut ayant ce nom existe déjà. Il suffit de lire les lignes 4 à 8 pour d'en rendre compte.

✎ 07° Que va être le nom du personnage zzz suivant si on exécute cette affectation ? Que contient la réponse du Constructeur Personnage() ?

>>> zzz = Personnage() >>> zzz.nom

08° QCM : que contient le paramètre self de la méthode spéciale __init__() ? Utiliser la nouvelle classe ci-dessous pour le découvrir ou avoir confirmation de votre avis.

  1. Le nom du personnage
  2. L'adresse du personnage
  3. L'age du capitaine
  4. La date de création de l'objet
1 2 3 4 5 6 7 8 9
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self): self.nom = 'aucun' self.prenom = 'aucun' self.niveau = 0 self.profession = 'aucune' print(f"Depuis la fonction __init__, le paramètre self contient {self}")
>>> a = Personnage() Depuis la fonction __init__, le paramètre self contient
<__main__.Personnage object at 0x7fc9c389aba8>
>>> hex(id(a)) '0x7fc9c389aba8'

Rappel : la fonction native hex() permet d'exprimer un nombre entier en hexadécimal plutôt qu'en décimal.

...CORRECTION...

On remarque que self correspond à l'adresse de l'objet lui-même.

En utilisant self, on a donc accès à l'objet comme si on avait tapé le nom de la variable qui lui fait référence.

✎ 09° Lors de l'appel automatique à __init__() sur la première instruction ci-dessous (L01), que va contenir le paramètre self de la méthode __init__() ?

  1. l'adresse de a
  2. l'adresse de b
  3. le contenu de a
  4. le contenu de b
L1 >>> a = Personnage() L2 >>> b = Personnage()

Même question avec l'instruction en ligne 2.

1 2 3 4 5 6 7 8 9
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self): self.nom = 'aucun' self.prenom = 'aucun' self.niveau = 0 self.profession = 'aucune' print(f"Depuis la fonction __init__, le paramètre self contient {self}")

Les valeurs de base, c'est bien beau mais il faudrait ensuite placer dans les attributs les bonnes valeurs. C'est pénible de faire la création de l'objet en deux fois.

Heureusement, le Constructeur fonctionne comme n'importe quelle autre fonction : il peut lui envoyer des arguments qu'il placera dans ces paramètres. On va donc rajouter de nouveaux paramètres.

10° Utiliser la classe ci-dessous qui permet de créer une liaison entre le Constructeur et la méthode spéciale __init__().

1 2 3 4 5 6 7 8
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self, a, b, c, d): self.nom = a self.prenom = b self.niveau = c self.profession = d
>>> bob = Personnage("Luke", "Skywalker", 5, "Jedi") >>> bob.nom 'Luke'

On voit donc ici que :

  1. Paramètre 1 : self. On ne le fournit pas d'argument. Il sera automatiquement affecté avec l'adresse de l'objet bob.
  2. Paramètre 2 : a. Il reçoit l'argument-string "Luke", qui est normalement le prénom du personnage.
  3. Paramètre 3 : b. Il reçoit l'argument-string "Skywalker", qui est normalement le nom du personnage.
  4. Paramètre 4 : c. Il reçoit l'argument-entier 5, qui est le niveau du personnage.
  5. Paramètre 5 : d. Il reçoit l'argument-string "Jedi", qui est sans doute la profession du personnage.

11° Expliquer le problème visualisé sur le nom du personnage (lorsqu'on tape bob.nom, on obtient Luke et pas Skywalker) en répondant à ceci :

  • Quel est le nom du paramètre qui reçoit l'argument "Luke" (un prénom) ? Est-ce logique lorsqu'on regarde ensuite les lignes 5 et 6 ?
  • Quel est le nom du paramètre qui reçoit l'argument "Skywalker" (un nom) ?
  • Quel est le nom du paramètre qui reçoit l'argument 5 ?
  • Quel est le nom du paramètre qui reçoit l'argument "Jedi" ?
  • Le paramètre self n'a pas été transmis lors de l'utilisation du Constructeur Personnage() dans la console. Est-ce une erreur ?

...CORRECTION...

4
def __init__(self, a, b, c, d):
>>> bob = Personnage("Luke", "Skywalker", 5, "Jedi")
  • Quel est le nom du paramètre qui reçoit l'argument "Luke" ?
  • On le stocke dans a. Pas très logique puisque a sert ensuite à initialiser l'attribut nom...

  • Quel est le nom du paramètre qui reçoit l'argument "Skywalker" ?
  • On le stocke dans b

  • Quel est le nom du paramètre qui reçoit l'argument 5 ?
  • On le stocke dans c

  • Quel est le nom du paramètre qui reçoit l'argument "Jedi" ?
  • On le stocke dans d

  • Le paramètre self n'est pas transmis, est-ce une erreur ?
  • Non : c'est le Constructeur lui-même qui va automatiquement renvoyer l'adresse de l'objet à la méthode __init__().

Alors où est le problème ? Au fait que le paramètre a contient un prénom envoyé par l'utilisateur, or on le stocke ligne 5 dans l'attribut nom !

✎ 12° Corriger le programme de façon à bien mettre le nom dans l'attribut nom et le prénom dans l'attribut prénom.

Dernière chose à propos de la méthode __init__() (et globalement des fonctions en Python) : son alias est comme les autres alias juste un raccourci vers une adresse-mémoire. La différence avec une variable normale ? On y trouve des instructions à exécuter. Mais puisqu'on connaît son adresse, c'est bien qu'on peut la stocker.

>>> bob.__init__ <bound method Personnage.__init__ of <__main__.Personnage object at 0x7f90d24d7cf8>>

Conclusion

On peut donc voir les objets comme des conteneurs : ils permettent de stocker

  • des variables (les fameux attributs)
  • des fonctions (les fameuses méthodes).
objet : conteneur à variables et fonctions
Méthodes spéciales

Les méthodes dont le nom commence et finit par deux tirets sont nommées des méthodes spéciales. Ce sont des méthodes que Python utilise pour jouer un rôle particulier. On peut les redéfinir à la main de façon à modifier le comportement par défaut du système.

Quelques exemples :

  • Méthode permettant permettant au développeur d'initialiser un objet nouvellement créé : __init__()
  • Méthode permettant au développeur d'expliquer comment gérer l'addition de deux objets de même type : __add__()
  • Méthode permettant au développeur d'expliquer comment gérer l'affichage lorsqu'on demande à voir l'objet : __str__()
  • ...

Seule la méthode __init__() est au programme de NSI mais sachez qu'il en existe d'autres si on veut aller plus loin dans la gestion des objets.

6 - Exercices paramètres nommés et par défaut

Première astuce : vous la connaissez. Choisir des noms de variables explicites. Franchement "a, b, c, d, e". Pas très malin comme noms de variables...

Voici un premier exemple qui rendra la classe plus lisible :

1 2 3 4 5 6 7 8
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self, nom, pre, niv, pro): self.nom = nom self.prenom = pre self.niveau = niv self.profession = pro

Dans la première version, j'ai choisi de ne pas mettre exactement le même nom pour le paramètre et pour l'attribut. Mais on a le droit. Voici le même exemple en utilisant exactement le même nom à chaque fois.

1 2 3 4 5 6 7 8
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self, nom, prenom, niveau, profession): self.nom = nom self.prenom = prenom self.niveau = niveau self.profession = profession

✎ 13° Fournir l'instruction à écrire pour créer un personnage nommé Loulou Skydroper, un Technicien niveau 12. Attention, Loulou, c'est le prénom. On considère que la classe Personnage est l'une des deux précédentes.

Comme l'ordre est parfois un peu difficile à imposer, on peut également faire l'appel à une fonction en donnant les noms des paramètres qu'on veut voir associer à un argument. On parle de paramètres nommés.

Ca fonctionne avec toutes les fonctions, pas uniquement avec la fonction Constructeur.

>>> bob = Personnage(prenom="Luke", nom="Skywalker", niveau=5, profession="Jedi") >>> jim = Personnage(profession="Capitaine de l'Enterprise", nom="Kirk", niveau=6, prenom="James")

14° Utiliser la classe Personnage utilisant des paramètres nommés. Utiliser les exemples de création ci-dessus pour vérifier que cela fonctionne bien.

Question : lorsqu'on utilise des paramètres nommés, est-on obligé de respecter l'ordre des paramètres fourni dans le code de la fonction ?

...CORRECTION...

Non. Puisqu'on donne le nom du paramètre à chaque fois, pas la peine de fournir les paramètres dans un ordre précis.

Convention sur les paramètres

Mais... mais... il n'y a pas d'espace autour du = lorsqu'on envoie nos paramètres nommés :

>>> bob = Personnage(prenom="Luke", nom="Skywalker", niveau=5, profession="Jedi")

C'est la convention sur les paramètres contrairement aux déclarations de variables habituelles.

C'est comme ça.

Ici, on met un espace :

>>> bob = Personnage...

Ici, on n'en met pas :

>>> ...(prenom="Luke", ...)...

Dernière chose : on peut imposer des valeurs par défaut à nos fonctions. Il suffit de fournir la valeur par défaut associée au paramètre si on n'en fournit pas lors de l'appel.

✎ 15° Placer cette classe en mémoire puis lancer la création d'un personnage mais sans donner de valeur à l'attribut "classe" de personnage. En regardant le code, deviner la valeur qui va être associée à cet attribut.

1 2 3 4 5 6 7 8
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, profession="Humain de base"): self.nom = nom self.prenom = prenom self.niveau = niveau self.profession = profession
>>> jim = Personnage(prenom="Toto", nom="l'Asticot", niveau=10) >>> jim.profession '???'

7 - Pyxel

Le programme de cette partie n'est pas un exemple des bonnes pratiques lorsqu'on utilise Pyxel avec des objets.

Pourquoi ?

Vous n'avez pas encore vu comment créer des méthodes, donc pour l'instant les objets ne sont finalement que des structures de données :

  • Permettant de stocker des données dans des attributs plutôt que dans des cases.
  • Permettant d'initialiser les valeurs en fonction de valeurs reçues lors de la création.
7.1 Principe d'un programme pyxel

Logo Pyxel
Logo Pyxel

Le principe de fonctionnement d'un programme utilisant le module pyxel est basé sur une séquence qui inclut une boucle infinie.

Déroulé fondamental

On peut résumer le déroulé d'un programme Pyxel de cette façon 

  • Séquence de réglage : init() puis run()
  • 30 fois par seconde : controle() puis vue()
Déroulé plus détaillé

    1 - La fonction init() crée la fenêtre graphique.

    2 - La fonction run() active l'alternance infinie ci dessous :

    3 - TANT QUE le programme est actif :

      On mémorise les actions de l'utilisateur

      SI la dernière activation date de plus de 1/30e de seconde

        On active la fonction controle() qui va

        faire appel à différentes fonctions pour mettre à jour les données

        On active la fonction vue() qui va

        faire appel à cls() pour effacer l'écran

        faire appel à des fonctions afficher_...() pour afficher des formes à l'écran

      Fin du SI

    Fin TANT QUE

Système d'axes

Le point (0,0) est le bord haut gauche.

L'abscisse va bien vers la gauche.

Par contre, l'ordonnée est orientée vers le bas, attention.

Les axes dans Pyxel

16 (pyxel)° Vérifier si le module pyxel est installé :

>>> import pyxel

Si cela ne fonctionne pas, installer la bibliothèque pyxel dans Thonny :

  • Allez dans l'onglet "Outils"
  • Cherchez "Gérer les paquets"
  • Lancez la recherche sur pyxel puis activer l'installation
  • Attendez que l'installation soit totalement effectuée
  • Validez l'installation en utilisant le bouton Fermer

17 (pyxel)° Placer ce prototype de Pong version Pyxel en mémoire. Lancer pour vérifier qu'il fonctionne : touches A et Q pour le joueur de gauche, flèches Haut et Bas pour le joueur de droite. Attention, ce n'est qu'un prototype. Son but est de montrer comment accéder aux informations stockées dans les objets, pas de faire un vrai jeu.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
""" Interface Homme-machine (ihm) basée sur pyxel et le modèle MVC On découpe les fonctions en 3 catégories : - celles du (M)ODELE : la gestion des données pures - celles de la (V)UE : la gestion de l'interaction avec l'utilisateur - celles du (C)ONTROLEUR : la logique du programme, notamment liaison vue-modèle La documentation utilise le mot "Adresse" pour désigner la référence d'un objet. Plus simple et explicite, surtout en CPython. Mais vous pouvez le remplacer par référence si vous voulez être rigoureux. """ # 1 - Importation(s) ========================================================== import pyxel import random # 2 - Constantes ============================================================== LRG = 128 HTR = 128 # 3 - Déclarations des classes ================================================ class Jeu: def __init__(self): self.DIMX = LRG # Largeur constante de l'écran self.DIMY = HTR # Hauteur constante de l'écran self.j1 = Joueur(1, self) # Adresse d'une instance de Joueur self.j2 = Joueur(2, self) # Adresse d'une instance de joueur self.balle = Balle(6, self) # Adresse d'une instance de Balle class Joueur: def __init__(self, n:int, jeu:Jeu): self.n = n # numéro du joueur self.pts = 0 # points du joueur self.jeu = jeu # Adresse de l'instance de Jeu auquel appartient ce joueur self.raq = None # Adresse de l'instance de Raquette de ce joueur # Instanciation de la raquette if n == 1: self.raq = Raquette(10, 40, 2, self) elif n == 2: self.raq = Raquette(110, 40, 3, self) class Raquette: def __init__(self, x:int, y:int, couleur:int, joueur:Joueur): self.j = joueur # Adresse de l'instance de Joueur qui possède cette raquette self.x = x # coordonnée x du coin en haut à gauche self.y = y # coordonnée y du coin en haut à gauche self.couleur = couleur # int, couleur du vaisseau dans Pyxel self.hauteur = self.j.jeu.DIMY // 5 # hauteur en pixel self.epaisseur = self.j.jeu.DIMX // 16 # épaisseur en pixel class Balle: def __init__(self, couleur:int, jeu:Jeu): self.jeu = jeu # Adresse de l'instance de Jeu où apparaît cette balle self.x = jeu.DIMX / 2 # Float, coordonnée horizontale du centre self.y = jeu.DIMY / 2 # Float, coordonnée verticale du centre self.rayon = jeu.DIMX // 50 # Rayon du cercle en pixel self.couleur = couleur # Int, couleur du cercle self.dx = random.randint(-10, 10) / 10 # Float, variation horizontale en px chaque 1/30e s self.dy = random.randint(-5, 5) / 10 # Float, variation verticale en px chaque 1/30e s # 4 - Déclarations des fonctions ============================================== def controle() -> None: # CONTROLEUR """Fonction qui récupère les événements et modifie les données en conséquence""" if pyxel.btn(pyxel.KEY_A): # Si on a appuyé sur A mvt_raq(jeu.j1.raq, 0, -1) # on modifie les données de la raquette 1 if pyxel.btn(pyxel.KEY_Q): # Si on a appuyé sur Q mvt_raq(jeu.j1.raq, 0, 1) # on modifie les données de la raquette 1 if pyxel.btn(pyxel.KEY_UP): # Si on a appuyé sur haut mvt_raq(jeu.j2.raq, 0, -1) # on modifie les données de la raquette 2 if pyxel.btn(pyxel.KEY_DOWN): # Si on a appuyé sur bas mvt_raq(jeu.j2.raq, 0, 1) # on modifie les données de la raquette 2 mvt_balle(jeu.balle) # on modifie les données de la balle def mvt_raq(r:Raquette, dx:int, dy:int) -> None: # MODELE """Modifie les données de l'objet pour intégrer le déplacement dx et dy voulu""" r.x = gerer_blocage(r.x, dx, r.j.jeu.DIMX) r.y = gerer_teleportation(r.y, dy, r.j.jeu.DIMY) def mvt_balle(b:Balle) -> None: # MODELE """Modifie les données de la balle pour intégrer le nouveau déplacement""" # calcul des nouvelles coordonnées b.x = b.x + b.dx b.y = b.y + b.dy # gestion des collisions éventuelles avec les raquettes c = collision_avec(b) if c == 1: r1 = b.jeu.j1.raq b.x = r1.x + r1.epaisseur + b.rayon + 1 b.dx = -b.dx if c == 2: r2 = b.jeu.j2.raq b.x = r2.x - b.rayon - 1 b.dx = -b.dx # gestion des collisions éventuelles avec plafond et sol if b.y > b.jeu.DIMY: b.y = b.jeu.DIMY - 1 b.dy = -b.dy elif b.y < 1: b.y = 1 b.dy = -b.dy def gerer_teleportation(ancienne:int, deplacement:int, maximum:int) -> int: """Calcule la nouvelle coordonnée de l'objet en passant d'un côté à l'autre""" nouvelle = ancienne + deplacement if nouvelle > maximum: nouvelle = 1 elif nouvelle < 1: nouvelle = maximum - 1 return nouvelle def gerer_blocage(ancienne:int, deplacement:int, maximum:int) -> int: """Calcule la nouvelle coordonnée de l'objet en considérant des cloisons limites""" nouvelle = ancienne + deplacement if nouvelle > maximum: nouvelle = maximum - 1 elif nouvelle < 1: nouvelle = 1 return nouvelle def collision_avec(b:Balle) -> int: """Renvoie le numéro de la raquette 1 ou 2 touchant la balle, 0 si aucune""" # Récupération des adresses des deux raquettes r1 = b.jeu.j1.raq r2 = b.jeu.j2.raq # Si la balle possède un x potentiellement au contact de la raquette 1 if (b.x - b.rayon >= r1.x + r1.epaisseur//2) and (b.x - b.rayon <= r1.x + r1.epaisseur): # Si la balle est également verticalement dans la zone de la raquette 1 if (b.y + b.rayon >= r1.y) and (b.y - b.rayon <= r1.y + r1.hauteur): # C'est que la balle est en contact avec la raquette du joueur 1 return 1 # Si la balle possède un x potentiellement au contact de la raquette 2 if (b.x + b.rayon >= r2.x) and (b.x - b.rayon <= r2.x + r2.epaisseur//2): # Si la balle est également verticalement dans la zone de la raquette 2 if (b.y + b.rayon >= r2.y) and (b.y - b.rayon <= r2.y + r2.hauteur): # C'est que la balle est en contact avec la raquette du joueur 2 return 2 # Si on arrive ici, c'est que la balle n'est au contact d'aucune raquette return 0 def actualiser_vue() -> None: # VUE """destruction et création du visuel (30 fois par seconde)""" pyxel.cls(0) # efface la fenetre afficher_raquette(jeu.j1.raq) # raquette 1 afficher_raquette(jeu.j2.raq) # raquette 2 afficher_balle(jeu.balle) def afficher_raquette(r:Raquette) -> None: # VUE """Dessine une raquette""" pyxel.rect(r.x, r.y, r.epaisseur, r.hauteur, r.couleur) # (x,y) est en haut à gauche def afficher_balle(b:Balle) -> None: # VUE """Dessine une balle""" pyxel.circ(int(b.x), int(b.y), b.rayon, b.couleur) # (x,y) est le centre # 5 - PROGRAMME PRINCIPAL========= ================================================ if __name__ == "__main__": jeu = Jeu() pyxel.init(LRG, HTR, title="Mon jeu") # Démarrage de la fenêtre graphique pyxel.run(controle, actualiser_vue) # Alternance controle/vue 30 fois par seconde

18 (pyxel)° Suivre ces explications. Il y aura quelques questions supplémentaires lors des questions 19 et 20.

Etape 1 de l'analyse : lecture de la documentation

1 2 3 4 5 6 7 8 9 10 11 12 13 14
""" Interface Homme-machine (ihm) basée sur pyxel et le modèle MVC On découpe les fonctions en 3 catégories : - celles du (M)ODELE : la gestion des données pures - celles de la (V)UE : la gestion de l'interaction avec l'utilisateur - celles du (C)ONTROLEUR : la logique du programme, notamment liaison vue-modèle La documentation utilise le mot "Adresse" pour désigner la référence d'un objet. Plus simple et explicite, surtout en CPython. Mais vous pouvez le remplacer par référence si vous voulez être rigoureux. """

Cette documentation explique que vous allez utiliser une architecture MVC : on isole trois parties dans le programme :

  • La partie MODELE gère les données mises en mémoire.
  • La partie VUE gère l'interaction avec l'utilisateur (affichage et récupération des infos souris et clavier par exemple)
  • La partie CONTROLEUR gère les événements en permettant de faire la liaision entre les parties MODELE et VUE.

Voici l'image disponible sur Wikipedia permettant de visualiser cette architecture.

Image CC BY-SA 4.0
Image CC BY-SA 4.0 tirée de Wikipedia

Le programme fonctionne donc différemment du projet Rue ou d'Internet, plutôt basé sur une architecture en couches où chaque partie du programme ne peut communiquer qu'avec la couche juste en dessous ou juste au dessus.

Etape 2 de l'analyse : tout sauf les déclarations de classes et de fonctions

Importations, constantes, variables globale jeu lue directement depuis les fonctions et activation de l'Interface aveec init() et run().

15 16 17 18 19 20 21 22 23 24 .. .. 181 182 183 184 185 186
# 1 - Importation(s) ========================================================== import pyxel import random # 2 - Constantes ============================================================== LRG = 128 HTR = 128 # 5 - PROGRAMME PRINCIPAL========= ================================================ if __name__ == "__main__": jeu = Jeu() pyxel.init(LRG, HTR, title="Mon jeu") # Démarrage de la fenêtre graphique pyxel.run(controle, actualiser_vue) # Alternance controle/vue 30 fois par seconde

On y voit qu'on génère notre instance de Jeu (il faudra aller voir ce qu'elle contient plus tard) puis qu'on crée une interface de 128 pixels sur 128 pixels dans laquelle on nomme la fonction CONTROLEUR controle() et la fonction VUE actualiser_vue().

On retrouve bien la structure générale des programmes Pyxels : création avec init() et run() puis appels 30 fois par seconde de la séquence controle() et actualiser_vue.

Etape 3 de l'analyse : suivre les instanciations et les appels de fonctions

  1. Que contient l'objet jeu ? Que contiennent ses attributs ?
  2. 29 30 31 32 33 34 35 36 ... 186 187 ...
    class Jeu: def __init__(self): self.DIMX = LRG # Largeur constante de l'écran self.DIMY = HTR # Hauteur constante de l'écran self.j1 = Joueur(1, self) # Adresse d'une instance de Joueur self.j2 = Joueur(2, self) # Adresse d'une instance de joueur self.balle = Balle(6, self) # Adresse d'une instance de Balle if __name__ == "__main__": jeu = Jeu()
  3. Que contiennent les instances de Joueur et Raquette ?
  4. 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
    class Joueur: def __init__(self, n:int, jeu:Jeu): self.n = n # numéro du joueur self.pts = 0 # points du joueur self.jeu = jeu # Adresse de l'instance de Jeu auquel appartient ce joueur self.raq = None # Adresse de l'instance de Raquette de ce joueur # Instanciation de la raquette if n == 1: self.raq = Raquette(10, 40, 2, self) elif n == 2: self.raq = Raquette(110, 40, 3, self) class Raquette: def __init__(self, x:int, y:int, couleur:int, joueur:Joueur): self.j = joueur # Adresse de l'instance de Joueur qui possède cette raquette self.x = x # coordonnée x du coin en haut à gauche self.y = y # coordonnée y du coin en haut à gauche self.couleur = couleur # int, couleur du vaisseau dans Pyxel self.hauteur = self.j.jeu.DIMY // 5 # hauteur en pixel self.epaisseur = self.j.jeu.DIMX // 16 # épaisseur en pixel class Balle: def __init__(self, couleur:int, jeu:Jeu): self.jeu = jeu # Adresse de l'instance de Jeu où apparaît cette balle self.x = jeu.DIMX / 2 # Float, coordonnée horizontale du centre self.y = jeu.DIMY / 2 # Float, coordonnée verticale du centre self.rayon = jeu.DIMX // 50 # Rayon du cercle en pixel self.couleur = couleur # Int, couleur du cercle self.dx = random.randint(-10, 10) / 10 # Float, variation horizontale en px chaque 1/30e s self.dy = random.randint(-5, 5) / 10 # Float, variation verticale en px chaque 1/30e s
  5. Que veut dire jeu.balle ? A quoi cela mène-t-il ?
  6. Que veut dire jeu.j1.raq ? A quoi cela mène-t-il ?
  7. Même si son appel arrive en dernier tous les trentièmes de seconde, expliquer le fonctionnement de la fonction actualiser_vue().
  8. 165 166 167 168 169 170 171 172 173 174 175 176 177 178
    def actualiser_vue() -> None: # VUE """destruction et création du visuel (30 fois par seconde)""" pyxel.cls(0) # efface la fenetre afficher_raquette(jeu.j1.raq) # raquette 1 afficher_raquette(jeu.j2.raq) # raquette 2 afficher_balle(jeu.balle) def afficher_raquette(r:Raquette) -> None: # VUE """Dessine une raquette""" pyxel.rect(r.x, r.y, r.epaisseur, r.hauteur, r.couleur) # (x,y) est en haut à gauche def afficher_balle(b:Balle) -> None: # VUE """Dessine une balle""" pyxel.circ(int(b.x), int(b.y), b.rayon, b.couleur) # (x,y) est le centre

19 (pyxel)° Dans cette question, nous allons nous focaliser sur controle() et les raquettes.

  1. Expliquer ce que signifie jeu.j1.raq en ligne 79 et suivre le chemin d'objet en objet pour vérifier pourquoi cela fonctionne.
  2. Comprendre ce qui se passe lorsqu'on appuie sur la touche A puis Q. Suivre le déroulé jusqu'à aboutir à des fonctions qui n'appellent pas d'autres fonctions.
  3. 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
    def controle() -> None: # CONTROLEUR """Fonction qui récupère les événements et modifie les données en conséquence""" if pyxel.btn(pyxel.KEY_A): # Si on a appuyé sur A mvt_raq(jeu.j1.raq, 0, -1) # on modifie les données de la raquette 1 if pyxel.btn(pyxel.KEY_Q): # Si on a appuyé sur Q mvt_raq(jeu.j1.raq, 0, 1) # on modifie les données de la raquette 1 if pyxel.btn(pyxel.KEY_UP): # Si on a appuyé sur haut mvt_raq(jeu.j2.raq, 0, -1) # on modifie les données de la raquette 2 if pyxel.btn(pyxel.KEY_DOWN): # Si on a appuyé sur bas mvt_raq(jeu.j2.raq, 0, 1) # on modifie les données de la raquette 2 mvt_balle(jeu.balle) # on modifie les données de la balle def mvt_raq(r:Raquette, dx:int, dy:int) -> None: # MODELE """Modifie les données de l'objet pour intégrer le déplacement dx et dy voulu""" r.x = gerer_blocage(r.x, dx, r.j.jeu.DIMX) r.y = gerer_teleportation(r.y, dy, r.j.jeu.DIMY) def mvt_balle(b:Balle) -> None: # MODELE """Modifie les données de la balle pour intégrer le nouveau déplacement""" # calcul des nouvelles coordonnées b.x = b.x + b.dx b.y = b.y + b.dy # gestion des collisions éventuelles avec les raquettes c = collision_avec(b) if c == 1: r1 = b.jeu.j1.raq b.x = r1.x + r1.epaisseur + b.rayon + 1 b.dx = -b.dx if c == 2: r2 = b.jeu.j2.raq b.x = r2.x - b.rayon - 1 b.dx = -b.dx # gestion des collisions éventuelles avec plafond et sol if b.y > b.jeu.DIMY: b.y = b.jeu.DIMY - 1 b.dy = -b.dy elif b.y < 1: b.y = 1 b.dy = -b.dy def gerer_teleportation(ancienne:int, deplacement:int, maximum:int) -> int: """Calcule la nouvelle coordonnée de l'objet en passant d'un côté à l'autre""" nouvelle = ancienne + deplacement if nouvelle > maximum: nouvelle = 1 elif nouvelle < 1: nouvelle = maximum - 1 return nouvelle def gerer_blocage(ancienne:int, deplacement:int, maximum:int) -> int: """Calcule la nouvelle coordonnée de l'objet en considérant des cloisons limites""" nouvelle = ancienne + deplacement if nouvelle > maximum: nouvelle = maximum - 1 elif nouvelle < 1: nouvelle = 1 return nouvelle

20 (pyxel)° Dans cette question, nous allons nous focaliser sur controle() et la balle.

  1. Expliquer ce que signifie jeu.balle en ligne 88 et suivre le chemin d'objet en objet pour vérifier pourquoi cela fonctionne.
  2. Comprendre alors ce qui se passe lorsqu'on lance l'appel mvt_balle(jeu.balle). Une fois arrivé dans la fonction collision_avec(), il faudra faire un schéma pour comprendre la situation.
  3. 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
    def controle() -> None: # CONTROLEUR """Fonction qui récupère les événements et modifie les données en conséquence""" if pyxel.btn(pyxel.KEY_A): # Si on a appuyé sur A mvt_raq(jeu.j1.raq, 0, -1) # on modifie les données de la raquette 1 if pyxel.btn(pyxel.KEY_Q): # Si on a appuyé sur Q mvt_raq(jeu.j1.raq, 0, 1) # on modifie les données de la raquette 1 if pyxel.btn(pyxel.KEY_UP): # Si on a appuyé sur haut mvt_raq(jeu.j2.raq, 0, -1) # on modifie les données de la raquette 2 if pyxel.btn(pyxel.KEY_DOWN): # Si on a appuyé sur bas mvt_raq(jeu.j2.raq, 0, 1) # on modifie les données de la raquette 2 mvt_balle(jeu.balle) # on modifie les données de la balle def mvt_raq(r:Raquette, dx:int, dy:int) -> None: # MODELE """Modifie les données de l'objet pour intégrer le déplacement dx et dy voulu""" r.x = gerer_blocage(r.x, dx, r.j.jeu.DIMX) r.y = gerer_teleportation(r.y, dy, r.j.jeu.DIMY) def mvt_balle(b:Balle) -> None: # MODELE """Modifie les données de la balle pour intégrer le nouveau déplacement""" # calcul des nouvelles coordonnées b.x = b.x + b.dx b.y = b.y + b.dy # gestion des collisions éventuelles avec les raquettes c = collision_avec(b) if c == 1: r1 = b.jeu.j1.raq b.x = r1.x + r1.epaisseur + b.rayon + 1 b.dx = -b.dx if c == 2: r2 = b.jeu.j2.raq b.x = r2.x - b.rayon - 1 b.dx = -b.dx # gestion des collisions éventuelles avec plafond et sol if b.y > b.jeu.DIMY: b.y = b.jeu.DIMY - 1 b.dy = -b.dy elif b.y < 1: b.y = 1 b.dy = -b.dy def gerer_teleportation(ancienne:int, deplacement:int, maximum:int) -> int: """Calcule la nouvelle coordonnée de l'objet en passant d'un côté à l'autre""" nouvelle = ancienne + deplacement if nouvelle > maximum: nouvelle = 1 elif nouvelle < 1: nouvelle = maximum - 1 return nouvelle def gerer_blocage(ancienne:int, deplacement:int, maximum:int) -> int: """Calcule la nouvelle coordonnée de l'objet en considérant des cloisons limites""" nouvelle = ancienne + deplacement if nouvelle > maximum: nouvelle = maximum - 1 elif nouvelle < 1: nouvelle = 1 return nouvelle

🏠 Exercices supplémentaires° Faire ces exercices en rédigeant correctement vos réponses.

Exos à réaliser (20 questions)

Pour l'instant, cela semble un peu compliqué mais c'est surtout dû au fait que nous n'avons pas profité d'un grand avantage des objets : le fait qu'on puisse y stocker directement les fonctions permettant de gérer l'objet. Les fameuses méthodes que nous allons voir dans l'activité suivante.

8 - FAQ

J'ai vu sur le Web qu'il existe une méthode __new__() qui intervient avant __init__(). C'est quoi ?

La méthode __new__() est une méthode permettant de modifier le comportement de la création initiale, celle qui attribue l'adresse de l'objet.

Elle intervient donc avant la méthode __init__().

Voici une classe qui permet de demander d'afficher un message lors de l'appel de la méthode __new__() et de la méthode __init__(). Il vous permettra de voir que la première méthode est bien activée avant la deuxième.

1 2 3 4 5 6 7 8 9
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __new__(self): print("On est dans __new__ : on va créer l'objet") return super(Personnage, self).__new__(self) def __init__(self): print("On est donc __init__' : on va initialiser les attributs de l'objet")

Voici le résultat d'une des utilisations :

>>> a = Personnage() On est dans __new__ : on va créer l'objet On est donc __init__' : on va initialiser les attributs de l'objet

Voilà, on voit bien que la méthode __new__() est activée d'abord lorsqu'on crée un objet.

Si la ligne 6 ne vous a pas calmé, vous pouvez me poser des questions pour savoir ce qu'elle veut dire :o)

Sinon, sachez simplement que l'explication sur cette méthode n'est vraiment pas au programme. Pour comprendre son intéret, vous auriez besion de connaissances sur les objets qui sont hors programme en NSI. Vous verrez cela l'année prochaine dans le supérieur. Pour l'instant, vous pouvez donc oublier __new__(). La méthode de création par défaut suffira bien pour nos projets.

On parle aussi de variables d'instance ou de variables de classe. Quelle est la différence avec les attributs ?

Cette notion n'est pas au programme du BAC. A ne lire que si vous voulez en savoir plus.

1 2 3 4 5 6 7 8 9 10 11
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" pv_max = 100 # pv_max est une variable de classe Personnage def __init__(self, nom, pv): self.nom = nom # self.nom est une variable d'instance ou attribut if pv > Personnage.pv_max: self.pv = Personnage.pv_max else: self.pv = pv

Comme vous pouvez le voir, on peut aussi créer des variables qui sont stockées directement dans les informations de la Classe et pas dans chacune des instantes créées à partir de cette classe. Utiliser cette variable de classe permet d'uniformiser certaines valeurs : vous êtes ainsi certain que toutes les classes utiliseront les mêmes valeurs pour certains calculs.

Nous avons fait la première partie de cette découverte de la programmation orientée objet.

La Classe est donc le moule permettant de créer des structures de stockage contenant des données (des variables) et des fonctions.

Lorsqu'on utilise le Constructeur d'une classe, il renvoie l'adresse de l'objet qu'il vient de créer. On dit que cet objet est une instance de la Classe.

La structure de cet objet a ceci de particulier par rapport à ce qu'on a vu précédemment qu'elle permet de stocker :

  • Des variables d'instance, qu'on nomme également des attributs.
  • Des fonctions, qu'on nomme également des méthodes.

L'activité suivante va maintenant décrire précisement la création des méthodes et leurs utilisations.

Activité publiée le 01 09 2020
Dernière modification : 07 09 2022
Auteur : ows. h.