python objets

Identification

Infoforall

38 - Création d'objets avec Python


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

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

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

Prérequis : savoir utiliser Python :o)

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

Evaluation : 8 questions

  question 02

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

DM 🏠 : oui

Documents de cours : open document ou pdf

1 - Rappel pour percevoir ce qu'est un objet

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

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

RAPPEL : référence, adresse ou identifiant

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

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

Dans le cadre de Python, les contenus sont caractérisés par un identifiant, 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
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
  • L'espace mémoire contient des données rangées dans des zones identifiées par leurs adresses.
espace des noms et mémoire

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

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 qu'on voit via l'interface :

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

Ce qui se passe en interne : l'ordinateur a juste modifié vers où aller à partir de l'adresse 1485 : on 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 :

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'adresse de notre nouvel objet.

En lignes 4 et 5, on agit sur notre objet en utilisant son adresse : on va sur place et on cherche une "fonction" se nommant pencolor() et forward().

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

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 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
class Personnage: """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 :
  • >>> isinstance(alice, Personnage) True >>> isinstance(alice, list) False

    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 classes class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" bob = Personnage() bob.nom = "Skywalker" bob.prenom = "Bob" bob.age = 25

Que fait ce programme ? 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 :

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 : les objets sont muables.

Si on résume :

On commence par faire appel au Constructeur.

>>> alice = Personnage()

Le Constructeur renvoie l'adresse de l'objet.

>>> alice = 0x7f3ca24372b0

On affecte cette adresse à la variable alice.

>>> alice = 0x7f3ca24372b0

A partir de là, notre nouvel objet est 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'

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

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

  1. il faudrait lire toutes les lignes de code pour connaître tous les attributs qu'on a créé. Pas top avec 300000 lignes.
  2. 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
  1. Réservation d'une adresse (et donc d'un espace mémoire)
  2. Initialisation du contenu à cette adresse
  3. Renvoi de l'adresse du nouvel objet

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.

  1. Réservation d'une adresse
  2. 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.

  3. Initialisation des attributs
  4. 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
    class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG"""
  5. Renvoi de l'adresse
  6. 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
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 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.

>>> 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'
4.2.4 - A quoi ça sert de faire cela ?

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

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) return adresse

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

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

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. n'est jamais envoyé directement lors de l'appel
  3. 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
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

On peut alors lancer des appels au constructeur en utilisant par exemple :

>>> p = Personnage("InBorderLand", "Alice", 20, "Héroïne") >>> p.prenom 'Alice' >>> p.niveau 20

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
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
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
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 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
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'

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
    class Personnage: """Ceci est une classe permettant de créer un personnage dans mon super RPG""" def __init__(self, nom, prenom, niveau=1, profession="Humain de base"):
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")

5 - Exercices

Création d'attributs à la volée (pas bien)

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

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

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

Questions

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

...CORRECTION...

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

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

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

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

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

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

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

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

...CORRECTION...

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

Création des attributs via __init__

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

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

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

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

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

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

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

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

...CORRECTION...

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

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

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

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

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

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

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

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

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

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

On voit donc ici que :

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

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

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

...CORRECTION...

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

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

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

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

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

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

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

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

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

Conclusion

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

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

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

Quelques exemples :

  • Méthode permettant permettant au 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 - Exercices paramètres nommés et par défaut

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

Voici un premier exemple qui rendra la classe plus lisible :

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

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

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

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

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

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

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

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

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

...CORRECTION...

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

Convention sur les paramètres

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

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

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

C'est comme ça.

Ici, on met un espace :

>>> bob = Personnage...

Ici, on n'en met pas :

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

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

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

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

🏠 DM° Réaliser le Devoir Maison (voir le lien) en rédigeant correctement vos réponses.

DM à réaliser (20 questions)

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

8 -

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.