python objets

Identification

Infoforall

36 - Création d'objets avec Python


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

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

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

Prérequis : savoir utiliser Python :o)

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

Evaluation ✎ : 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 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

Rappellons 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/3 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 publique

On retrouve donc ce que nous avions vu lors de l'activité sur les modules et sur les structures de données : le fonctionnement interne d'un système n'a pas forcément à voir avec son comportement visible.

2/3 Variable "conteneur" : variable et type construit

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 publique

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']
3/3 - 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'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 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'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)

  • Il existe des types simples faisant référence à un contenu unique
  • Il existe des types construits 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 !

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 - Cours : l'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 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 permettent de réaliser de vrais gateaux, tous un peu similaires mais potentiellement différents : tous les gateaux n'auront pas les mêmes caractéristiques (forme, couleur, taille, gout...).

Les instructions d'une Classe permettent 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

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 documentation-description d'une ligne.

Les instructions à réaliser lors de la création sont à décaler de 4 espaces. 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

2-2-2 - 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é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éz 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

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

2.3.1 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 active alors une fonction un peu particulière qu'on nomme le Constructeur. Cette fonction est particulière car sa réponse est l'adresse d'un nouvel objet qu'on vient de générer.

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

L'action de création d'un nouvel objet par le Constructeur se nomme l'instanciation. Si on stocke l'adresse fournie, on pourra agir plus tard sur notre objet. On remarquera ci-dessous que les deux adresses sont bien différentes sur les deux objets.

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.

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

L'objet alice est donc indépendante de la classe Personnage, même si alice a été créé en utilisant comme moule Personnage.

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

2.3.2 - Informations sur les objets

On peut connaître la Classe d'alice en utilisant la fonction native type().

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

Cette fois, on voit que alice est une instance de la classe Personnage.

D'ailleurs, 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) == Personnage True >>> isinstance(alice, Personnage) True >>> isinstance(alice, list) False

2.3.3 - 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'affichent 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 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 - Cours : 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

3.1.1 - 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
# 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 proche de la syntaxe d'activation des méthodes : objet.attribut = ....

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

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

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

Comment se déroule l'instanciation ?

3.3 - 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 de l'objet à celui qui a appellé le constructeur

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

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 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 determiner_immeuble() du projet rue)

4 - Cours : la méthode spéciale __init__

4.1 - La méthode __init__()

4.1.1 - Rôle de __init__()

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

Il 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 donc pour but de centraliser la création et l'initialisation des attributs.

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

Puisqu'une méthode n'est rien d'autre 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.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.1.3 - Quelles conséquences lors de la création ?

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

Cette fois, les objets ne sont plus vides au départ. Il a bien 4 attributs et ils possédent les valeurs visibles sur les lignes 5 à 8.

4.1.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 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.2 - Le paramètre self

Ce paramètre est un peu particulier puisqu'on ne l'envoie pas.

Voici la création d'alice :

>>> alice = Personnage()

Cette fonction va alors généner un objet et une nouvelle adresse est créée : celle de l'objet.

Avant de renvoyer cette adresse, Personnage() va alors lancer automatiquement un appel à __init__() en lui envoyant cette adresse en argument.

Si vous observez la déclaration de la méthode __init__(), vous constaterez qu'elle possède justement 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'

A retenir (hyper-important pour comprendre la POO)

self est donc un paramètre qu'on ne fournit pas mais qui sera associé à l'adresse de l'objet en cours d'utilisation.

Toujous placé en premier dans le prototype d'une méthode, self contiendra l'adresse de l'objet qui a lancé l'appel.

4.3 - Transmission d'arguments lors de la constuction

4.3.1 Principe

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.

Si on envoie des arguments au constructeur, il va les retransmettre intégralement à la méthode __init__().

Un exemple où notre méthode __init__() peut recevoir 4 arguments et les stocker dans les 4 paramètres nom pre niv pro.

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 la déclaration de la méthode __init__().

1
p = Personnage("InBorderLand", "Alice", 20, "Héroïne")
. 4 .
class Personnage: ... 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 qui va servir à stocker la valeur reçue.

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 elles 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, tout 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 déclarer d'abord les paramètres normaux puis ensuite les valeurs avec valeur par défaut. N'oubliez pas que l'interpréteur remplit dans l'ordre de réception, sauf si on lui donne le nom des paramètres à remplir.

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 donc 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 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 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. Incrementer 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'agument. 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 connait 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 - Excercices paramètres nommés et par défaut

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

Voici un premier exemple qui rendra la classe plus lisible :

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

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

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

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

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

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

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

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

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

...CORRECTION...

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

Convention sur les paramètres

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

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

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

C'est comme ça.

Ici, on met un espace :

>>> bob = Personnage...

Ici, on n'en met pas :

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

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

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

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

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