IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Python, de zéro


précédentsommairesuivant

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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 »).

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 » (.).

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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…

 
Sélectionnez
1.
2.
3.
4.
5.
>>> p.prenom="Arthur"
>>> p.nom
'toto'
>>> p.prenom
'Arthur'

… ce qui est rarement une bonne chose !!!

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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).

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 __).

 
Sélectionnez
1.
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).

 
Sélectionnez
1.
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…

 
Sélectionnez
1.
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.
 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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…

 
Sélectionnez
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.
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()).

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 _).

 
Sélectionnez
1.
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é).

 
Sélectionnez
1.
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).


précédentsommairesuivant

Copyright © 2022 Svear (svear@free.fr) Permission est accordée de copier, distribuer ou modifier ce document selon les termes de la « Licence de Documentation Libre GNU » (GNU Free Documentation License), version 1.1 ou toute version ultérieure publiée par la Free Software Foundation.