python objets

Identification

Infoforall

36 - 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 - Rappels 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é.

(Rappel) 1.1 Type simple : principe
flowchart LR D([Variable]) --> A([Contenu])

On a une liaison entre un NOM de variable et un CONTENU en mémoire :

une sorte de boite une sorte de liaison
(Rappel) 1.2 Type construit : principe
flowchart LR D([Variable]) --> M[Conteneur] M -- Emplacement A --> A([Contenu A]) M -- Emplacement B --> B([Contenu B]) M -- Emplacement C --> C([Contenu C])

On a une liaison entre un NOM de variable et un CONTENEUR en mémoire.

A partir de ce CONTENEUR, on peut ensuite localiser différents contenus.

Version naîve d'un tableau t1 qui "contient" 10, 100 et 1000 :

une sorte d'armoire

Important : en Python, la variable t1 désigne donc l'adresse du conteneur-tableau-armoire et pas le contenu du tableau.

une sorte de liaison plus complexe
(Rappel) 1.3 Variables en Python : l'espace des noms

Lors d'une affectation, Python crée une association entre le nom de la variable et un contenu en mémoire.

une sorte de liaison

L'espace des noms correspond au mécanisme qui lie le nom de la variable à un contenu mémoire, représenté ici par le simple trait entre les deux.

(Rappel) 1.3.1 L'espace des noms de Python

Les associations entre nom et contenu sont mémorisées dans une table qu'on nomme l'espace des noms.

Imaginons qu'on dispose de ce programme :

1 2 3
a = 10 b = 20 c = b - a

Voici l'image simpliste qu'on peut se faire de la liaison entre l'espace des noms et l'espace mémoire :

liaison magique espace des noms et mémoire

Cette liaison est réalisée en associant dans une table chaque nom de variable à une zone mémoire.

espace des noms type simple

En Python, on peut récupérer l'adresse / référence / identifiant à l'aide de la fonction native id().

>>> a = 10 >>> b = 20 >>> id(a) 108 >>> id(b) 524
(Rappel) 1.3.2 LES espaceS des noms de Python

Il existe en réalité plusieurs espaces des noms pouvant référencer l'espace mémoire :

  • L'espace des noms global et permanent du programme : il existe pendant tout le déroulement du programme.
  • Chaque appel de fonction crée un espace des noms local temporaire qui est détruit après l'appel.

On peut voir visuellement ces zones dans Python Tutor. La zone grise est l'espace des noms actuellement utilisé par Python.

Exemple avec ce court programme :

1 2 3 4 5
def f(x): return x * 2 a = 10 b = f(a)
espaces des noms
(Rappel) 1.3.3 Utilisation par Python

Lorsque Python doit évaluer une variable dans une fonction :

  1. Il commence par chercher dans l'espace local.
  2. Si il ne trouve pas, il cherche ensuite dans l'espace global.
  3. S'il ne trouve toujours pas de variable portant ce nom, il déclenche une exception NameError.

Traduit en Python, cela donnerait quelque chose comme ceci :

1 2 3 4 5 6 7 8 9 10 11 12 13
def evaluer_variable(nom): espace_local = locals() # espace local actuel sous forme d'un dico espace_global = globals() # idem mais pour l'espace global if nom in espace_local: # si le nom est bien une clé de l'espace local return espace_local[nom] # on renvoie son contenu mémoire elif nom in espace_global: # sinon si le nom est une clé de l'espace global return espace_global[nom] # on renvoie son contenu mémoire else: # sinon, raise NameError # on lève l'exception NameError
ATTENTION

Dans d'autres langages (comme le C par exemple), l'adresse d'une variable ne change pas après déclaration. C'est bien le contenu de la case mémoire qui change.

1.4 Objet : une notion déjà rencontrée !
La list de Python est un objet

Le type list ou le type dict de Python sont des types construits, ok.

Mais, les lists Python sont plus que cela car elles contiennent :

  • des données mais également
  • des fonctions : append(), pop(), keys() ...

On accède aux données de la liste en utilisant l'opérateur "CROCHETS" : t[i].

On accède aux fonctions internes (qu'on nomme méthodes) en utilisant l'opérateur "POINT" : t.nom().

La réprésentation symbolique de list ressemble donc à cela :

flowchart LR D([Variable]) --> L([Conteneur
objet de type list]) L --> MA([append]) L --> MP([pop]) L -- "opérateur crochets" --> M[Données] M -- [0] --> A([Contenu A]) M -- [1] --> B([Contenu B]) M -- [2] --> C([Contenu C])
La tortue du module turtle est un objet

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().

L'objet-fichier créé avec open() est... un objet

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().

1.5 Type list : plus compliqué qu'un simple tableau, même dynamique

A - Rappel : Tableau et espace des noms
  • La variable associée à un tableau contient en réalité l'adresse de la structure et pas directement son contenu
  • 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é.

Exemple

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

C'est 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.

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 - Le type list n'est pas juste un tableau

Ce que nous avons vu plus haut correspond aux vrais tableaux (comme en C par exemple). Or, le type list est en réalité un objet : c'est un conteneur à données mais également à méthodes.

L'adresse de la structure n'est donc pas nécessairement pas identique à l'adresse de sa première case.

En réalité, sa structure mène tout simplement à une zone mémoire qui contient une table d'espace des noms, l'espace des noms de ce tableau précis !

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 créer nos propres objets.

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 qu'on nommera également instances de cette classe : ils auront au départ des attributs (des variables) dont les contenus sont identiques mais pourront être personnalisés.

Résumé de l'analogie :

  • Recette ---- Classe
  • Gâteau ---- Objet ou Instance de la classe
  • Caractéristiques d'un Gâteau ---- Attributs d'un Objet
2.2 Création d'une Classe puis création d'instance de cette classe

A - Déclaration d'une Classe ("moule" ou "recette")

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. Comme toute déclaration, on finit par le caractère ':'.

1 2 3 4 5 6 7
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" pass class Monstre: """Ceci est une classe permettant de créer un monstre dans mon super RPG""" pass

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

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

B - Instanciation : création d'un objet

Une fois la classe déclarée, on peut l'utiliser pour créer de vrais objets basés sur elle. La création d'une instance se nomme une instanciation.

On peut voir 4 instanciations ci-dessous, sur les lignes 10-11-12-13.

Pour cela, on utiliser la fonction Constructeur : une fonction qui porte le même nom que la Classe. Comme c'est une fonction, il y a des parenthèses.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" pass class Monstre: """Ceci est une classe permettant de créer un monstre dans mon super RPG""" pass alice = Personnage() bob = Personnage() squelette_12 = Monstre() vlad = Monstre() print("INFOS sur la variable alice") print(alice) print("\nINFOS sur la variable vlad") print(vlad)

Ce programme affiche les messages suivants à cause des différents print() :

INFOS sur la variable alice <__main__.Personnage object at 0x79dd7bc0f670> <-- résultat de L16 : print(alice) INFOS sur la variable vlad <__main__.Monstre object at 0x79dd7bc0e5c0> <-- résultat de L19 : print(vlad)

Par défaut, l'affichage d'un objet comporte :

  • Sa Classe et la localisation de cette classe. Ici, il s'agit de la Classe Personnage qui est définie dans le programme principal (__main__);
  • Son adresse-mémoire (ici 0x79dd7bc0e5c0).
C - Type des instances de classe

On peut récupérer le type d'un objet en utilisant simplement la fonction native type() :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" pass class Monstre: """Ceci est une classe permettant de créer un monstre dans mon super RPG""" pass alice = Personnage() bob = Personnage() squelette_12 = Monstre() vlad = Monstre() print("INFOS sur la variable alice") print(type(alice)) print("\nINFOS sur la variable vlad") print(type(vlad)) print("\nCombat ou pas combat ?") if type(alice) == Personnage and type(vlad) == Monstre: print("Fight !")

Voici l'affichage obtenu :

INFOS sur la variable alice <class '__main__.Personnage'> <-- résultat de print(type(alice)) <-- résultat de L16 : print(type(alice)) INFOS sur la variable vlad <class '__main__.Monstre'> <-- résultat de L19 : print(type(vlad)) Combat ou pas combat ? Fight ! <-- résultat de L23

En lignes 22-23, on demande d'afficher "Fight !" uniquement si alice est bien une instance de Personnage et vlad une instance de Monstre.

2.3 Un objet vide pas si vide que cela

Un peu de vocabulaire

Nous venons de voir qu'on crée un objet "vide".

En réalité, cet objet n'est pas si vide que cela. C'est un conteneur qui contient déjà quelques variables et quelques fonctions qui lui sont automatiquement attribués par Python.

Les variables contenues dans un objet seront nommés ses attributs. La syntaxe pointée permet d'accéder à un attribut : objet.attribut

Les fonctions contenues dans un objet seront nommées ses méthodes. La syntaxe pointée permet de lancer l'appel à une méthode : objet.methode()

Voir le contenu avec dir()

Pour observer le contenu d'un objet (ses attributs et se méthodes), on peut utiliser la fonction native de Python nommée dir() qui renvoie un tableau de strings qui contient les noms des attributs et des méthodes.

>>> dir(alice) ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__'] >>> for v in dir(alice): print(v) __class__ __delattr__ __dict__ __dir__ __doc__ __eq__ __format__ __ge__ __getattribute__ __gt__ __hash__ __init__ __init_subclass__ __le__ __lt__ __module__ __ne__ __new__ __reduce__ __reduce_ex__ __repr__ __setattr__ __sizeof__ __str__ __subclasshook__ __weakref__

On voit que notre objet alice contient déjà de nombreuses choses. D'ailleurs, tous les noms commencent par deux underscores et finissent par deux underscopres pour signaler que ce sont des méthodes spéciales : l'utilisateur n'est pas censé faire appel lui-même à ces méthodes, c'est Python qui va leur appeler lorsqu'il en a besoin.

Quelques exemples

Lorsque vous avez fait appel à la fonction native type() celle-ci va juste lire le nom de la classe dans la propriété spéciale alice.__class__

>>> type(alice) <class '__main__.Personnage'> >>> alice.__class__ <class '__main__.Personnage'>

Utilisateur  ⇒  type()  ⇒  __class__

Lorsque vous avez fait appel à la fonction native dir() celle-ci va juste faire appel à la méthode spéciale alice.__dir__()

>>> dir(alice) ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__'] >>> alice.__dir__() ['__module__', '__doc__', '__dict__', '__weakref__', '__new__', '__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__init__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']

Utilisateur  ⇒  dir()  ⇒  __dir__()

On peut également aller récupérer la documentation d'un objet ou sa taille en octets (bytes en anglais) en utilisant la propriété spéciale __doc__ et la méthode spéciale __sizeof__() :

>>> alice.__doc__ 'Ceci est une classe permettant de créer un personnage dans mon super RPG' >>> alice.__sizeof__() 32

Résumé des notions découvertes pour le moment

Un objet est un conteneur qui

  • est construit à l'aide du Constructeur de sa Classe
  • dispose ensuite d'une adresse qui lui appartient personnellement
  • contient des attributs et des méthodes

Reste à voir comment le remplir avec ce qu'on désire et pas simplement utiliser les données que Python y place au départ.

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

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

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

3.1 Gestion des attributs

Pour l'instant, toutes les instances de la classe Personne sont strictement identiques si ce n'est qu'elles ne sont pas stockées au même endroit.

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 pointée : objet.attribut = valeur.
On notera que l'affectation se fait sur l'attribut, pas sur l'objet lui-même.

>>> alice.agilite = 18

Traduction en français :

On agit sur alice en lui rajoutant un nouvel attribut agilite référençant 18.

Effet réel :

Va à l'adresse d'alice puis crée et stocke sur place un attribut agilite référençant 18.

C - Exemple

Si on veut créer un cousin éloigné de Luke Skywalker dans Star Wars, on pourrait 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""" pass bob = Personnage() bob.nom = "Skywalker" bob.prenom = "Bob" bob.age = 25

Que fait ce programme ?

Ligne 7 : il crée un objet bob, une instance de la Classe Personnage.

Lignes 8-9-10 : on ajoute des attributs en utilisant la syntaxe pointée.

D - Accéder aux valeurs des attributs

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

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

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

On utilise la syntaxe suivante : objet.attribut = nouvelle_valeur

>>> bob.nom 'Skywalker' >>> bob.nom = "Bond" >>> bob.nom 'Bond'

Exercices sur la création d'attributs à la volée (rappel : c'est une mauvaise pratique !)

01° Mettre le programme suivant en mémoire : il déclare deux classes Personnage et Monstre puis crée deux instances de Personnage nommées alice et bob.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" pass class Monstre: """Ceci est une classe permettant de créer un monstre dans mon super RPG""" pass alice = Personnage() bob = Personnage() print("INFOS sur la variable alice") print(alice) print("\nINFOS sur la variable bob") print(bob)

Questions

  1. Quelle est la particularité du nom de la classe au niveau majuscule/minuscule ?
  2. Comment se nomment les deux variables qui contiennent des objets ?
  3. Sur quelles lignes a-t-on créé ces objets ?
  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 ?
    • On crée alice et bob
  • Sur quelles lignes a-t-on créé ces objets ?
    • Lignes 10 et 11.
  • Les deux objets font-ils référence à la même zone mémoire ?
    • Non, vous devriez constater via l'affichage obtenu que les deux objets ont bien des adresses différentes en mémoire. Il s'agit bien deu deux choses distinctes mais basées sur le même "moule".

02° A la suite du programme, rajouter deux attributs à la volée à chacune des instances :

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

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" pass class Monstre: """Ceci est une classe permettant de créer un monstre dans mon super RPG""" pass alice = Personnage() bob = Personnage() print("INFOS sur la variable alice") print(alice) print("\nINFOS sur la variable bob") print(bob) alice.profession = "Jedi" alice.niveau = 8 bob.profession = "Contrebandier" alice.niveau = 3

03° Rajouter les instructions de façon à

  1. Afficher le niveau du personnage alice
  2. Afficher l'adresse-mémoire d'alice (avec la fonction native id())
  3. Incrémenter de 1 son niveau
  4. Afficher le nouveau niveau
  5. Afficher à nouveau l'adresse-mémoire d'alice (avec la fonction native id())

...CORRECTION...

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
class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" pass class Monstre: """Ceci est une classe permettant de créer un monstre dans mon super RPG""" pass alice = Personnage() bob = Personnage() print("INFOS sur la variable alice") print(alice) print("\nINFOS sur la variable bob") print(bob) alice.profession = "Jedi" alice.niveau = 8 bob.profession = "Contrebandier" alice.niveau = 3 print(alice.niveau) print(id(alice)) alice.niveau = alice.niveau + 1 print(alice.niveau) print(id(alice))

Exemple d'affichage obtenu

INFOS sur la variable alice <__main__.Personnage object at 0x74c934eeb670> INFOS sur la variable bob <__main__.Personnage object at 0x74c934eea7a0> 3 128407525308016 4 128407525308016

04° Expliquer à partir des résultats précédents si un objet est une donnée muable/mutable ou immuable.

...CORRECTION...

INFOS sur la variable alice <__main__.Personnage object at 0x74c934eeb670> INFOS sur la variable bob <__main__.Personnage object at 0x74c934eea7a0> 3 128407525308016 <--- adresse d'alice avant la modification 4 128407525308016 <--- adresse d'alice après la modification

On voit donc clairement qu'avoir modifié le contenu du conteneur alice n'a pas modifié l'adresse du conteneur lui-même.

C'est la définition même de mutable ou muable.

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

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

...CORRECTION...

  1. r2d2 est-il un objet, une instance, une classe ou un attribut ?
  2. r2d2 est un objet et une instance de la classe Personnage.

  3. profession est-il un objet, une instance, une classe ou un attribut ?
  4. profession est un attribut.

  5. niveau est-il un objet, une instance, une classe ou un attribut ?
  6. niveau est un attribut.

3.2 Création d'attributs à 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.

3.3 Retrouver la liste des attributs

On peut obtenir la liste des attributs disponibles dans un objet en utilisant la fonction native vars() qui vous renvoie un dictionnaire dont les clés sont les noms des attributs et les valeurs sont les valeurs des attributs.

>>> bob = Personnage() >>> bob.nom = "Skywalker" >>> bob.prenom = "Bob" >>> bob.age = 25 >>> vars(bob) {'nom': 'Skywalker', 'prenom': 'Bob', 'age': 25}

Une bonne pratique est donc de ne pas créer d'attributs à la volée. Comment fait-on alors ? 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__()

4.1 La méthode spéciale __init__()

A - La méthode spéciale __init__()

Les méthodes spéciales sont des méthodes qui sont appelées directement par Python.

C'est le cas de la méthode __init__() :

  • L'utilisateur fait appel au Constructeur de la Classe.
  • Le Constructeur lance alors automatiquement un appel à la méthode spéciale __init__() (c'est pour cela qu'elle est spéciale).

Utilisateur  ⇒  Personnage()  ⇒  __init__()

La méthode spéciale __init__() va alors générer certains attributs voulus par la conceptrice de la Classe. Cette méthode a pour but de centraliser la création et l'initialisation des attributs.

En Python, les méthodes spéciales sont faciles à repérer : leurs noms commencent et finissent par deux underscores.

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

Il existe deux différences entre une fonction classique et une méthode :

  1. Sa définition commençant par def doit être tabulée par rapport à la Classe à laquelle elle appartient.
  2. Elle possède TOUJOURS un premier paramètre. Par convention, on le nomme souvent self. Nous allons en parler.
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 après avoir utilisé le constructeur Personnage().
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 et initialiser les attributs disponibles. Si vous désirez créer un nouvel attribut, il vous suffit d'aller voir dans cette méthode pour avoir si vous pouvez utiliser tel ou tel nom ou s'il est déjà pris.

La méthode spéciale __init__() est donc nommée la méthode-initialisateur.

4.2 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() # Réservation (ETAPE 1) Personnage.__init__(adresse) # Initialisation (ETAPE 2) return adresse # Renvoi (ETAPE 3)

A l'étape 2, Personnage() lance automatiquement un appel à __init__() qui se trouve dans la classe Personnage 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.

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 ? Réponse, non : ce nom est déjà porté par un attribut. 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 instanciation ? Que contient la réponse du Constructeur Personnage() qu'on affecte à la variable zzz ?

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

08° QCM : que contient le paramètre self de la méthode spéciale __init__() ?

  1. Le nom du personnage
  2. L'adresse du personnage
  3. L'age du capitaine
  4. La date de création de l'objet

Utiliser la nouvelle classe ci-dessous pour le découvrir ou avoir confirmation de votre avis.

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.

✎ 09° Lors de l'appel automatique à __init__() sur la première instruction ci-dessous (LA), 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
LA >>> a = Personnage() LB >>> b = Personnage()

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

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.

4.3 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")

Regardons ce que contient notre objet :

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

Pour comprendre la transmission d'informations, il faut comparer appel au contructeur et déclaration de la méthode __init__() :

04 Décla 10 Appel
def __init__(self, nom, pre, niv, pro): | | | | | | | | | | | | p = Personnage("InBorderLand", "Alice", 20, "Héroïne")

On voit alors visuellement quels sont les transferts effectués.

  1. self : on ne l'a pas envoyé, ce paramètre-adresse est géré automatiquement.
  2. "InBorderLand" vers nom.
  3. ...

Que fait-on ensuite des paramètres ?

On voit ligne 6 que le paramètre 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 sans valeur par défaut puis
  • mettre en fin de prototype ceux avec une valeur par défaut.
  • 4
    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")

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

Donner le contenu des différents attributs en faisant la comparaison entre appel et déclaration.

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'

...CORRECTION...

def __init__(self, a b c d ) | | | | | | | | | | | | >>> bob = Personnage("Luke", "Skywalker", 5, "Jedi") >>> bob.nom 'Luke'

On voit 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 pourquoi lorsqu'on tape bob.nom, on obtient Luke (son prénom) et pas Skywalker (qui est normalement son nom).

...CORRECTION...

Au vu de l'appel, on stocke "Luke" dans le paramètre a. Or a sert ensuite à initialiser l'attribut nom...

Le problème vient donc paramètre a contient un prénom envoyé par l'utilisateur, et pas le nom.

12° Corriger l'appel puis le programme en donnant aux paramètres des noms EXPLICITES qui devraient nous simplifier la vie.

...CORRECTION...

Pour l'appel, il suffit d'inverser le nom et le prénom :

>>> bob = Personnage("Skywalker", "Luke", 5, "Jedi") >>> bob.nom 'Luke'

Dans le programme, pour éviter les confusions, le plus simple est de donner des noms explicites aux paramètres de réception :

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.

On considère qu'on dispose de cette version de la classe :

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

14° En utilisant les paramètres nommés, réaliser un appel où on fournit d'abord le niveau, la profession, le nom puis le prénom.

...CORRECTION...

15° Cette dernière version utilise la notion de paramètres par défaut.

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

Donner le contenu des attributs des objets alice puis bob créés avec ces deux lignes de code :

>>> alice = Personnage(prenom="Alina", nom="Cloud", profession="Jedi") >>> bob = Personnage(prenom="Bob", nom="Sky", niveau=5)
Convention sur les paramètres

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

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

Ici, on met un espace autour du = qui déclare la variable bob mais pas autour des = qui donnent les valeurs par défauts des paramètres :

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

5 - Compléments sur le fonctionnement et la structure des objets

5.1 L'objet comme un conteneur ayant son propre espace des noms

A - Représentation mentale d'une recherche d'attributs

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.

Bilan

Les objets sont donc des conteneurs qui référencent un ESPACE DES NOMS INTERNES. Sur place, on y trouve :

  • des variables (les attributs)
  • des fonctions (les méthodes).
objet : conteneur à variables et fonctions
5.2 Vocabulaire autour de __init__

A - Instanciation
  • Instanciation : création d'un nouvel objet à partir du Constructeur d'une Classe.
  • Instance : synonyme d'objet sauf qu'on indique la classe lorsqu'on parle d'instance. On dira instance de Personnage alors qu'on parlera juste d'objet.
  • 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
  • Méthode-initialisateur : le Constructeur lance lui-même l'appel à cette méthode spéciale nommée __init__ lorsqu'on lui demande de produire une nouvelle instance.
Les autres noms de __init__

Plutôt que méthode-initialisateur ou méthode-initialisateur, on utilise parfois également :

  • juste initialisateur ou initialisatrice;
  • méthode constructeur puisque c'est une méthode qui est appelée uniquement par le Constructeur;
  • et même parfois juste constructeur.
5.3 Fonction native isinstance

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() qui est un prédicat 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. Utilisez juste type() cette année, mais si vous voyez isinstance() dans un sujet du BAC vous savez maintenant que ce prédicat teste la classe d'un objet.

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 - 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 conteneurs de données.

6.1 Principe d'un programme pyxel

Logo Pyxel
Logo Pyxel

Un programme utilisant le module pyxel est basé sur une boucle infinie décrite ci-dessous.

Principe de fonctionnement

    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 mise à jour date de 1/30e de seconde ou plus

        On active la fonction controler() qui mettra les données à jour en fonction des événements détéctés.

        On active la fonction afficher() qui effacera l'écran avec cls() puis générera le nouvel affichage.

      Fin du SI

    Fin TANT QUE

Au vu de la condition sur l'activation des deux fonctions principales (nommées controler et afficher ici), on voit qu'il va y avoir 30 images par seconde.

La structure usuelle du code Python pour ce module
1 2 3 4 5 6 7 8 9 10 11 12 13
import pyxel def controler(): """Récupère l'événement et le gère les données (30 fois par seconde)""" ... # A modifier def afficher(): """création des objets (30 fois par seconde)""" pyxel.cls(0) # vide la fenetre ... # A modifier pyxel.init(128, 128, title="Mon premier jeu") pyxel.run(controler, afficher)
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)° Enregistrer dans le répertoire Pong ce programme Python en le nommant modele.py :

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
# 1 - Importation(s) ========================================================== import random # 2 - Constantes et variables globales ======================================== 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 mvt_raq(r:Raquette, dx:int, dy:int) -> None: """Modifie les données de l'objet pour intégrer le déplacement dx et dy voulu""" r.x = gerer_x_raquette(r.x, dx, r.j.jeu.DIMX) # impossible d'aller plus loin que la limite r.y = gerer_y_raquette(r.y, dy, r.j.jeu.DIMY) # pour apparaître en bas si on sort en haut et inversement def mvt_balle(b:Balle) -> None: """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) # c vaut 1 ou 2 si collision avec la raquette 1 ou 2 if c == 1: r1 = b.jeu.j1.raq # on récupère l'adresse de l'objet-raquette 1 b.x = r1.x + r1.epaisseur + b.rayon + 1 # on place la balle juste à droite de la raquette b.dx = -b.dx # on inverse la vitesse en x de la balle pour qu'elle part dans l'autre sens if c == 2: r2 = b.jeu.j2.raq # on récupère l'adresse de l'objet-raquette 2 b.x = r2.x - b.rayon - 1 # on place la balle juste à gache de la raquette b.dx = -b.dx # on inverse la vitesse en x de la balle pour qu'elle part dans l'autre sens # gestion des collisions éventuelles avec plafond et sol if b.y > b.jeu.DIMY: # si la balle atteint le plafond b.y = b.jeu.DIMY - 1 # on modifie sa position pour bien la replacer b.dy = -b.dy # on inverse la vitesse en y de la balle pour qu'elle part dans l'autre sens elif b.y < 1: # si la balle atteint le sol b.y = 1 # on modifie sa position pour bien la replacer b.dy = -b.dy # on inverse la vitesse en y de la balle pour qu'elle part dans l'autre sens def gerer_y_raquette(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_x_raquette(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 # Tests de contact avec la raquette 1 xd_1 = r1.x + r1.epaisseur # x droite de la raquette 1 xm_1 = r1.x + r1.epaisseur // 2 # x du milieu de la raquette 1 xg_b = b.x - b.rayon # x gauche de la balle # Si la balle possède un x au contact de la raquette 1 if xg_b <= xd_1 and xg_b > xm_1: yh_1 = r1.y # y haut de la raquette 1 yb_1 = r1.y + r1.hauteur # y bas de la raquette 1 yh_b = b.y - b.rayon # y haut de la balle yb_b = b.y + b.rayon # y bas de la balle # Si la balle est également verticalement dans la zone de la raquette 1 if (yb_b >= yh_1) and (yh_b <= yb_1): return 1 # Tests de contact avec la raquette 2 xg_2 = r2.x # x gauche de la raquette 2 xm_2 = r2.x + r2.epaisseur # x du milieu de la raquette 2 xd_b = b.x + b.rayon # x gauche de la balle # Si la balle possède un x au contact de la raquette 2 if xd_b >= xg_2 and xg_b < xm_2: yh_2 = r2.y # y haut de la raquette 2 yb_2 = r2.y + r2.hauteur # y bas de la raquette 2 yh_b = b.y - b.rayon # y haut de la balle yb_b = b.y + b.rayon # y bas de la balle # Si la balle est également verticalement dans la zone de la raquette 1 if (yb_b >= yh_2) and (yh_b <= yb_2): return 2 # Si on arrive ici, c'est que la balle n'est au contact d'aucune raquette return 0 # 5 - PROGRAMME DE TEST ================================================ if __name__ == "__main__": jeu = Jeu()

Questions

  • Quelle est la seule ligne de code du programme ?
  • Cette ligne sera-t-elle exécutée si on importe ce module depuis un autre fichier ?
  • Donner la liste des différents objets et du contenu de leur attributs lorsqu'on exécute la ligne 157.

18 (pyxel)° Passons maintenant à vue.py que vous enregistrerez également dans le répertoire Pong.

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
# 1 - Importations ============================================================ import pyxel import random from modele import Raquette # Uniquement pour la documentation des types from modele import Balle # idem from modele import Jeu # idem # 2 - Constantes ============================================================== LRG = 128 HTR = 128 # 3 - Déclarations des classes ================================================ # 4 - Déclarations des fonctions ============================================== def actualiser(jeu:Jeu) -> None: """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: """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: """Dessine une balle""" pyxel.circ(int(b.x), int(b.y), b.rayon, b.couleur) # (x,y) est le centre # 5 - PROGRAMME DE TEST ============================================ if __name__ == "__main__": def controler(): # pour tester la VUE uniquement pass def afficher(): # pour tester la VUE uniquement actualiser(jeu) jeu = Jeu() pyxel.init(128, 128, title="Mon jeu") # Démarrage de la fenêtre graphique pyxel.run(controler, afficher) # Alternance controle/vue 30 fois par seconde

Questions et actions à réaliser

  • Lancer directement ce programme pour voir qu'il affiche certains éléments.
  • Suivre le déroulement du programme de test et les importations pour répondre à ces questions :
    • Ligne 45 : pourquoi peut-on directement utiliser le constructeur Jeu() lors du test ?
    • Ligne 46 : quelles seront les dimensions en pixels de notre jeu de test ?
    • Ligne 47 : quelles sont les deux fonctions qui vont être appelées 30 fois par seconde ?
    • Lignes 47 puis 42 puis... : suivre le déroulement de la fonction afficher() pour comprendre ce qu'elle affiche : quelle est la fonction qui gère réellement l'affichage au final ?
    • Lancer des recherches sur le Web pour comprendre ce qu'on doit envoyer aux fonctions rect() et circ() du module pyxel.

19 (pyxel)° Reste à définir le coeur de notre Pong : le fichier controleur.py contient les instructions qui vont faire la liaison entre les données du Modèle et l'affichage de la Vue.

Enregistrez ce nouveau programme au même endroit que les deux autres et testez que les deux raquettes peuvent effectivement bouger en utilisant :

  • touches A et Q pour le joueur de gauche,
  • flèches Haut et Bas pour le joueur de droite.
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
""" 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 """ # 1 - Importation(s) ========================================================== import pyxel import random import modele # module du modèle / données / data import vue # module de la vue / view # 2 - Constantes et variables globales ============================================================== jeu = modele.Jeu() # jeu stocke globalement la référence d'une instance de Jeu # 3 - Déclarations des classes ================================================ # 4 - Déclarations des fonctions ============================================== def controler() -> None: """Fonction qui récupère les événements et demande de modifier les données en conséquence""" if pyxel.btn(pyxel.KEY_A): # Si on a appuyé sur A modele.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 modele.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 modele.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 modele.mvt_raq(jeu.j2.raq, 0, 1) # on modifie les données de la raquette 2 modele.mvt_balle(jeu.balle) # on modifie les données de la balle def afficher() -> None: """Demande à la vue de créer la nouvelle image""" vue.actualiser(jeu) # 5 - PROGRAMME PRINCIPAL====================================================== if __name__ == "__main__": pyxel.init(modele.LRG, modele.HTR, title="Mon jeu") # Démarrage de la fenêtre graphique pyxel.run(controler, afficher) # Alternance controle/vue 30 fois par seconde

Questions

  1. En regardant le programme de test, quelles seront les dimensions graphiques de notre jeu ?
  2. Quelle est la fonction (qui sera appelée par la fonction-constructeur affichage()) pour gérer concrétement l'affichage ?
  3. Quelle est la fonction qui va gérer les actions de l'utilisateur 30 fois par seconde ?
  4. En allant lire le code de la fonction controler(), dire comment se nomme la fonction du module pyxel qui permet de gérer l'appui sur les boutons.
  5. Comment se nomme la constante du module pyxel qui code la touche A puis la flèche du haut ?
  6. Aller voir cette page : projet pyxel sur github. Elle vous permettra de voir les constantes associées aux numéros de couleur 0 à 15 et de voir le nom des constantes associées aux différentes touches du clavier.
  7. Expliquer ce que signifie jeu.j2.raq en ligne 39 ? Pour cela, partez de l'objet jeu et suivez les attributs au fur et à mesure.
  8. Expliquer ce qu'il se passe lorsqu'on appuie sur la flèche du haut : quelle est la fonction qui fait bouger la raquette ? Quelle raquette bouge ? Pourquoi va-t-elle vers le haut ?
  9. Sur quelle ligne demande-t-on de bouger la balle 30 fois par seconde ? Avec quelle fonction ? Où se situe cette fonction ?

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

  1. Expliquer ce que signifie jeu.balle en ligne 43.
  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 certainement faire un schéma papier avec des coordonnées pour comprendre la situation.

🏠 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.

7 - Pyxel directement sur Pyxelstudio

Cette partie est destinée à ceux qui ne parviennent pas lancer directement le module Pyxel depuis leur éditeur de texte local.

7.1 Principe d'un programme pyxel

Logo Pyxel
Logo Pyxel

Un programme utilisant le module pyxel est basé sur une boucle infinie décrite ci-dessous.

Principe de fonctionnement

    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 mise à jour date de 1/30e de seconde ou plus

        On active la fonction controler() qui mettra les données à jour en fonction des événements détéctés.

        On active la fonction afficher() qui effacera l'écran avec cls() puis générera le nouvel affichage.

      Fin du SI

    Fin TANT QUE

Au vu de la condition sur l'activation des deux fonctions principales (nommées controler et afficher ici), on voit qu'il va y avoir 30 images par seconde.

La structure usuelle du code Python pour ce module
1 2 3 4 5 6 7 8 9 10 11 12 13
import pyxel def controler(): """Récupère l'événement et le gère les données (30 fois par seconde)""" ... # A modifier def afficher(): """création des objets (30 fois par seconde)""" pyxel.cls(0) # vide la fenetre ... # A modifier pyxel.init(128, 128, title="Mon premier jeu") pyxel.run(controler, afficher)
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 (pyxelstudio)° Aller sur le site pyxelstudio et ouvrir un nouveau projet en cliquant sur Créer.

Regardons ce que contient votre écran.

A FAIRE

17 (pyxel studio)° Enregistrer ce programme dans app.py :

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
# 1 - Importation(s) ========================================================== import random import pyxel # pour Vue et Controleur en réalité # MODELE ======================================================================== # =============================================================================== # 2 - Constantes et variables globales du MODELE ======================================== LRG = 128 HTR = 128 # 3 - Déclaration des classes du MODELE ======================================================== 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 du MODELE ============================================== def mvt_raq(r:Raquette, dx:int, dy:int) -> None: """Modifie les données de l'objet pour intégrer le déplacement dx et dy voulu""" r.x = gerer_x_raquette(r.x, dx, r.j.jeu.DIMX) # impossible d'aller plus loin que la limite r.y = gerer_y_raquette(r.y, dy, r.j.jeu.DIMY) # pour apparaître en bas si on sort en haut et inversement def mvt_balle(b:Balle) -> None: """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) # c vaut 1 ou 2 si collision avec la raquette 1 ou 2 if c == 1: r1 = b.jeu.j1.raq # on récupère l'adresse de l'objet-raquette 1 b.x = r1.x + r1.epaisseur + b.rayon + 1 # on place la balle juste à droite de la raquette b.dx = -b.dx # on inverse la vitesse en x de la balle pour qu'elle part dans l'autre sens if c == 2: r2 = b.jeu.j2.raq # on récupère l'adresse de l'objet-raquette 2 b.x = r2.x - b.rayon - 1 # on place la balle juste à gache de la raquette b.dx = -b.dx # on inverse la vitesse en x de la balle pour qu'elle part dans l'autre sens # gestion des collisions éventuelles avec plafond et sol if b.y > b.jeu.DIMY: # si la balle atteint le plafond b.y = b.jeu.DIMY - 1 # on modifie sa position pour bien la replacer b.dy = -b.dy # on inverse la vitesse en y de la balle pour qu'elle part dans l'autre sens elif b.y < 1: # si la balle atteint le sol b.y = 1 # on modifie sa position pour bien la replacer b.dy = -b.dy # on inverse la vitesse en y de la balle pour qu'elle part dans l'autre sens def gerer_y_raquette(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_x_raquette(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 # Tests de contact avec la raquette 1 xd_1 = r1.x + r1.epaisseur # x droite de la raquette 1 xm_1 = r1.x + r1.epaisseur // 2 # x du milieu de la raquette 1 xg_b = b.x - b.rayon # x gauche de la balle # Si la balle possède un x au contact de la raquette 1 if xg_b <= xd_1 and xg_b > xm_1: yh_1 = r1.y # y haut de la raquette 1 yb_1 = r1.y + r1.hauteur # y bas de la raquette 1 yh_b = b.y - b.rayon # y haut de la balle yb_b = b.y + b.rayon # y bas de la balle # Si la balle est également verticalement dans la zone de la raquette 1 if (yb_b >= yh_1) and (yh_b <= yb_1): return 1 # Tests de contact avec la raquette 2 xg_2 = r2.x # x gauche de la raquette 2 xm_2 = r2.x + r2.epaisseur # x du milieu de la raquette 2 xd_b = b.x + b.rayon # x gauche de la balle # Si la balle possède un x au contact de la raquette 2 if xd_b >= xg_2 and xg_b < xm_2: yh_2 = r2.y # y haut de la raquette 2 yb_2 = r2.y + r2.hauteur # y bas de la raquette 2 yh_b = b.y - b.rayon # y haut de la balle yb_b = b.y + b.rayon # y bas de la balle # Si la balle est également verticalement dans la zone de la raquette 1 if (yb_b >= yh_2) and (yh_b <= yb_2): return 2 # Si on arrive ici, c'est que la balle n'est au contact d'aucune raquette return 0 # 5 - PROGRAMME DE TEST du MODELE ================================================ if __name__ == "__main__": jeu = Jeu()

Questions

  • Quelle est la seule ligne de code du programme ?
  • Cette ligne sera-t-elle exécutée si on importe ce module depuis un autre fichier ?
  • Donner la liste des différents objets et du contenu de leur attributs lorsqu'on exécute la ligne 159.

18 (pyxel studio)° Passons maintenant à la partie vue que vous enregistrerez dans le même fichier : le code est situé sous celui du modèle, à partir de la ligne 144. Le programme de test est modifié et va maintenant afficher des éléments en fonction des valeurs fournies par le modèle.

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 187 188 189 190 191
# 1 - Importation(s) ========================================================== import random import pyxel # pour Vue et Controleur en réalité # MODELE ======================================================================== # =============================================================================== # 2 - Constantes et variables globales du MODELE ======================================== LRG = 128 HTR = 128 # 3 - Déclaration des classes du MODELE ======================================================== 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 du MODELE ============================================== def mvt_raq(r:Raquette, dx:int, dy:int) -> None: """Modifie les données de l'objet pour intégrer le déplacement dx et dy voulu""" r.x = gerer_x_raquette(r.x, dx, r.j.jeu.DIMX) # impossible d'aller plus loin que la limite r.y = gerer_y_raquette(r.y, dy, r.j.jeu.DIMY) # pour apparaître en bas si on sort en haut et inversement def mvt_balle(b:Balle) -> None: """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) # c vaut 1 ou 2 si collision avec la raquette 1 ou 2 if c == 1: r1 = b.jeu.j1.raq # on récupère l'adresse de l'objet-raquette 1 b.x = r1.x + r1.epaisseur + b.rayon + 1 # on place la balle juste à droite de la raquette b.dx = -b.dx # on inverse la vitesse en x de la balle pour qu'elle part dans l'autre sens if c == 2: r2 = b.jeu.j2.raq # on récupère l'adresse de l'objet-raquette 2 b.x = r2.x - b.rayon - 1 # on place la balle juste à gache de la raquette b.dx = -b.dx # on inverse la vitesse en x de la balle pour qu'elle part dans l'autre sens # gestion des collisions éventuelles avec plafond et sol if b.y > b.jeu.DIMY: # si la balle atteint le plafond b.y = b.jeu.DIMY - 1 # on modifie sa position pour bien la replacer b.dy = -b.dy # on inverse la vitesse en y de la balle pour qu'elle part dans l'autre sens elif b.y < 1: # si la balle atteint le sol b.y = 1 # on modifie sa position pour bien la replacer b.dy = -b.dy # on inverse la vitesse en y de la balle pour qu'elle part dans l'autre sens def gerer_y_raquette(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_x_raquette(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 # Tests de contact avec la raquette 1 xd_1 = r1.x + r1.epaisseur # x droite de la raquette 1 xm_1 = r1.x + r1.epaisseur // 2 # x du milieu de la raquette 1 xg_b = b.x - b.rayon # x gauche de la balle # Si la balle possède un x au contact de la raquette 1 if xg_b <= xd_1 and xg_b > xm_1: yh_1 = r1.y # y haut de la raquette 1 yb_1 = r1.y + r1.hauteur # y bas de la raquette 1 yh_b = b.y - b.rayon # y haut de la balle yb_b = b.y + b.rayon # y bas de la balle # Si la balle est également verticalement dans la zone de la raquette 1 if (yb_b >= yh_1) and (yh_b <= yb_1): return 1 # Tests de contact avec la raquette 2 xg_2 = r2.x # x gauche de la raquette 2 xm_2 = r2.x + r2.epaisseur # x du milieu de la raquette 2 xd_b = b.x + b.rayon # x gauche de la balle # Si la balle possède un x au contact de la raquette 2 if xd_b >= xg_2 and xg_b < xm_2: yh_2 = r2.y # y haut de la raquette 2 yb_2 = r2.y + r2.hauteur # y bas de la raquette 2 yh_b = b.y - b.rayon # y haut de la balle yb_b = b.y + b.rayon # y bas de la balle # Si la balle est également verticalement dans la zone de la raquette 1 if (yb_b >= yh_2) and (yh_b <= yb_2): return 2 # Si on arrive ici, c'est que la balle n'est au contact d'aucune raquette return 0 # VUE ======================================================================== # =============================================================================== # 5 - Déclarations des fonctions de la VUE ============================================== def actualiser(jeu:Jeu) -> None: """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: """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: """Dessine une balle""" pyxel.circ(int(b.x), int(b.y), b.rayon, b.couleur) # (x,y) est le centre # PROGRAMME DE TEST ================================================================== # =============================================================================== # 6 - PROGRAMME DE TEST ============================================ if __name__ == "__main__": def controler(): # pour tester la VUE uniquement pass def afficher(): # pour tester la VUE uniquement actualiser(jeu) jeu = Jeu() pyxel.init(128, 128, title="Mon jeu") # Démarrage de la fenêtre graphique pyxel.run(controler, afficher) # Alternance controle/vue 30 fois par seconde

Questions et actions à réaliser

  • Lancer directement ce programme pour voir qu'il affiche certains éléments.
  • Suivre le déroulement du programme de test et les importations pour répondre à ces questions :
    • Ligne 189 : imaginons que la classe Jeu soit dans un fichier à part, nommé modele.py, quelle importation aurait-on dû faire pour pouvoir utiliser directement le constructeur Jeu() lors du test ?
    • Ligne 190 : quelles seront les dimensions en pixels de notre jeu de test ?
    • Ligne 191 : quelles sont les deux fonctions qui vont être appelées 30 fois par seconde ?
    • Ligne 191-186-... : suivre le déroulement de la fonction afficher() pour comprendre ce qu'elle affiche : quelle est la fonction qui gère réellement l'affichage au final ?
    • Lancer des recherches sur le Web pour comprendre ce qu'on doit envoyer aux fonctions rect() et circ() du module pyxel.

19 (pyxel studio)° Reste à définir le coeur de notre Pong : le controleur regroupe les instructions qui vont faire la liaison entre les données du Modèle et l'affichage de la Vue.

Enregistrez ce nouveau programme et testez que les deux raquettes peuvent effectivement bouger en utilisant :

  • touches A et Q pour le joueur de gauche,
  • flèches Haut et Bas pour le joueur de droite.
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 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
# 1 - Importation(s) ========================================================== import random import pyxel # pour Vue et Controleur en réalité # MODELE ======================================================================== # =============================================================================== # 2 - Constantes et variables globales du MODELE ======================================== LRG = 128 HTR = 128 # 3 - Déclaration des classes du MODELE ======================================================== 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 du MODELE ============================================== def mvt_raq(r:Raquette, dx:int, dy:int) -> None: """Modifie les données de l'objet pour intégrer le déplacement dx et dy voulu""" r.x = gerer_x_raquette(r.x, dx, r.j.jeu.DIMX) # impossible d'aller plus loin que la limite r.y = gerer_y_raquette(r.y, dy, r.j.jeu.DIMY) # pour apparaître en bas si on sort en haut et inversement def mvt_balle(b:Balle) -> None: """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) # c vaut 1 ou 2 si collision avec la raquette 1 ou 2 if c == 1: r1 = b.jeu.j1.raq # on récupère l'adresse de l'objet-raquette 1 b.x = r1.x + r1.epaisseur + b.rayon + 1 # on place la balle juste à droite de la raquette b.dx = -b.dx # on inverse la vitesse en x de la balle pour qu'elle part dans l'autre sens if c == 2: r2 = b.jeu.j2.raq # on récupère l'adresse de l'objet-raquette 2 b.x = r2.x - b.rayon - 1 # on place la balle juste à gache de la raquette b.dx = -b.dx # on inverse la vitesse en x de la balle pour qu'elle part dans l'autre sens # gestion des collisions éventuelles avec plafond et sol if b.y > b.jeu.DIMY: # si la balle atteint le plafond b.y = b.jeu.DIMY - 1 # on modifie sa position pour bien la replacer b.dy = -b.dy # on inverse la vitesse en y de la balle pour qu'elle part dans l'autre sens elif b.y < 1: # si la balle atteint le sol b.y = 1 # on modifie sa position pour bien la replacer b.dy = -b.dy # on inverse la vitesse en y de la balle pour qu'elle part dans l'autre sens def gerer_y_raquette(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_x_raquette(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 # Tests de contact avec la raquette 1 xd_1 = r1.x + r1.epaisseur # x droite de la raquette 1 xm_1 = r1.x + r1.epaisseur // 2 # x du milieu de la raquette 1 xg_b = b.x - b.rayon # x gauche de la balle # Si la balle possède un x au contact de la raquette 1 if xg_b <= xd_1 and xg_b > xm_1: yh_1 = r1.y # y haut de la raquette 1 yb_1 = r1.y + r1.hauteur # y bas de la raquette 1 yh_b = b.y - b.rayon # y haut de la balle yb_b = b.y + b.rayon # y bas de la balle # Si la balle est également verticalement dans la zone de la raquette 1 if (yb_b >= yh_1) and (yh_b <= yb_1): return 1 # Tests de contact avec la raquette 2 xg_2 = r2.x # x gauche de la raquette 2 xm_2 = r2.x + r2.epaisseur # x du milieu de la raquette 2 xd_b = b.x + b.rayon # x gauche de la balle # Si la balle possède un x au contact de la raquette 2 if xd_b >= xg_2 and xg_b < xm_2: yh_2 = r2.y # y haut de la raquette 2 yb_2 = r2.y + r2.hauteur # y bas de la raquette 2 yh_b = b.y - b.rayon # y haut de la balle yb_b = b.y + b.rayon # y bas de la balle # Si la balle est également verticalement dans la zone de la raquette 1 if (yb_b >= yh_2) and (yh_b <= yb_2): return 2 # Si on arrive ici, c'est que la balle n'est au contact d'aucune raquette return 0 # VUE ======================================================================== # =============================================================================== # 5 - Déclarations des fonctions de la VUE ============================================== def actualiser(jeu:Jeu) -> None: """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: """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: """Dessine une balle""" pyxel.circ(int(b.x), int(b.y), b.rayon, b.couleur) # (x,y) est le centre # CONTROLEUR ======================================================================== # 6 - Déclarations des fonctions du CONTROLEUR ============================================== def controler() -> None: """Fonction qui récupère les événements et demande de modifier 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 afficher() -> None: """Demande à la vue de créer la nouvelle image""" actualiser(jeu) # PROGRAMME DE TEST ================================================================== # =============================================================================== # 7 - PROGRAMME DE TEST ============================================ if __name__ == "__main__": jeu = Jeu() pyxel.init(LRG, HTR, title="Mon jeu") # Démarrage de la fenêtre graphique pyxel.run(controler, afficher) # Alternance controle/vue 30 fois par seconde

Questions

  1. En regardant le programme de test, quelles seront les dimensions graphiques de notre jeu ?
  2. Quelle est la fonction (qui va être appelée par la fonction-contrôleur afficher()) pour gérer concrétement l'affichage ?
  3. Quelle est la fonction qui va gérer les actions de l'utilisateur 30 fois par seconde ?
  4. En allant lire le code de la fonction controler(), dire comment se nomme la fonction du module pyxel qui permet de gérer l'appui sur les boutons.
  5. Comment se nomme la constante du module pyxel qui code la touche A puis la flèche du haut ?
  6. Aller voir cette page : projet pyxel sur github. Elle vous permettra de voir le nom des constantes associées aux différentes touches du clavier.
  7. Expliquer ce que signifie jeu.j2.raq en ligne 191 ? Pour cela, partez de l'objet jeu et suivez les attributs au fur et à mesure.
  8. Expliquer ce qu'il se passe lorsqu'on appuie sur la flèche du haut : quelle est la fonction qui fait bouger la raquette ? Quelle raquette bouge ? Pourquoi va-t-elle vers le haut ?
  9. Sur quelle ligne demande-t-on de bouger la balle 30 fois par seconde ? Avec quelle fonction ? Dans quelle partie (VUE, CONTROLEUR ou MODELE) se situe cette fonction ?

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

  1. Expliquer ce que signifie jeu.balle en ligne 195.
  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 certainement faire un schéma papier avec des coordonnées pour comprendre la situation.

🏠 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.