II. Objet▲
Tous les programmes que l'on a écrits jusqu'à présent sont des programmes procéduraux, dont l'un des éléments constitutifs est une fonction. L'exécution de tels programmes consiste en l'exécution successive de leurs instructions, modifiant l'état de la mémoire (les variables). Dans ce chapitre, on va voir la programmation orientée objet, dont l'un des éléments constitutifs est un objet.
II-A. Objet et état▲
Jusqu'à présent, on a déjà pu voir et utiliser plusieurs types de données différents, partant de simples nombres et booléens, jusqu'à des types permettant de stocker des collections d'éléments (listes, chaines de caractères, tuples, intervalles, ensembles et dictionnaires).
II-A-1. Tuple nommé▲
On a également vu les tuples nommés qui permettent de rassembler plusieurs variables en leur associant à chacune un nom, il s'agit des champs du tuple nommé. Voici, par exemple, une définition d'un nouveau tuple nommé permettant de représenter un vecteur dans le plan :
from
collections import
namedtuple
Vector =
namedtuple
(
'Vector'
, ['x'
, 'y'
])
La seconde instruction définit un nouveau type de tuple nommé, dont le nom est Vector, constitué de deux champs nommés x et y. À partir de cette définition, on peut créer des tuples nommés représentant des vecteurs dans le plan :
u =
Vector
(
1
, -
1
)
v =
Vector
(
2
, 1
)
Le tuple nommé permet donc de stocker des données de manière organisée, avec des champs qui possèdent un nom. On peut, par exemple, afficher les coordonnées du vecteur en accédant aux champs. Afin d'avoir un joli affichage, on va construire une chaine de caractères par concaténation, en n'oubliant pas la conversion en str :
Si on veut également afficher l'autre vecteur, on pourrait simplement dupliquer cette instruction, en remplaçant les u par des v. Néanmoins, comme on l'a déjà vu, ce n'est pas une bonne pratique. Lorsqu'on doit répéter du code qui varie légèrement, une bonne pratique consiste à définir une fonction. Voici une fonction vector2string qui renvoie une chaine de caractères à partir d'un vecteur :
Maintenant que la fonction est définie, on va pouvoir l'appeler autant de fois que l'on souhaite, pour afficher joliment nos vecteurs. Voici deux appels à cette fonction, avec le résultat produit lors de l'exécution :
print
(
vector2string
(
u))
print
(
vector2string
(
v))
(
1
, -
1
)
(
2
, 1
)
Dans l'exemple que l'on vient de voir, on a donc associé des données sous forme d'un tuple nommé, et on a défini des fonctions permettant d'effectuer des opérations sur ces dernières. C'est exactement cette association qui est à la base de la programmation orientée objet, concept sur lequel le langage Python a été construit.
II-A-1-a. Objet▲
Qu'est-ce qu'un objet ? Un objet est une entité qui possède des caractéristiques et qu'on va pouvoir manipuler. On peut faire un parallèle immédiat avec les objets du monde réel.
Prenons, par exemple, l'objet GSM (pour être précis, on devrait dire téléphone mobile puisque GSM désigne une norme, mais ce dernier terme est abusivement utilisé pour désigner le téléphone), qui possède une certaine marque, un modèle, un numéro de série, une couleur, un niveau de charge de la batterie, un niveau de luminosité de l'écran, etc. Ces différentes caractéristiques sont appelées attributs de l'objet. Pour un objet donné, chaque attribut possède une valeur, dont l'ensemble définit l'état de l'objet. La figure 1 montre deux objets de type GSM, avec leurs cinq attributs et valeurs associées.
Alors que les attributs sont communs aux deux GSM, les valeurs de ces derniers peuvent être différentes. Mais comme on peut le constater sur cet exemple, plusieurs attributs peuvent également avoir les mêmes valeurs (la marque et le modèle, dans cet exemple).
En plus des attributs, un objet offre toute une série de fonctionnalités. On peut, par exemple, effectuer et recevoir un appel, envoyer un SMS, écouter un fichier MP3, consulter le niveau de charge de la batterie, prendre une photo, jouer à un jeu, etc. Il s'agit d'opérations qui vont être appliquées à un objet.
Pour résumer, un objet est une entité qui possède des attributs et propose des fonctionnalités. Chaque objet est unique et possède son état propre, qui est défini par la valeur de ses attributs.
II-A-1-b. Objet en Python▲
Python est, entre autres, un langage de programmation orienté objet. On manipule dès lors déjà des objets depuis le début, sans vraiment le savoir. Par exemple, les listes, les chaines de caractères et les ensembles sont des objets. Lorsqu'on crée un objet, les trois éléments illustrés sur la figure 2 existent :
- l'objet, c'est-à-dire les valeurs de ses attributs, est en mémoire ;
- une variable, du même type que l'objet, est déclarée et initialisée ;
- une référence vers l'objet est stockée dans la variable.
L'instruction suivante crée un nouvel ensemble contenant six nombres entiers représentant les valeurs des faces d'un dé :
dice =
{1
, 2
, 3
, 4
, 5
, 6
}
Un ensemble est un objet de type set, qui stocke les valeurs des six faces du dé. Une référence vers cet objet est stockée dans la variable dice, dont le type est le même que celui de l'objet, à savoir set.
En réalité, tout est objet en Python. D'autres types d'objets qu'on a déjà vus sont donc les nombres entiers et flottants et les booléens.
II-B. Construction et utilisation▲
Pour pouvoir utiliser un objet dans un programme, il faut avant tout le créer, c'est-à-dire préparer de la place en mémoire pour stocker la valeur de ses attributs, et initialiser ces valeurs. Une fois cela fait, on va pouvoir utiliser l'objet, c'est-à-dire accéder à ses attributs (les lire et les modifier) et exploiter les fonctionnalités offertes par l'objet. L'utilisation d'un objet se fait grâce à la variable stockant une référence vers celui-ci.
Dans cette section, on va utiliser des objets prédéfinis de type time. Ces derniers permettent de représenter un instant temporel identifié par des heures, minutes et secondes. Pour pouvoir utiliser des objets time, il faut avant tout importer le module datetime.
II-B-1. Constructeur▲
La création d'un nouvel objet passe par l'appel d'un constructeur. Il s'agit d'une fonction particulière dont le nom est le même que celui du type de l'objet à créer.
On a déjà vu plusieurs fois l'utilisation d'un constructeur, notamment lorsqu'on voulait créer des copies de structures de données avec les fonctions list, set et dict, par exemple, ou lorsqu'on effectuait des conversions de données avec les fonctions int, float et str, par exemple.
L'exemple suivant crée deux objets de type time :
2.
3.
4.
from
datetime import
time
start =
time
(
14
, 45
, 21
)
end =
time
(
16
, 15
, 56
)
Pour créer un nouvel objet time, il faut donc fournir trois paramètres correspondants aux heures, minutes et secondes de l'objet à créer. Les deux objets créés, représentés sur la figure 3, représentent respectivement les instants 14:45:21 et 16:15:56.
Une fois un objet créé, on va pouvoir l'utiliser, et en particulier accéder à ses attributs. Pour cela, on passe par la variable contenant la référence vers l'objet et on utilise l'opérateur d'accès (.).
Un objet de type time possède plusieurs attributs dont hour, minute et second. L'exemple suivant calcule l'équivalent en secondes d'un instant temporel, afin de faire la différence entre deux instants. Pour cela, il suffit de multiplier les heures par kitxmlcodeinlinelatexdvp3600finkitxmlcodeinlinelatexdvp, les minutes par kitxmlcodeinlinelatexdvp60finkitxmlcodeinlinelatexdvp et de sommer le tout, pour les deux objets :
2.
3.
4.
5.
6.
7.
8.
9.
10.
from
datetime import
time
start =
time
(
14
, 45
, 21
)
end =
time
(
16
, 15
, 56
)
startsec =
3600
*
start.hour +
60
*
start.minute +
start.second
endsec =
3600
*
end.hour +
60
*
end.minute +
end.second
diff =
endsec -
startsec
print
(
"La différence est de"
, diff, "secondes."
)
La différence est de 5435
secondes.
L'accès aux attributs peut être fait en lecture et/ou en écriture. Par exemple, pour les objets de type time, les trois attributs qu'on vient de voir ne sont accessibles qu'en lecture. Si on tente de modifier la valeur d'un de ces attributs, une erreur se produit lors de l'exécution :
2.
3.
4.
from
datetime import
time
start =
time
(
14
, 45
, 21
)
start.minute =
15
Comme on le constate ci-dessous, l'attribut minute des objets de type time n'est pas modifiable :
2.
3.
4.
Traceback (
most recent call last):
File "program.py"
, line 4
, in
<
module>
start.minute =
15
AttributeError
: attribute 'minute'
of 'datetime.time'
objects is
not
writable
II-B-1-a. Paramètre de type objet▲
Si on revient sur l'exemple précédent, qui permet de calculer la différence en secondes entre deux instants, on se rend compte qu'il y a de la duplication de code.
Ce n'est pas une bonne pratique, comme on l'a déjà précédemment vu, et on va donc améliorer ce code. Une solution consiste à définir une fonction qui reçoit un objet de type time en paramètre et qui renvoie l'équivalent en secondes de cet instant :
def
toseconds
(
t):
return
3600
*
t.hour +
60
*
t.minute +
t.second
Le paramètre t reçu en paramètre est donc un objet de type time, ce qui permet d'accéder à ses attributs dans le corps de la fonction. Sur base de cette fonction, on peut simplifier le calcul de la différence entre deux instants. Le précédent exemple devient :
2.
3.
4.
5.
6.
7.
from
datetime import
time
start =
time
(
14
, 45
, 21
)
end =
time
(
16
, 15
, 56
)
diff =
toseconds
(
end) -
toseconds
(
start)
print
(
"La différence est de"
, diff, "secondes."
)
II-B-1-b. Valeur de retour de type objet▲
En plus de pouvoir recevoir un paramètre de type objet, une fonction peut également renvoyer un objet, ou plus précisément une référence vers un objet. Définissons une fonction after qui reçoit un premier paramètre de type time et un second de type int. Le premier représente un instant dans le temps et le second une durée en minutes. Le but de la fonction est de calculer l'instant qu'il sera une fois la durée écoulée (en supposant que l'on ne dépasse pas 23:59:59).
Pour résoudre ce problème, on va utiliser les opérateurs de division entière et de reste de la division entière. Comme vous le constatez ci-dessous, la fonction renvoie bel et un bien un nouvel objet :
2.
3.
4.
def
after
(
start, duration):
minute =
start.minute +
(
duration %
60
)
hour =
start.hour +
(
duration //
60
) +
(
minute //
60
)
return
time
(
hour, minute %
60
, start.second)
Étant donné duration minutes, duration //
60
représente le nombre d'heures et duration %
60
le nombre de minutes correspondants. Par exemple, un duration de kitxmlcodeinlinelatexdvp90finkitxmlcodeinlinelatexdvp correspond à une heure et trente minutes. Pour calculer l'instant final, il suffit d'ajouter ces deux quantités aux heures et minutes initiales, et si le nombre de minutes dépasse kitxmlcodeinlinelatexdvp60finkitxmlcodeinlinelatexdvp, il faut augmenter les heures d'une unité et diminuer les minutes de kitxmlcodeinlinelatexdvp60finkitxmlcodeinlinelatexdvp unités.
Une fois la fonction définie, on peut l'utiliser pour construire des objets de type time, comme on peut le constater sur base de l'exemple suivant :
2.
3.
4.
5.
start =
time
(
14
, 45
, 21
)
end =
after
(
start, 90
)
print
(
'Après 90 minutes, il sera'
, end)
print
(
type(
end))
On voit bien sur le résultat de l'exécution que la valeur renvoyée par l'appel de la fonction after est un objet de type time :
Après 90
minutes, il sera 16
:15
:21
II-C. Méthode▲
Une fois un objet construit, on vient de voir qu'on peut accéder à ses attributs, en lecture et/ou et écriture. On peut également utiliser les fonctionnalités qu'il offre. Ces dernières sont représentées par des fonctions, appelées méthodes, qu'il sera possible d'appeler, une fois l'objet créé, sur ce dernier.
II-C-1. Appel de méthode▲
Une fonction associée à un objet est appelée méthode. Pour appeler une méthode, on utilise l'opérateur d'appel (.) sur la variable contenant une référence vers l'objet dont on veut utiliser une fonctionnalité.
Pour illustrer cela, regardons du côté des objets de type TextCalendar, qui représentent des calendriers. Pour créer un objet TextCalendar, il faut importer le module calendar, puis on peut directement créer un nouvel objet comme suit, le constructeur n'admettant aucun paramètre :
2.
3.
4.
from
calendar import
TextCalendar
cal =
TextCalendar
(
)
print
(
type(
cal))
<
class
'calendar.TextCalendar'
>
Les objets de type TextCalendar offrent une méthode prmonth qui permet d'afficher le calendrier de n'importe quel mois de n'importe quelle année. Pour cela, il faut fournir deux paramètres lors de l'appel à la méthode, respectivement l'année et le mois désiré. Par exemple, si on veut afficher le calendrier d'octobre 2015, il suffit d'écrire :
cal.prmonth
(
2015
, 10
)
Dans cette instruction, on a donc appelé la méthode prmonth sur l'objet qui est référencé par la variable cal, en lui fournissant deux paramètres, à savoir les nombres entiers kitxmlcodeinlinelatexdvp2015finkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvp10finkitxmlcodeinlinelatexdvp. Cet appel de méthode a pour résultat d'afficher le calendrier demandé à l'écran :
2.
3.
4.
5.
6.
7.
October 2015
Mo Tu We Th Fr Sa Su
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
La méthode appelée affiche donc directement le calendrier demandé. Il s'agit d'une méthode sans valeur de retour. Tout comme pour les fonctions (une méthode étant simplement une fonction associée à un objet), une méthode peut être avec ou sans valeur de retour.
Au lieu d'afficher le calendrier directement, on pourrait vouloir le récupérer dans une variable. Pour cela, les objets de type TextCalendar proposent une méthode formatmonth qui fonctionne exactement de la même manière que la méthode prmonth, sauf qu'elle renvoie le calendrier sous forme d'une chaine de caractères au lieu de l'afficher :
result =
cal.formatmonth
(
2015
, 10
)
print
(
result)
II-C-1-a. Objet cible▲
Une méthode est donc toujours appelée sur un objet. Ce dernier, appelé objet cible, est celui sur lequel la méthode appelée pourra opérer. Déclarons, par exemple, deux objets de type set :
numbers =
{8
, 3
, 1
, -
2
, 0
}
letters =
{'A'
, 'P'
, 'Q'
}
Ces deux instructions créent deux objets en mémoire, ceux-ci étant référencés par les variables numbers et letters, comme le montre la figure 4. Le premier ensemble contient cinq éléments et le second trois.
Les objets de type set possèdent une méthode pop qui permet de récupérer un élément arbitraire dans l'ensemble. On peut évidemment appeler cette méthode sur chacun des deux objets, en utilisant numbers ou letters comme objet cible. La méthode pop agira à chaque fois sur l'objet cible, en en récupérant un élément de manière arbitraire, comme le montre l'exemple suivant, avec un résultat d'exécution possible :
print
(
numbers.pop
(
))
print
(
letters.pop
(
))
8
Q
II-D. Programmation orientée objet▲
L'objet est au cœur de la programmation orientée objet, par opposition à la programmation procédurale où un programme se construit sur base de fonctions. Ici, on commence par créer un objet, pour ensuite l'utiliser par le biais de ses méthodes.
II-D-1. Identité et état d'un objet▲
Un objet, une fois créé, possède un état constitué des valeurs de ses attributs. Juste après sa construction, l'objet est dit initialisé, se trouvant dans son état initial. De plus, un objet possède une identité propre, notamment caractérisée par la position où il se trouve en mémoire.
Alors que deux objets différents peuvent tout à fait avoir le même état, ils n'auront jamais la même identité. En Python, on peut utiliser la fonction prédéfinie id pour obtenir l'identité d'un objet. L'exemple ci-dessous crée deux objets différents de type set et affiche leur identité :
Les deux valeurs affichées après exécution sont bien différentes, témoignant des identités distinctes des deux objets créés :
4302577224
4329799752
Dans l'exemple suivant, on crée de nouveau deux objets différents, de type time cette fois-ci, mais qui représentent la même heure. Ces deux objets ont donc le même état, mais gardent des identités différentes :
2.
3.
4.
from
datetime import
time
t1 =
time
(
12
, 45
, 32
)
t2 =
time
(
12
, 45
, 32
)
Deux opérateurs permettent de comparer des objets entre eux. L'opérateur d'égalité (==) permet de tester si deux objets ont le même état. L'opérateur d'identité (is
) permet de tester si deux objets ont la même identité. On peut donc, par exemple, écrire :
print
(
t1 ==
t2)
print
(
t1 is
t2)
La première instruction affiche True
, car les deux objets possèdent exactement le même état, puisqu'ils représentent tous les deux l'instant 12:45:32. Par contre, la seconde instruction affiche False
, car les deux objets n'ont pas la même identité. Il y a en effet bel et bien deux objets qui ont été créés en mémoire, même s'ils ont le même état :
True
False
II-D-1-a. Alias▲
On peut stocker des références vers le même objet dans plusieurs variables différentes, c'est-à-dire créer des alias. On a déjà vu cette notion précédemment, mais revoyons un nouvel exemple :
s =
{9
, 12
, -
5
}
a =
s
Dans cet exemple, un objet est créé par la première instruction et une référence vers ce dernier est stockée dans la variable s. La deuxième instruction copie juste le contenu de la variable s dans la variable a, c'est-à-dire la référence vers l'objet comme le montre la figure 5. On dit que la variable a est un alias de la variable s.
Les deux variables permettent d'agir sur le même objet. Par exemple, si on utilise la variable a pour ajouter un élément dans l'ensemble, puis qu'on affiche le contenu de l'ensemble avec la variable s, on verra que le changement a bien eu lieu sur l'unique objet qui existe en mémoire, référencé par les deux variables :
a.add
(
'NEW'
)
print
(
s)
{'NEW'
, 9
, -
5
, 12
}
II-D-1-b. Différence tuple nommé/objet▲
Les objets ressemblent beaucoup aux tuples nommés, ces derniers étant en quelque sorte les précurseurs des objets. Tous les deux permettent de stocker des données, appelées champs pour les tuples nommés et attributs pour les objets, accessibles avec l'opérateur d'accès. Alors qu'un tuple nommé est toujours non modifiable, les attributs d'un objet peuvent être modifiés s'ils ne sont pas en lecture seule.
Si on veut effectuer des opérations sur un tuple nommé, il faut définir une fonction qui reçoit le tuple nommé en paramètre. Pour effectuer des opérations sur un objet, on l'utilise comme objet cible lors de l'appel d'une méthode. La figure 6 résume ces deux types d'appels.
Enfin, une dernière différence entre les tuples nommés et les objets est qu'avec ces derniers, l'accès aux attributs peut être restreint aux méthodes de l'objet. Ils sont en effet parfois simplement cachés, et dès lors accessibles ni en lecture ni en écriture. Pour un tuple nommé, ils sont par contre toujours accessibles en lecture.
II-D-1-c. Tout est objet▲
En Python, toutes les données sont de type objet, même les nombres entiers, par exemple. La figure 7 montre un objet de type int créé en mémoire et dont une référence est stockée dans la variable temperature. On l'obtient avec l'instruction suivante :
temperature =
19
Tout ce qu'on a vu sur les objets s'applique donc aussi sur les types de données qu'on a vus depuis le début du livre. Par exemple, l'instruction x =
temperature crée un alias.
Quelles sont les méthodes disponibles pour les objets de type int ? On peut le savoir à l'aide de la fonction prédéfinie dir, qui renvoie une liste comme le montre l'exemple suivant :
temperature =
19
print
(
dir(
temperature))
['__abs__'
, '__add__'
, '__and__'
, '__bool__'
, '__ceil__'
, '__class__'
, '__delattr__'
, '__dir__'
, '__divmod__'
, '__doc__'
, '__eq__'
, '__float__'
, '__floor__'
, '__floordiv__'
, '__format__'
, '__ge__'
, '__getattribute__'
, '__getnewargs__'
, '__gt__'
, '__hash__'
, '__index__'
, '__init__'
, '__int__'
, '__invert__'
, '__le__'
, '__lshift__'
, '__lt__'
, '__mod__'
, '__mul__'
, '__ne__'
, '__neg__'
, '__new__'
, '__or__'
, '__pos__'
, '__pow__'
, '__radd__'
, '__rand__'
, '__rdivmod__'
, '__reduce__'
, '__reduce_ex__'
, '__repr__'
, '__rfloordiv__'
, '__rlshift__'
, '__rmod__'
, '__rmul__'
, '__ror__'
, '__round__'
, '__rpow__'
, '__rrshift__'
, '__rshift__'
, '__rsub__'
, '__rtruediv__'
, '__rxor__'
, '__setattr__'
, '__sizeof__'
, '__str__'
, '__sub__'
, '__subclasshook__'
, '__truediv__'
, '__trunc__'
, '__xor__'
, 'bit_length'
, 'conjugate'
, 'denominator'
, 'from_bytes'
, 'imag'
, 'numerator'
, 'real'
, 'to_bytes'
]
On voit, par exemple, une méthode real qui permet d'obtenir la partie réelle du nombre et une méthode imag qui permet d'obtenir la partie imaginaire. Dans le cas d'un nombre entier, cette dernière renverra évidemment toujours kitxmlcodeinlinelatexdvp0finkitxmlcodeinlinelatexdvp.
Les méthodes dont les noms sont entourés de __ sont privées et ne devraient pas être utilisées directement, mais indirectement. Par exemple, la méthode __add__
correspond à l'opérateur d'addition (+). Les deux instructions suivantes font exactement la même opération, et affichent toutes les deux kitxmlcodeinlinelatexdvp30finkitxmlcodeinlinelatexdvp :
print
(
temperature +
11
)
print
(
temperature.__add__
(
11
))
Comme expliqué dans le chapitre suivant, Python permet de définir des notations simplifiées pour appeler des méthodes, remplaçant l'appel par l'utilisation d'un opérateur.