python objets

Identification

Infoforall

36 - Création d'objets avec Python


Activité en cours de modification

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 ✎ : questions 07-08-09-10-12-14-17-18-20

Documents de cours : open document ou pdf

1 - Rappel pour percevoir ce qu'est un objet

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

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

Variable "simple"

Modèle simple de la boîte

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

>>> a = 5 >>> b = 9
représentation en boîte

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-identifant mémoire
  • L'espace mémoire contient des données rangées dans des zones identifiées par l'adresse-identifiant mémoire.
espace des noms et mémoire

C'est la raison pour laquelle, en Python, l'adresse-mémoire d'une variable change lorsqu'on lui donne une nouvelle affectation.

Comportement visible vue de l'extérieur

Connaître l'identifiant mémoire d'une variable est rarement utile à un humain.

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

>>> a = 5 >>> a 5

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

>>> id(t) 1485

Comportement réel privé et comportement visible publique

On retrouve donc ce que nous avions vu lors de l'activité sur les modules : le comportement réel d'un système (son fonctionnement privé interne) n'a pas forcément à voir avec son comportement visible (son interface a été programmé pour montrer ce qu'on veut qu'elle montre).

Variable "conteneur"

Une variable "conteneur" fait référence à une structure 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é (nous verrons dans l'activité Dictionnaire comment on réalise cela en pratique).

Principe d'organisation interne

Voici les deux choses à comprendre :

  • La variable d'un conteneur contient l'identifiant la structure et pas le contenu de la structure.
  • Il existe un mécanisme permettant de trouver la référence d'une case en fonction
    • de la valeur de son indice pour list, tuple et str et
    • de la valeur de sa clé pour dict.
  • Sur mon exemple fictif, c'est simple : l'identifiant 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'identifiant mémoire 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'identifiant mémoire, il faut utiliser la fonction native id().

>>> id(t) 1485

Comportement réel privé et comportement visible publique

Comme avec les contenus basiques. 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'adresse du tableau, on peut l'utiliser pour aller modifier le contenu des cases sans modifier l'adresse de la structure elle-même : vous pouvez modifier la page 5 d'un classeur sans devoir changer de classeur à chaque fois.

Ce qu'on voit via l'interface :

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

Ce qui se passe en interne :

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

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

  • La création d'un alias d'un tableau (deux variables menant à 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 de 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 à 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']

Nous avions vu l'année dernière que si le tableau contenait lui-même des structures et pas des données basiques, il fallait faire une copie profonde et pas une simple copie partielle.

Variable "objet"

De façon simplifiée, les objets sont des structures qui peuvent contenir à la fois des données internes et des fonctions internes.

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

Objet Turtle

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

1 2 3 4 5
import turtle as trt crayon = trt.Turtle() crayon.pencolor('red') crayon.forward(200)

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.

La variable crayon contient alors l'identifiant mémoire de notre nouvel objet.

En lignes 4 et 5, on agit sur notre objet en utilisant sa référence mémoire : on va sur place et on cherche une "fonction" se nommant pencolor() et pencolor().

Ces "fonctions" sont particulières puisqu'elle sont directement présentes dans l'objet lui-même et qu'on y accède avec la syntaxe OBJET.METHODE().

Objet Fichier

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

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-constructeur native nommée open().

La variable crayon contient alors l'identifiant mémoire de notre nouvel objet.

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

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

Encore une fois, on retrouve la syntaxe OBJET.METHODE().

Objet Tkinter

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

1 2 3 4 5 6
import tkinter as tk fenetre = tk.Tk() 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 fenetren contient alors l'identifiant mémoire de notre nouvel objet.

En lignes 4, 5 et 6, on agit sur notre objet en utilisant sa référence mémoire : 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 faux)

  • Il existe des contenus simples ne faisant référence qu'un contenu unique
  • Il existe des structures plus complexes permettant de stocker plusieurs contenus
  • Il existe des structures encore plus complexes permettant de stocker plusieurs contenus et même des "fonctions. Il s'agit des objets.

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 !

>>> 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 - Création d'un objet : l'instanciation

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 gateau, vous serez capable d'en créer autant que vous le voulez.

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

C'est pareil en informatique.

2.1 - Classe et instance d'une classe

Les instructions d'une Recette permet de réaliser de vrais gateaux, tous un peu similaire mais potentiellement différents : tous les gateaux n'auront pas les mêmes caractéristiques (forme, couleur, taille, gout...).

Les instructions d'une Classe permet de réaliser des objets : ils auront au départ des attributs identiques mais pourront être personnalisés.

2.2 - Création d'une Classe vide

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 comme pour les fonctions une documentation-description d'une ligne.

Les instructions à réaliser lors de la création sont à décaler de 4 espaces, comme avec les fonctions, les boucles ou les instructions conditionnelles. Ici, c'est juste pass : ne rien faire de particulier.

1 2 3
class Personnage: "Ceci est une classe permettant de créer un personnage dans mon super RPG" pass

Informations sur la Classe (le moule)

Une fois que la Classe Personnage est en mémoire, on peut demander le nom de ce type de données 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 d'une donnée est 'type', il veut donc dire qu'il s'agit d'une Classe configuré manuellement. Il ne s'agit pas d'un type natif comme int, float, list... qui eux ne commencent pas par une majuscule.

Comme toutes données, les instructions liées à la Classe sont stockées quelque part en mémoire :

>>> id(Personnage) 21741560

Si vous demander à la console Python d'évaluer votre classe Personnage, il n'est pas configuré pour faire quelque chose de particulier : il indique simplement le nom de la classe et l'endroit où elle est définie. Ci-dessous, il indique que la classe Personnage est définie dans le programme principal, 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

Puisque la Classe est en mémoire, nous pouvons maintenant l'utilser pour réaliser de véritables objets ou instances de cette Classe.

Instanciation et instances de la classe

La syntaxe pour créer un nouvel objet est simple : on utilise le nom de la classe et on rajoute simplement des parenthèses derrière ! On obtient alors une fonction un peu particulière qu'on nomme le Constructeur. Cette fonction est particulière car sa réponse est l'identifiant mémoire d'un nouvel objet qu'on vient de générer.

L'action de création d'un nouvel objet par le Constructeur se nomme l'instanciation. Si on stocke l'identifiant en mémoire, on pourra agir plus tard sur notre objet. On remarquera qu'ailleurs ci-dessous que les deux adresses mémoires sont différentes.

Instanciation : création d'un nouve l objet à partir du Constructeur d'une Classe.

Instance : synonyme d'objet si ce n'est qu'on dira souvent Instance de Personnage alors qu'on parlera souvent juste d'objet.

Ci-dessous, on crée deux objets différents issus du constructeur Personnage() et stockés dans alice et bob.

alice et bob sont alors nommés objet ou instance de la classe Personnage.

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

La variable alice (l'objet) est donc indépendante de Personnage (la Classe), même si alice a été créé en utilisant le moule de Personnage.

De la même façon, les objets alice et bob sont maintenant deux entités indépendantes.

Informations sur les objets

On peut connaître la Classe d'alice en utilisant la fonction native type(). De la même façon, on peut tester si alice est du bon type de deux façons : soit en testant type(), soit en utilisant la fonction native isinstance().

>>> type(alice) <class '__main__.Personnage'> >>> type(alice) == Personnage True >>> isinstance(alice, Personnage) True >>> isinstance(alice, list) False

Comportement visible vue de l'extérieur

Contrairement aux objets créés à partir des types natifs (int, float, list...), les objets créés à partir d'une Classe déclarée manuellement n'affiche pas leur contenu automatiquement par défaut. 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 mémoire. C'est simplement que 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 - Rajouter des attributs à la volée

Pour l'instant, la classe Personnage est un moule vide. Elle se sert à rien et ne contient rien de particulier.

Commençons par voir comment stocker des variables rattachées à notre objet. On nomme ces variables internes à l'objet des attributs.

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

Rajouter des attributs

Voyons comment rajouter des attributs (des variables) dans notre objet.

Avec Python, c'est possible. Regardons comment ça fonctionne :

1 2 3 4 5 6 7 8 9 10 11 12 13 14
# Déclaration des classes class Personnage: "Ceci est une classe permettant de créer un personnage dans mon super RPG" # Instructions du programme principal si on lance directement le fichier if __name__ == "__main__": bob = Personnage() bob.nom = "Skywalker" bob.prenom = "Bob" bob.age = 25

Que fait ce programme ? Il crée un objet bob (ligne 11).

Lignes 12-13-14 : on rajoute des attributs en utilisant la syntaxe proche de la syntaxe d'activation des méthodes : on place le nom de l'objet, on rajoute un point et on rajoute le nom de l'attribut.

C'est comme si on avait rajouté trois variables à l'intérieur de l'objet.

Récupérer les valeurs des attributs

Si on lance ce programme, on pourra alors visualiser le contenu dans la console.

>>> bob.nom 'Skywalker' >>> bob.prenom 'Bob'

Encore une fois, la syntaxe est simple : objet.attribut

Modifier les valeurs des attributs

Par définition, les objets sont fortement muables. On peut donc modifier leurs attributs avec la syntaxe intuitive : objet.attribut = nouvelle_valeur

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

Un objet possède un identifiant-mémoire. A cette "adresse" se trouve une sorte de table listant les attributs disponibles et l'identifiant-mémoire qui permet d'en connaître le contenu. Nous n'allons pas étudier l'implémentation réelle des objets dans Python, mais vous pouvez travailler avec cette représentation :

structure d'un objet

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.

structure d'un objet

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-objet elle-même : les objets sont muables.

01° Mettre le code suivant en mémoire. Utiliser ensuite les instructions indiquées dans la console. Il permet de déclarer une classe nommée Personnage. Et à l'aide de cette classe, on parvient à créer plusieurs objets basés sur la même structure.

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

Questions

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

...CORRECTION...

  • Quelle est la particularité du nom de la classe (qui devrait vous sauter aux yeux) ?
    • Le nom commence par une majuscule. Une majuscule ! Personnage et pas personnage.
  • Comment se nomment les deux variables qui contiennent des objets basés sur cette classe ?
    • On crée visiblement deux variables par affectation : alice et bob
  • Quelles sont les deux lignes qui permettent de créer des objets basés sur la classe Personnage ?
    • On crée des objets basés sur la classe Personnage en utilisant une fonction qui porte le nom de la classe utilisée (avec une majuscule aussi du coup). Par contre, s'agissant d'une fonction, on retrouve des paranthè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 identifiants-mémoires qui désignent bien deux zones différentes.
    • >>> alice <__main__.Personnage object at 0x7f25459ff9b0> >>> bob <__main__.Personnage object at 0x7f25459ff898>

✎ 02° Rajouter un attribut classe (contenant par exemple "Guerrier" ou "Ninja" et un attribut niveau.

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

  1. Afficher le niveau du personnage
  2. Afficher l'identifiant-mémoire du personnage
  3. Incrementer de 1 niveau du personnage
  4. Afficher le nouveau niveau du personnage
  5. Afficher l'identifiant-mémoire du personnage

✎ 04° Un objet est-il mutable ? On rappelle que cela veut dire qu'on peut modifier le contenu de la structure sans modifier l'adresse de l'objet lui-même. Proposer une suite d'instructions et de demandes dans la console Python pour justifier votre réponse.

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

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

...CORRECTION...

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

4 - Précisions : les étapes du Constructeur

En peu de théorie : comment se déroule l'instanciation ?

4 - Les 3 étapes lors de l'instanciation

Lorsqu'on crée un objet à partir d'une Classe, on dit qu'on réalise une instanciation.

Pour créer un objet (ou instance), il faut faire appel à un Constructeur. Exemple :

>>> alice = Personnage()

Ce Constructeur va alors créer la structure de données qui permettra de contenir les attributs et les méthodes de la Classe.

  1. D'abord, on commence par réserver une nouvelle place mémoire et générer l'espace des noms correspondant en se basant sur le moule : la Classe. En Python, c'est fait automatiquement. Après cette étape, l'objet est créé et possède un identifiant personnel.
  2. 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.
  3. Pour l'instant, notre Classe ne contient pas de méthode __init__(). Lors de cette phase, on ne fait donc... rien.

    1 2
    class Personnage: "Ceci est une classe permettant de créer un personnage dans mon super RPG"
  4. Finalement, le Constructeur peut renvoyer l'identifiant-mémoire de l'objet (comme avec return). La variable de réception contient donc la référence de cet objet et on pourra agir dessus par la suite. Si vous ne mémoriser pas la réponse, l'objet est créé mais vous ne pourrez plus interagir facilement avec lui.

Si on résume :

On commence par lancer le Constructeur.

>>> alice = Personnage()

Le Constructeur renvoie l'identifiant de l'objet lorsqu'on arrive à l'étape 3 finale (ici 0x7f3ca24372b0).

>>> alice = 0x7f3ca24372b0

On affecte cette adresse à la variable alice.

>>> alice = 0x7f3ca24372b0

A partir de là, notre nouvelle objet est juste un objet vide. Mais, on peut créer puis modifier les attributs stockés dans notre objet en tapant par exemple :

>>> alice.nom = "skywalker"

Si vous voulez juste lire le contenu d'un attribut :

>>> alice.nom 'skywalker'

5 - Méthode __init__

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

Le problème du rajout d'attributs à la volée est double :

  1. on peut finir par écraser le contenu d'un attribut qui existe déjà en voulant créer un attribut qui porte le même nom qu'un autre attribut.
  2. il est difficile de se souvenir du nom des attributs et dans ce cas, il faut aller chercher l'endroit où il a été généré.

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.

Si vous placez toutes les créations d'attributs dans cette méthode, il devient très facile de retrouver les noms d'attributs déjà utilisés pour cet objet : il suffit d'aller voir le contenu de cette méthode.

5.1 - La méthode __init__()

Rôle de __init__()

Nous avons vu que Personnage() était un Constructeur.

On place dans la méthode spéciale __init__() tous les attributs qu'on veut créer, avec des indications permettant de déterminer la valeur initiale.

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

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

Puisqu'une méthode n'est rien d'autres qu'une fonction référencée directement dans l'objet, on utilise presque la même codification que les fonctions :

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.classe = 'aucune'

Lignes 5-6-7-8 : on comprend qu'on va créer 4 attributs nommés nom, prenom, niveau et classe.

De la même façon, on voit facilement quelles seront leur valeurs initiales.

Quelles conséquences lors de la création ?

>>> alice = Personnage() >>> alice.nom 'aucun' >>> alice.prenom 'aucun' >>> alice.niveau 0 >>> alice.classe 'aucune' >>> bob = Personnage() >>> bob.nom 'aucun' >>> bob.prenom 'aucun' >>> bob.niveau 0 >>> bob.classe 'aucune'

Cette fois, nous ne créons plus créés d'objets vides au départ. Ils ont bien 4 attributs et possédent les valeurs par défaut visibles sur les lignes 5 à 8.

A quoi ça sert de faire cela ?

Nous l'avons déjà dit : en centralisant toutes les créations d'attributs dans __init__(), on centralise l'information sur les attributs disponibles, ou pas.

La méthode spéciale __init__() est donc nommée la méthode-initialisateur ou même 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.

5.2 - Le paramètre self
5.3 - Le paramètre self et la méthode __init__()

Vous avez vu que self contient la référence-mémoire de l'objet qui est en train d'activer la méthode.

Lors de l'appel de la méthode spéciale __init__(), ce paramètre va automatiquement contenir l'"adresse" de l'objet sur lequel nous sommes en train d'agir.

Imaginons qu'on lance ceci :

>>> a = Personnage()

Que se passe-t-il ?

  1. Le constructeur crée un nouvel objet basé sur la classe Personnage et le système fournit un identifiant-mémoire libre à notre nouvel objet, disons l'identifiant 42.
  2. Le constructeur lance de lui-même l'appel à la méthode spéciale __init__() et lui fournit automatiquement la valeur du paramètre self  il reçoit l'identifiant 42. C'est comme si vous aviez tapé __init__(self=42). Elle va donc agir sur les attributs de l'objet situé à la référence numéro 42.
  3. Finalement, le constructeur renvoie l'identifiant de l'objet (42) vers le programme. Il peut alors être stocké dans la variable a.

06° Mettre la classe ci-dessous en mémoire puis utiliser les commandes 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.classe = 'aucune'
>>> bob = Personnage() >>> bob.nom 'aucun' >>> bob.prenom 'aucun' >>> bob.niveau 0 >>> bob.classe 'aucune' >>> bob.prenom = 'bob' >>> bob.prenom 'bob'

Question et réponse : en regardant les attributs déjà créés, serait-il intelligent de vouloir créer un attribut contenant le niveau de magie en le nommant niveau ? Ben non. Un attribut ayant ce nom existe déjà.

✎ 12° Que va être le nom du personnage suivant si on exécute cette instruction ?

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

13° Que contient le paramètre self de la méthode spéciale __init__() ? Utiliser la nouvelle classe ci-dessous pour le découvrir.

  1. Le nom du personnage
  2. La référence-mémoire du personnage
  3. L'age du capitaine
  4. La date de création de l'objet
1 2 3 4 5 6 7 8 9
class Personnage: "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self): self.nom = 'aucun' self.prenom = 'aucun' self.niveau = 0 self.classe = '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'identifiant-mémoire 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.

✎ 14° Lors de l'appel automatique à __init__() sur la première commande-console ci-dessous, que va contenir le paramètre self ?

  1. l'identifiant-mémoire de a
  2. l'identifiant-mémoire de b
  3. le contenu de a
  4. le contenu de b
>>> a = Personnage() >>> b = Personnage()

Même question avec la deuxième instruction visible dans la console de Python.

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.classe = 'aucune' print(f"Depuis la fonction __init__, le paramètre self contient {self}")

Bien entendu, il n'est pas très pratique d'avoir à initialiser une deuxième fois les attributs pour leur donner la bonne valeur.

Heureusement, le Constructeur fonctionne comme n'importe quelle autre fonction : il peut recevoir des arguments qu'il placera dans des paramètres.

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

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

On voit donc ici que :

  1. Paramètre 1 : self. On ne le fournit pas d'agument. Il sera automatiquement affecté avec l'identifiant-mémoire 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 classe du personnage.

16° 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'indentifiant-mémoire de l'objet qu'il vient de créer.

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 !

✎ 17° 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 zone-mémoire. La différence avec une variable normale ? On y trouve des instructions à exécuter.

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

Conclusion

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

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

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

Quelques exemple :

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

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

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

Voici un premier exemple qui rendra la classe plus lisible :

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

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

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

✎ 18° 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, classe="Jedi") >>> jim = Personnage(classe="Capitaine de l'Enterprise", nom="Kirk", niveau=6, prenom="James")

19° 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, classe="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.

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

✎ 20° Lancer la création d'un personnage sans donner sa classe de personnage. En regardant le code, deviner la valeur qui va être associée à la classe de personnage.

>>> jim = Personnage(prenom="Toto", nom="l'Asticot", niveau=10) >>> jim.classe '???'

7 - Bilan

Cette partie est destinée à être lue et commentée au tableau. Rien de nouveau.

a - Classe

Une Classe est une structure de données qu'il faut définir au préalable, une sorte de mode d'emploi permettant ensuite de créer des instances de cette classe, des objets "réels".

La grande différence avec les structures de données précédentes est qu'on peut y placer des données et des fonctions.

Vous pouvez reprendre l'analogie suivante :

  • La Recette de cuisine est la Classe
  • Les gateaux créés à partir de la Recette sont les instances de la recette, des objets.

On retiendra qu'on crée une Classe en utilisant le mot-clé class suivi du nom de la Classe qui commencera par une Majuscule par convention.

Les instructions contenues dans la Classe permettent de construire et préciser le contenu d'une instance.

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, cla): self.nom = nom self.prenom = pre self.niveau = niv self.classe = cla

b - Instanciation : création d'une instance, un objet

Une fois la Classe en mémoire, on peut créer des objets basés sur cette structure.

Pour cela, on a besoin d'utiliser un Constructeur. En Python. Comme toute fonction, elle peut nécessiter ou non l'envoi d'arguments.

Pour savoir si un Constructeur nécessite d'envoyer des arguments qu'on placera dans des paramètres, il faut aller voir la méthode spéciale __init__() (la méthode initialisateur ou constructeur) si elle existe.

Exemple

1 4
class Personnage: def __init__(self, nom, pre, niv, cla):

Lors de l'instanciation, le constructeur Personnage() a besoin de recevoir 4 arguments et pas 5 : le paramètre self va être automatiquement affecté : il contient l'identifiant-mémoire de l'objet sur lequel on applique la méthode.

Ce Constructeur Personnage() fait trois choses :

  1. il crée l'objet en lui donnant une référence-mémoire,
  2. il active la méthode-initialisateur __init__() avec un self qui contient automatiquement la nouvelle référence-mémoire puis
  3. il renvoie vers le programme la référence-mémoire du nouvel objet.
1
heros = Personnage("McClane", "John", 10, "Encaisse tout")

Pour savoir dans quels attributs ces paramètres vont être stockés, il faut aller voir le code contenu dans la méthode spéciale __init__().

4 5 6 7 8
def __init__(self, nom, pre, niv, cla): self.nom = nom self.prenom = pre self.niveau = niv self.classe = cla

On voit que "John" va être attribué au paramètre pre. Et ce paramètre va être attribué à l'attribut prenom à l'aide de l'instruction suivante :

5
self.prenom = pre

Il faut juste se souvenir que self contient l'adresse de l'objet. Cela veut dire donc :

  1. Va voir à l'adresse de cet objet(self)
  2. et trouve (.)
  3. l'adresse de l'attribut qui se nomme prenom
  4. et affecte lui =
  5. pre

c - Utilisation des attributs et des méthodes

La codification est toujours la même :

  • Récupération du contenu d'un attribut : objet.attribut. Vous pourrez alors faire ce que vous voulez du contenu : l'utiliser pour un calcul, le placer dans une variable...
  • Affectation d'un attribut : objet.attribut = nouvelle_valeur.
  • Utilisation d'une méthode : objet.methode(...)

Un exemple avec la méthode __init__() (puisqu'on en a pas défini d'autres pour l'instant)

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, cla): self.nom = nom self.prenom = pre self.niveau = niv self.classe = cla
>>> heros = Personnage("McClane", "John", 10, "Encaisse tout") >>> heros.prenom 'John' >>> heros.prenom = 'bob' >>> heros.prenom 'bob' >>> id(heros) 140441285082528

On peut utiliser directement la méthode sur notre instance si on veut changer les caractéristiques. Vous allez revoir que lorsqu'on fait appel à une méthode, on n'a pas besoin de fournir d'arguments pour le paramètre self. Ce paramètre est automatiquement rempli par la référence de l'objet lui-même lors de l'appel.

>>> heros.__init__("Darth", "Vador", 28, "Casque Noir") >>> heros.prenom 'Vador' >>> id(heros) 140441285082528

Nous sommes bien parvenus à modifier l'objet après création grace à l'appel de la méthode et l'identifiant de l'objet n'a pas été modifié. Seul son contenu a changé.

En réalité, lorsqu'on demande d'effectuer ceci :

heros.__init__("Darth", "Vador", 28, "Casque Noir")

l'interpréteur Python effectue en quelque sorte l'appel suivant :

Personnage.__init__(heros, "Darth", "Vador", 28, "Casque Noir").

d - Paramètre nommés et par défaut

Ces deux notions peuvent être utilisées également avec les fonctions. Mais elles sont très utiles lorsque on utilise un Constructeur puisqu'on peut

  1. Transmettre les paramètres dans l'ordre qu'on veut
  2. Ne pas transmettre certains paramètres. On utilise alors les valeurs par défaut qu'on trouve dans la méthode-initialisateur __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, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base"): self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe

On peut alors utiliser le constructeur sous différentes formes :

10 11 12
heros = Personnage("McClane", "John", 10, "Encaisse tout") heros2 = Personnage(prenom="Junior", nom="MacClane", classe="Enfant", niveau=4) heros3 = Personnage(nom="Darth", prenom="Vador")

Ligne 10 : on envoie des arguments pour remplir les paramètres dont le bon ordre.

Ligne 11 : on envoie des arguments pour remplir les paramètres dans l'ordre qu'on veut car on les nomme.

Ligne 12 : on envoie certains arguments pour certains paramètres nommés mais pas tous. Les paramètres nommés non présents vont alors être initialisés à la valeur par défaut.

On notera que les paramètres ont exactement ici le même nom que les attributs. C'est une pratique courante. Mais ce n'est pas de la magie. Ce n'est pas parce que la méthode __init__() reçoit un paramètre nommé qu'il y a automatiquement création d'un attribut qui porte le même nom. Il faut alors voir les lignes 5 à 8 pour comprendre ce qu'on stocke ou pas.

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'identifiant-mémoire 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.