XIV. Les classes▲
XIV-1. Préambule▲
Les classes en Python permettent de représenter des objets au sens informatique du terme. Comme telles, elles possèdent donc des attributs (des variables qui les caractérisent) et des méthodes (des fonctions qui permettent de les manipuler).
Une classe se définit en Python par le mot‑clef class
suivi du nom que l’on désire lui donner. Sans oublier évidemment les deux‑points habituels terminant toute définition de bloc.
2.
3.
4.
5.
6.
>>>
class
toto:
... pass
# Objet "toto" (vide)
...
>>>
a=
toto
(
)
>>>
a
<
__main__
.toto instance at 0x7fc685a86290
>
Avec l'objet, on définit généralement un constructeur (méthode automatiquement appelée à la création de l'objet) et un destructeur (méthode automatiquement appelée à sa suppression). Le constructeur est utilisé généralement pour associer à l’objet ses caractéristiques initiales ; et le destructeur sert à libérer les ressources système éventuellement utilisées durant la vie de l'objet.
Le constructeur se définit par la méthode __init__
(
) ; et le destructeur par la méthode __del__
(
). Ces deux méthodes (tout comme d'ailleurs toutes les méthodes de l'objet) étant à la base des fonctions, elles doivent être précédées du mot-clef def
déjà vu ; et doivent impérativement définir au moins un argument ; argument représentant l’instance de l'objet lui‑même. En effet, lorsqu’une instance appelle une fonction méthode de l’objet, cette instance est récupérée par la méthode qui peut alors la manipuler. Par convention, cet argument est nommé self.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
>>>
class
personne:
... def
__init__
(
self, nom):
... print
(
"Création de l'objet
%s
avec un paramètre [
%s
]"
%
(
self, nom))
... def
__del__
(
self):
... print
(
"Suppression de l’objet
%s
"
%
self)
>>>
p=
personne
(
"xxx"
)
Création de l’objet <
__main__
.personne instance at 0x7fc685a86290
>
avec un paramètre [xxx]
>>>
p
<
__main__
.personne instance at 0x7fc685a86290
>
>>>
del
p
Suppression de l’objet <
__main__
.personne instance at 0x7fc685a86290
>
XIV-2. L’instanciation et les instances▲
À ce point, il convient de faire un bref rappel sur les termes dédiés à l’objet en informatique.
Une classe, c’est à la base un type spécifique défini par l’utilisateur (représentant alors un objet). Une fois l'objet (le type) défini, il est possible de créer des variables de ce type. En effet, de même qu’on peut créer des variables de type string, entier ou flottant, on peut aussi bien évidemment créer des variables du type de l’objet que l’on a défini.
Le mécanisme de création d’une variable d’un type « objet » se nomme « instanciation ».
Une fois la variable créée, elle est du type de l’objet en question. On nomme alors cette variable une « instance » (ou, terme plus complet, « instance de classe »).
2.
3.
4.
5.
>>>
class
toto: pass
# Le même objet "toto" (toujours vide)
...
>>>
var=
toto
(
) # Ici se fait l’instanciation
>>>
var # "var" instance de "toto"
<
__main__
.toto instance at 0x7fc685a86290
>
Une instance de classe, c’est simplement une variable du type de la classe en question.
2.
>>>
type(
var)
<
class
'instance'
>
XIV-3. Les attributs▲
Un attribut est une variable située dans l'objet et qui permet de caractériser une instance d'une autre variable. Deux variables par exemple, toutes deux instances d'un objet « cercle », pourront ne pas avoir les mêmes caractéristiques (rayon différent pour chacune). Pour arriver à faire cette différence, il est nécessaire que l’objet « cercle » possède un attribut « rayon » permettant de stocker en interne sa caractéristique qui le fait différer d’un autre.
Au sein de l'objet lui‑même, un attribut se définit avec la variable représentant l’instance de l’objet (donc self). Et le lien entre l’objet et l’attribut se fait avec l’opérateur « point » (.).
2.
3.
4.
5.
6.
7.
8.
9.
>>>
class
personne:
... def
__init__
(
self, nom):
... self.nom=
nom
... print
(
"Création de l'objet
%s
avec un paramètre [
%s
]"
%
(
self, nom))
...
>>>
p=
personne
(
"xxx"
)
Création de l’objet <
__main__
.personne instance at 0x7fc685a86290
>
avec un paramètre [xxx]
>>>
p
<
__main__
.personne instance at 0x7fc685a86290
>
À partir de là, l'attribut nom est récupérable et utilisable depuis toute variable instance de cet objet. Et même modifiable.
2.
3.
4.
5.
>>>
p.nom
'xxx'
>>>
p.nom=
"toto"
>>>
p.nom
'toto'
Contrairement à beaucoup de langages où les attributs doivent être impérativement déclarés à l’avance dans leurs classes ; Python autorise la création directe d’un nouvel attribut depuis l’instance elle‑même…
2.
3.
4.
5.
>>>
p.prenom=
"Arthur"
>>>
p.nom
'toto'
>>>
p.prenom
'Arthur'
… ce qui est rarement une bonne chose !!!
2.
3.
4.
5.
6.
7.
8.
9.
10.
>>>
p1=
personne
(
"LeRoi"
)
>>>
p1.prenom=
"Arthur"
>>>
p2=
personne
(
"Autre"
)
>>>
>>>
for
x in
(
p1, p2):
... print
(
x.nom, x.prenom)
LeRoi, Arthur
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
AttributeError
: 'personne'
object has no attribute 'prenom'
Il est donc préférable de créer tous les attributs d’un objet dans l’objet lui‑même. Après tout, ce n’est pas pour rien qu’un objet possède un constructeur.
XIV-4. Les méthodes▲
Une méthode est une fonction permettant d'effectuer un travail sur (ou en relation avec) l'objet. Il s'agit en fait d'une fonction définie dans l'objet. Attention donc à ne pas oublier le mot-clef def
et à ne pas oublier que cette fonction (méthode) reçoit toujours en premier paramètre l'instance de l'objet lui-même (par conséquent, ne pas oublier le self) ; ce qui permet donc à la méthode d’accéder à l’objet qui l’invoque et à ses attributs. Sans oublier là encore les deux‑points habituels.
2.
3.
4.
5.
6.
7.
8.
9.
10.
>>>
class
rectangle
(
):
... def
__init__
(
self, height; width):
... self.height=
height
... self.width=
width
... def
diagonale
(
self):
... return
(
self.height **
2
+
self.width **
2
) **
0.5
...
>>>
p=
rectangle
(
3
, 4
)
>>>
p.diagonale
(
)
5.0
En fait, cette récupération obligatoire de l’instance dans le premier paramètre de la méthode s’explique par le fait que l’instruction instance.methode
(
) n’est qu’un raccourci de l’écriture littérale classe.methode
(
instance).
2.
3.
>>>
p=
rectangle
(
3
, 4
)
>>>
rectangle.diagonale
(
p)
5.0
À noter que __init__
(
) et __del__
(
) vues précédemment, étant elles aussi des fonctions permettant d'effectuer un travail sur l’objet, sont donc elles aussi des méthodes.
XIV-5. La surcharge de méthodes▲
La surcharge de méthode (appelée aussi surdéfinition ou polymorphisme) est une possibilité offerte par certains langages de programmation de définir des méthodes de même nom, mais qui diffèrent par le nombre ou le type des paramètres effectifs (signature).
Python ne permet malheureusement pas la surcharge de méthodes au sein d’une même classe. La seule façon en Python de pouvoir coder plusieurs actions distinctes dans une méthode selon le nombre ou le type des paramètres passés à ladite méthode est de jouer avec les valeurs par défaut ou de vérifier le type en question dans la méthode qui elle, ne peut qu’être unique.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
>>>
class
nbr:
... def
__init__
(
self, p):
... self.p=
p
... def
div
(
self, q=
1
):
... if
type(
q) ==
int: return
self.p //
q
... if
type(
q) ==
float: return
self.p /
q
... if
type(
q) ==
str: return
q
... return
None
...
>>>
n=
nbr
(
5
)
>>>
n.div
(
)
5
>>>
n.div
(
2
)
2
>>>
n.div
(
2.0
)
2.5
>>>
n.div
(
"toto"
)
'toto'
>>>
n.div
(
dict(
))
None
XIV-6. Les attributs et méthodes privés▲
XIV-6-a. Privatisation par Python▲
Dans les exemples précédents, tout attribut créé dans l’objet devient élément accessible de l’objet et peut même être modifié depuis l'extérieur de l’objet. C'est ce qu'on nomme communément un « attribut public ».
Il est souvent recommandé en programmation objet de ne pas donner d’accès public aux attributs d’un objet (aucun contrôle sur les modifications par autrui) et de préférer des attributs non accessibles depuis l'extérieur (attributs « privés »).
On peut privatiser un attribut en faisant précéder son nom de deux underscores accolés __).
2.
3.
4.
5.
6.
7.
8.
9.
class
personne:
def
__init__
(
self, nom):
self.__nom=
nom
print
(
"Je crée l'objet
%s
avec un paramètre [
%s
] "
%
(
self, nom))
>>>
p=
personne
(
"xxx"
)
Je crée l’objet <
__main__
.personne instance at 0x7fc685a86290
>
avec un paramètre [xxx]
>>>
p
<
__main__
.personne instance at 0x7fc685a86290
>
L’attribut reste accessible depuis l’intérieur de l’objet (donc de toutes ses méthodes), mais sera caché à tout ce qui y est extérieur (y compris malheureusement pour celui qui en aurait légitimement besoin).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
class
personne:
def
__init__
(
self, nom):
self.__nom=
nom
def
affich
(
self):
print
(
"nom=
%s
"
%
self.__nom)
>>>
p=
personne
(
"xxx"
)
>>>
p
<
__main__
.personne instance at 0x7fc685a86290
>
>>>
p.affich
(
)
nom=
xxx
>>>
p.nom
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
AttributeError
: 'personne'
object has no attribute 'nom'
>>>
p.__nom
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
AttributeError
: 'personne'
object has no attribute '__nom'
La notion de privatisation peut aussi s’étendre aux méthodes, en faisant précéder elles aussi leur nom de deux underscores. Cela rend donc la méthode non visible depuis l’extérieur de l’objet (permet par exemple de créer des méthodes dédiées à certains traitements internes non nécessaires aux utilisateurs de l’objet). À noter que le constructeur __init__
(
) et le destructeur __del__
(
), qui, même si cela ressort mal à l’impression, ont bel et bien eux aussi deux underscores devant leur nom, sont donc eux aussi des méthodes privées.
La façon qu’a Python de cacher l’attribut ou méthode privé reste cependant très basique. Il transforme le nom de l’attribut en « underscore + classe + underscore + underscore + nom » (mécanisme appelé « name mangling » en anglais) ; le but étant de protéger l’attribut (ou la méthode) contre une erreur de programmation et non contre une malveillance volontaire (philosophie Python : les développeurs Python sont adultes et consentants).
Cela signifie que l’attribut (ou la méthode) reste accessible pour qui veut s’en donner la peine…
2.
3.
4.
5.
6.
>>>
p=
personne
(
"xxx"
)
>>>
p._personne__nom
'xxx'
>>>
p._personne__nom=
"toto"
>>>
p._personne__nom
'toto'
… mais introduit des contraintes de maintenance liées à l’évolutivité du code (si le nom de l’objet change ou dans le cas d’un travail collaboratif entre divers intervenants par exemple).
XIV-6-b. Accès aux attributs privés▲
En dehors de cette manière particulière (et peu recommandée) d’accéder aux éléments privés en utilisant leur nom obfusqué, le propriétaire de l'objet pourra de son côté, s’il le désire, offrir un accès spécifique aux attributs privés de son objet en créant des accesseurs (getter/setter/deleter).
Ce getter/setter/deleter se crée en deux étapes :
- écriture des fonctions dédiées aux différents accès (lecture=getter, écriture=setter, suppression=deleter) de l’attribut,
- encapsulation de ces fonctions dans l’instruction property
(
getter, setter, deleter) qu’on affecte alors à, ce qui sera vu depuis l’extérieur, comme un simple attribut manipulable.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
class
personne:
# Écriture des fonctions dédiées
def
__get_nom
(
self): return
self.__nom
def
__set_nom
(
self, nom): self.__nom=
nom
def
__del_nom
(
self): del
self.__nom
# Encapsulation de ces fonctions dans la fonction property()
nom=
property(
__get_nom, __set_nom, __del_nom)
# Constructeur
def
__init__
(
self, nom): self.__nom=
nom
L’attribut nom ressemble à un attribut ordinaire, mais correspond en réalité à un point d’entrée vers le getter, le setter et le deleter. Tout accès en lecture à cet attribut appellera en interne le getter spécifié ; tout accès en écriture (affectation) appellera le setter et toute demande de suppression appellera le deleter.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
>>>
p=
personne
(
"xxx"
)
>>>
p.nom
'xxx'
>>>
p.nom=
"toto"
>>>
p.nom
'toto'
>>>
del
p.nom
>>>
p.nom
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
File "<stdin>"
, line 3
, in
__get_nom
def
__get_nom (
self): return
self.__nom
AttributeError
: 'personne'
object has no attribute '_personne__nom'
>>>
La différence avec un attribut public ne se perçoit pas immédiatement (c'est d'ailleurs le but voulu avec la fonction property(
)). L’utilisateur continue à accéder aux attributs comme s’ils étaient publics.
Toutefois, cela permet au programmeur de l'objet de contrôler ses entrées/sorties d'attributs…
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.
class
personne:
def
__set_nom
(
self, nom):
if
nom ==
""
:
print
(
"Pas de nom vide !!!"
)
return
print
(
"ok
%s
"
%
nom)
self.__nom=
nom
nom=
property(
lambda
self: self.__nom, # Getter intégré dans la lambda
__set_nom, # Setter
None
, # Deleter (pas de deleter)
)
# Constructeur
def
__init__
(
self, nom): self.__nom=
nom
>>>
p=
personne
(
"xxx"
)
>>>
p.nom
'xxx'
>>>
p.nom=
""
Pas de nom vide !!!
>>>
p.nom
'xxx'
>>>
p.nom=
"toto"
ok toto
>>>
p.nom
'toto'
… ou de faire l’impasse sur un ou deux des trois éléments getter/setter/deleter si l’opération s’y rapportant n’est pas envisagée (l’élément qui n’y est pas étant alors remplacé par None
dans la fonction property(
)).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
class
personne:
# Getter seul (modification impossible, suppression impossible)
nom=
property(
lambda
self: self.__nom, None
, None
) # Les None sont facultatifs
# Constructeur
def
__init__
(
self, nom): self.__nom=
nom
>>>
p=
personne
(
"xxx"
)
>>>
p.nom
'xxx'
>>>
p.nom=
"toto"
AttributeError
: can’t set attribute
>>>
del
p.nom=
"toto"
AttributeError
: can’t delete attribute
Une autre syntaxe possible est d’associer chaque élément (getter/setter/deleter) individuellement.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
class
personne:
# Écriture du getter et association immédiate
def
__get_nom
(
self): return
self.__nom
nom=
property(
__get_nom)
# Écriture du setter et association immédiate
def
__set_nom
(
self, nom): self.__nom=
nom
nom=
nom.setter
(
__set_nom)
# Écriture du deleter et association immédiate
def
__del_nom
(
self): del
self.__nom
nom=
nom.deleter
(
__del_nom)
# Constructeur
def
__init__
(
self, nom): self.__nom=
nom
Dans les trois cas, l’association se fera toujours sur l’élément que l’on veut montrer à l’extérieur. Accessoirement, rien n’interdit d’utiliser un autre token n’ayant aucune ressemblance syntaxique avec l’attribut interne (autrement dit, ce n’est pas parce que l’attribut interne se nomme __nom que l’attribut externe doit pour autant se nommer nom). Rien n’interdit même d’utiliser trois tokens totalement distincts pour les trois éléments getter/setter/deleter.
On peut raccourcir cette surcouche property(
) en utilisant le mot-clef @property
avant d’écrire le getter. De là, on peut rajouter ensuite @token
.setter pour le setter et @token
.deleter pour le deleter.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
class
personne:
# Écriture du getter de façon immédiate
@property
def
nom
(
self): return
self.__nom
# Écriture du setter de façon immédiate
@nom
.setter
def
nom
(
self, nom): self.__nom=
nom
# Écriture du deleter de façon immédiate
@nom
.deleter
def
nom
(
self): del
self.__nom
# Constructeur
def
__init__
(
self, nom): self.__nom=
nom
Le @xxx
est ce qu'on nomme communément un « sucre syntaxique » (syntaxe allégée permettant de simplifier certains termes). On le reverra dans d’autres conditions.
À noter : sous Python 2, la fonction property(
) n’est accessible que depuis un objet de base Python nommé object.
Dans cette version, il convient alors de faire hériter (la notion « d’héritage » sera vue ultérieurement) son propre objet de cet objet primitif. Cela se fait en mettant l'objet dont on veut hériter (donc ici object) en paramètre de son propre objet.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
# Exemple Python 2
class
personne
(
object):
# Écriture des fonctions dédiées
def
__get_nom
(
self): return
self.__nom
def
__set_nom
(
self, nom): self.__nom=
nom
def
__del_nom
(
self): del
self.__nom
# Encapsulation de ces fonctions dans la fonction property()
nom=
property(
__get_nom, __set_nom, __del_nom)
# Constructeur
def
__init__
(
self, nom): self.__nom=
nom
Sous Python 3, l’objet de base object est devenu implicitement hérité ce qui évite d'avoir à le faire manuellement.
XIV-6-c. Privatisation par convention▲
Une autre façon de créer des attributs privés est possible en faisant précéder l'attribut d'un simple underscore _).
2.
3.
4.
5.
6.
7.
8.
9.
class
personne:
def
__init__
(
self, nom):
self._nom=
nom
print
(
"Je crée l'objet
%s
avec un paramètre [
%s
] "
%
(
self, nom))
>>>
p=
personne
(
"xxx"
)
Je crée l’objet <
__main__
.personne instance at 0x7fc685a86290
>
avec un paramètre [moi]
>>>
p
<
__main__
.personne instance at 0x7fc685a86290
>
En réalité, cela ne protège l'attribut que de façon « conventionnelle » et non physique. En effet, le simple underscore n’a aucune signification pour Python (un tel attribut est alors public comme tout autre). Il est cependant universellement reconnu par les programmeurs. Bref, l’attribut reste public, mais l'ensemble des programmeurs accepte conventionnellement de ne pas y toucher (ou d’y toucher en connaissant les risques intrinsèques que cela peut entrainer sur la portabilité et l’évolutivité).
2.
3.
4.
5.
6.
7.
8.
9.
10.
class
personne:
def
__init__
(
self, nom):
self._nom=
nom
>>>
p=
personne
(
"xxx"
)
>>>
p._nom
'xxx'
>>>
p._nom=
"toto"
>>>
p._nom
'toto'
Bien que la terminologie ne soit pas exacte (elle correspond à une définition différente dans la méthodologie officielle objet), on nomme ces attributs des « attributs semi‑privés » ou « protégés » (protected).