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 Programmation Orienté Objet, car vous avez la Classe !
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 :
Avec une adresse réelle : une vraie adresse-mémoire
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, une adresse symbolique. Plutôt que d'utiliser ce terme, je parlerai constamment d'adresse, sans plus d'information. Plus court, plus simple. Il n'en reste pas moins qu'il s'agit bien d'un identifiant et donc d'une adresse symbolique.
1.1 Variable et type simple
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
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.
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.
Comportement visible vue 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.
Comportement réel privé et comportement 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")
Une variable "conteneur" fait référence à une structure de données qui contient plusieurs données.
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é.
Principe d'organisation interne
Voici les deux choses à comprendre :
La variable d'un conteneur contient l'adresse de la structure et pas le contenu de la structure.
Il existe un mécanisme permettant de trouver l'adresse d'une case en fonction :
de la valeur de son indice pour list, tuple et str : on doit taper nom[i]
de la valeur de sa clé pour dict : on doit taper nom[cle]
Sur mon exemple fictif, c'est simple : l'adresse correspond à l'adresse du tableau plus la valeur de l'indice.
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'.
Comportement visible vue 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
Comportement réel privé et comportement 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
t étant bien la référence vers l'" du tableau, on peut l'utiliser pour aller 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 qui se passe en interne : l'ordinateur a juste modifié vers où aller à partir de l'adresse 1485 : on doit aller vers l'adresse 100 et plus vers l'adresse 68.
Exemple 2 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 :
En ligne 1, on crée un nouveau objet-fichier en utilisant la fonction-constructeur native nommée open().
La variable crayon 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
importtkinterastkfenetre=tk.Tk()fenetre.geometry("600x300")fenetre.title("Ma première interface graphique")fenetre.configure(bg=FOND)
En ligne 2, on crée un nouveau objet de classe Tkinter en utilisant le constructeur nommée Tk() contenu dans le module connu sous le nom tk.
La variable fenetre contient alors l'adresse de notre nouvel objet.
En lignes 4, 5 et 6, on agit sur notre objet en utilisant son adresse : on va sur place et on y cherche les méthodes geometry(), title(), et 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 contenus et plusieurs fonctions (nommés 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.
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...).
Les instructions d'une Classe permettent de réaliser des 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
2.2.1 - 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 pour toute déclaration en Python)
On placera une docstring d'une ligne décrivant la Classe.
Les instructions à réaliser lors de la création sont à décaler 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
classPersonnage:"""Ceci est une classe permettant de créer un personnage dans mon super RPG"""pass
2.2.2 - Informations sur la Classe
Une fois que la Classe Personnage est en mémoire, on peut demander le type de cette donnée avec la fonction native type(). L'interpréteur répond alors qu'il s'agit d'une Classe en signalant qu'il s'agit d'un type... type.
>>> type(Personnage)
<class 'type'>
Lorsque Python répond que le type est type, il signale qu'il s'agit d'une Classe configurée manuellement. Il ne s'agit pas d'un type natif comme int, float, list... qui eux ne commencent pas par une majuscule.
Comme toute donnée, les instructions liées à la Classe sont stockées quelque part en mémoire et possèdent une adresse :
>>> id(Personnage)
21741560
Si vous demander à la console Python d'évaluer votre classe Personnage, elle n'est pas configurée pour faire quelque chose de particulier : elle indique simplement le nom de la classe et l'endroit où elle est définie. Ci-dessous, elle indique que la classe Personnage est définie dans le programme principal (__main__), pas depuis un module.
>>> Personnage
<class '__main__.Personnage'>
Maintenant que nous avons notre moule, nous allons pouvoir créer des objets-personnages.
2.3 - Création d'objets
La Classe étant en mémoire, nous pouvons réaliser de véritables objets, également nommés instances de cette Classe.
2.3.1 Instances
Instanciation : création d'un nouvel objet à partir du Constructeur d'une Classe.
Instance : synonyme d'objet. La différence est qu'on dira instance de Personnage alors qu'on parlera juste d'objet. On glisse donc le nom de la Classe derrière le mot instance.
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 en rajoutant des parenthèses derrière !
Classe : Personnage
Constructeur : Personnage()
Cette fonction Constructeur renvoie l'adresse du nouvel objet.
Pas d'entrée particulière
⇒
Constructeur
⇒
Adresse d'un nouvel objet
Objet / Instance de Personnage : x = Personnage()
C'est donc exactement la même chose que lorsqu'on crée un tableau de type list : la variable contient l'adresse du conteneur et pas le contenu (voir 1.2 Variable et type construit)
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 l'objet alice n'est pas la même chose que la classe Personnage.
De la même façon, alice et bob sont deux objets indépendants.
2.3.2 - Tester la classe d'origine d'un objet
On peut savoir si une variable référence un objet d'une classe donnée de deux façons.
Soit avec la fonction native type() :
>>> type(alice) == Personnage
True
Soit avec la fonction native isinstance() dont voici la syntaxe :
Les deux tests ne testent pas exactement la même chose mais, dans le cadre réduit de la NSI, elles auront toujours un comportement identique.
2.3.3 - Comportement visible vue de l'extérieur
Contrairement aux types natifs (int, float, list...), les objets créés à partir d'une Classe n'affichent pas leur contenu automatiquement. Si on demande d'afficher le contenu d'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 la Classe Personnage et alice qui est une instance de Personnage.
Pour rappel, voici ce que donne la demande pour une liste vide :
>>> t = list()
>>> t
[]>>> t2 = []
>>> t2
[]
On voit bien la différence entre un type natif et une Classe.
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, alors que l'adresse visualisée directement via l'objet est donnée en hexadécimal (elle commence par 0x)
>>> id(alice)
139663546393488>>> alice
<__main__.Personnage object at 0x7f05f4122390>>>> 0x7f05f4122390 == 139663546393488
True>>> int(0x7f05f4122390)
139663546393488>>> hex(139663546393488)
'0x7f05f4122390'
3 - Gestion des attributs à la volée (Mauvaise pratique)
Pour l'instant, la classe Personnage est un moule vide et les objets qu'on crée à partir d'elle sont donc des conteneurs vides. Ca ne sert à rien et ne contient rien de particulier.
Voyons comment stocker des variables rattachées à notre objet.
3.1 - Création d'attributs à la volée
3.1.1 Attributs
Un attribut est une variable placée à l'intérieur d'un objet.
3.1.2 Rajouter des attributs
Pour rajouter des attributs (des variables) à un objet, on utilise une affectation combinée à la syntaxe du point : objet.attribut = valeur
>>> alice.agilite = 18
On agit sur alice en lui rajoutant un nouvel 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 classesclassPersonnage:"""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 ? Il crée un objet bob (ligne 7).
Lignes 8-9-10 : on rajoute des attributs en utilisant la syntaxe du point. C'est comme si on avait rajouté trois variables à l'intérieur de l'objet.
3.1.3 - Récupérer les valeurs des attributs
On peut accéder à la valeur d'un attribut avec la syntaxe objet.attribut.
>>> bob.nom
'Skywalker'>>> bob.prenom
'Bob'
3.1.3 - Modifier les valeurs des attributs
Les objets sont fortement muables. On peut donc modifier leurs attributs sans modifier l'adresse de l'objet lui-même. Il s'agit juste de modifier le contenu du conteneur. Toujours avec la même syntaxe : objet.attribut = nouvelle_valeur
>>> bob.nom = "Bond"
>>> bob.nom
'Bond'
3.2 - Principe de la structure d'un objet
Un objet possède une 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 :
La codification bob.niveau veut donc dire en Français : va voir à l'adresse de cet objet et trouve le contenu qui correspond à cet attribut.
On comprend mieux la possibilité :
De lire le contenu d'un attribut : bob.niveau
De modifier le contenu d'un attribut avec une affectation du type bob.niveau=5, et cela sans modifier l'adresse de la structure : les objets sont muables.
Jamais plus d'attributs créés à la volée
Les deux problèmes du rajout d'attributs à la volée sont liés :
il faudrait lire toutes les lignes de code pour connaître tous les attributs qu'on a créé. Pas top avec 300000 lignes.
en conséquence, on pourrait écraser le contenu d'un attribut qui existe déjà sans s'en rendre compte, en utilisant un nom d'attribut qui est déjà utilisé mais qu'on n'avait pas vu.
Même si Python permet de rajouter des attributs à la volée n'importe où dans le code, on veillera à ne pas le faire.
C'est ici qu'intervient l'initialisation via la méthode __init__() dont l'un des buts est justement de centraliser la création des attributs (comme la fonction immeuble_aleatoire() du projet rue).
4 - Gestion des attributs avec la méthode spéciale __init__ (bonne pratique}
Voyons maintenant plus en détails comment se déroule l'instanciation.
4.1 - Les 3 étapes lors de l'instanciation
Réservation d'une adresse (et donc d'un espace mémoire)
Initialisation du contenu à cette adresse
Renvoi de l'adresse du nouvel objet
Pour réaliser l'instanciation d'un nouvel objet Personnage, on utilise le constructeur Personnage(), :
>>> alice = Personnage()
Voyons les étapes que va suivre le Constructeur.
Réservation d'une adresse
D'abord, on commence par réserver une nouvelle place mémoire et générer l'espace des noms. Après cette étape, l'objet est créé et possède une adresse personnelle.
Initialisation des attributs
Ensuite, la méthode spéciale __init__() est activée automatiquement 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__(). Lors de cette phase, on ne fait donc... rien.
1
2
classPersonnage:"""Ceci est une classe permettant de créer un personnage dans mon super RPG"""
Renvoi de l'adresse
Finalement, le Constructeur peut renvoyer l'adresse de l'objet créé avec son return. A qui ? 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.
4.2 - La méthode spéciale __init__()
4.2.1 - 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.
4.2.2 - 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'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
classPersonnage:"""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=0self.profession='aucune'
Lignes 5-6-7-8 : on comprend qu'on va créer 4 attributs nommés nom, prenom, niveau et profession.
De la même façon, on voit facilement quelles seront leur valeurs initiales.
4.2.3 - Quelles conséquences lors de la création ?
Cette fois, les instances de Personnage ne sont plus vides au départ. Il a bien 4 attributs et ils possèdent les valeurs visibles sur les lignes 5 à 8.
Nous l'avons déjà dit, à centraliser l'information sur les attributs disponibles.
La méthode spéciale __init__() est donc nommée la méthode-initialisateur ou même, parfois, juste l'initialisateur.
Par simplification, on la nomme parfois également méthode-constructeur ou même juste constructeur. C'est un abus de langage, mais il est très courant. Le vrai constructeur est ici Personnage() mais comme la méthode __init__() est activée automatiquement lors de la phase 2 de l'action du Constructeur, on comprend que ne pas faire la distinction n'est pas si grave que cela.
4.3 Le paramètre self
La particularité de self
Si vous observez la déclaration de la méthode __init__(), vous constaterez qu'elle possède un paramètre nommé self.
1
2
3
4
5
6
7
8
classPersonnage:"""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=0self.profession='aucune'
Voici la création d'alice :
>>> alice = Personnage()
Le paramètre self est particulier puisque vous ne l'envoyez pas au constructeur Personnage() pour qu'il le transmette à la méthode __init__().
Que contient donc self ?
Le contenu de self
Voici un code imaginaire simulant les trois étapes de travail du constructeur Personnage() :
Instructions de Personnage():
adresse = reserver_adresse_pour_objet()Personnage.__init__(adresse)returnadresse
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 l'adresse de l'objet en cours.
BILAN (hyper-important pour comprendre la POO)
Le paramètre self :
est toujours placé en premier dans le prototype de la méthode,
n'est jamais envoyé directement lors de l'appel
contiendra l'adresse de l'objet qui a lancé l'appel.
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
4.4.1 Principe
Tous les arguments envoyés au constructeur sont envoyés également à la méthode __init__().
Un exemple :
1
2
3
4
5
6
7
8
classPersonnage:"""Ceci est une classe permettant de créer un personnage dans mon super RPG"""def__init__(self,nom,pre,niv,pro):self.nom=nomself.prenom=preself.niveau=nivself.profession=pro
On peut alors lancer des appels au constructeur en utilisant par exemple :
Si on veut comprendre la transmission, il faut comparer :
l'appel au constructeur Personnage() et
1
p=Personnage("InBorderLand","Alice",20,"Héroïne")
la déclaration de la méthode __init__().
4
def__init__(self,nom,pre,niv,pro):
Que fait-on ensuite de ces valeurs stockées dans les paramètres ?
On voit ligne 6 que pre va alors servir à initialiser l'argument prenom par exemple.
1
2
3
4
5
6
7
8
classPersonnage:"""Ceci est une classe permettant de créer un personnage dans mon super RPG"""def__init__(self,nom,pre,niv,pro):self.nom=nomself.prenom=preself.niveau=nivself.profession=pro
4.3.2 Pratique usuelle mais qui peut paraître magique
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
classPersonnage:"""Ceci est une classe permettant de créer un personnage dans mon super RPG"""def__init__(self,nom,prenom,niveau,profession):self.nom=nomself.prenom=prenomself.niveau=niveauself.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 paramètres de la méthode et attributs de l'objet.
4.3.3 Paramètre par défaut
On peut avoir une situation où 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, nous avions vu qu'on peut donner des valeurs par défaut à certains paramètres.
1
2
3
4
5
6
7
8
classPersonnage:"""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=nomself.prenom=prenomself.niveau=niveauself.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'
Attention, si vous avez des paramètres sans valeur par défaut et d'autres avec valeur par défaut, il faut IMPERATIVEMENT
mettre au début du prototype les paramètres normaux puis
mettre en fin de prototype les paramètres avec valeurs par défaut.
1
2
3
4
classPersonnage:"""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"):
4.3.4 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")
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
classPersonnage:"""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
Quelle est la particularité du nom de la classe au niveau majuscule/minuscule (cela devrait vous sauter aux yeux) ?
Comment se nomment les deux variables qui contiennent des objets basés sur cette classe ?
Quelles sont les deux instructions qui ont permis de créer des objets basés sur la classe Personnage ?
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 à
Afficher le niveau du personnage
Afficher l'adresse du personnage
Incrémenter de 1 le niveau du personnage
Afficher le nouveau niveau du personnage
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 ?
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
classPersonnage:"""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=0self.profession='aucune'
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.
Le nom du personnage
L'adresse du personnage
L'age du capitaine
La date de création de l'objet
1
2
3
4
5
6
7
8
9
classPersonnage:"""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=0self.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__() ?
l'adresse de a
l'adresse de b
le contenu de a
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
classPersonnage:"""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=0self.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
classPersonnage:"""Ceci est une classe permettant de créer un personnage dans mon super RPG"""def__init__(self,a,b,c,d):self.nom=aself.prenom=bself.niveau=cself.profession=d
>>> bob = Personnage("Luke", "Skywalker", 5, "Jedi")
>>> bob.nom
'Luke'
On voit donc ici que :
Paramètre 1 : self. On ne le fournit pas d'argument. Il sera automatiquement affecté avec l'adresse de l'objet bob.
Paramètre 2 : a. Il reçoit l'argument-string "Luke", qui est normalement le prénom du personnage.
Paramètre 3 : b. Il reçoit l'argument-string "Skywalker", qui est normalement le nom du personnage.
Paramètre 4 : c. Il reçoit l'argument-entier 5, qui est le niveau du personnage.
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).
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 programmeur d'initialiser un objet nouvellement créé : __init__()
Méthode permettant au programmeur d'expliquer comment gérer l'addition de deux objets de même type : __add__()
Méthode permettant au programmeur 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.
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
classPersonnage:"""Ceci est une classe permettant de créer un personnage dans mon super RPG"""def__init__(self,nom,pre,niv,pro):self.nom=nomself.prenom=preself.niveau=nivself.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
classPersonnage:"""Ceci est une classe permettant de créer un personnage dans mon super RPG"""def__init__(self,nom,prenom,niveau,profession):self.nom=nomself.prenom=prenomself.niveau=niveauself.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
classPersonnage:"""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=nomself.prenom=prenomself.niveau=niveauself.profession=profession
>>> jim = Personnage(prenom="Toto", nom="l'Asticot", niveau=10)
>>> jim.profession
'???'
🏠 DM° Réaliser le Devoir Maison (voir le lien) en rédigeant correctement vos réponses.
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
classPersonnage:"""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")returnsuper(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
classPersonnage:"""Ceci est une classe permettant de créer un personnage dans mon super RPG"""pv_max=100# pv_max est une variable de classe Personnagedef__init__(self,nom,pv):self.nom=nom# self.nom est une variable d'instance ou attributifpv>Personnage.pv_max:self.pv=Personnage.pv_maxelse: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.