Différence entre variable et attribut de données et entre fonction et méthode
Les classes permettent de réunir des données et des fonctionnalités. Ici, vous devez bien comprendre qu’une classe n’est finalement qu’un objet qui permet de créer d’autres objets de même type. Comme cet objet est différent des autres, on appelle cela une “classe” mais ce n’est que du vocabulaire.
Créer une nouvelle classe en Python revient à créer un nouveau type d’objet et de fait un nouveau type de données. On va ensuite pouvoir instance notre classe pour créer des objets qui vont partager les variables et fonctions de leur classe.
Pour désigner les variables et les fonctions que les objets héritent de leur classe, Python utilise les termes “attributs de données” et “méthodes”.
Les termes employés sont différents (et le sont dans tous les langages qui supportent l’orienté objet) car ils servent à désigner des éléments de langage différents.
L’idée principale à retenir ici est qu’un attribut de donnée ou une méthode est propre à un objet tandis qu’une variable ou une fonction est indépendante de tout objet. C’est la raison pour laquelle pour accéder à un attribut de données ou à une méthode on doit préciser le nom de l’objet qui souhaite y accéder avant.
Si on tente d’accéder à un attribut de donnée ou à une méthode définis dans une classe sans objet ou à partir d’un objet d’une autre classe, Python renverra une erreur puisqu’encore une fois les attributs de données de classe et les méthodes de classes sont propres et ne sont partagés que par les objets de la classe. C’est le principe “d’encapsulation” que nous allons expliquer en détail juste après.
En plus de cela, notez qu’un objet peut également définir ses propres attributs de données ou surcharger des attributs de données de classe.
Les classes et le principe d’encapsulation
L’un des grands intérêts des classes est qu’elles permettent l’encapsulation du code, c’est-à-dire le fait d’enfermer le code dans une “capsule”, dans un espace en dehors de l’espace global. Cela permet d’éviter d’utiliser des variables globales et de polluer l’espace global du script.
En effet, dans tous les langages de programmation, il est considéré comme une bonne pratique de limiter le recours aux variables globales car cela rend le code non flexible, non modulable, et dur à entretenir.
Pour comprendre cela, il faut penser au fait que la plupart des programmes aujourd’hui contiennent des dizaines de fichiers qui contiennent chacun des centaines de lignes de code et qui font appel à de nombreux modules externes, c’est-à-dire à des code préconçus fournis par d’autres personnes.
Dans ces conditions, on ne peut pas se permettre de déclarer ses variables ou fonction n’importe comment car les risques de conflits, c’est-à-dire les risques qu’un même nom de variable ou de fonction soit utilisé plusieurs fois pour définir plusieurs variables ou fonctions entre fichiers et modules sont grands.
Pour cette raison, un bon développeur fera tout pour compartimenter son code en créant des espaces de portée ou “espaces de noms” bien définis et dont les éléments ne pourront pas entrer en conflit avec les autres.
Les classes nous permettent de mettre en place cela puisque chaque objet créé à partir d’une classe sa posséder SES attributs de données et SES méthodes qui ne vont pas être accessibles depuis l’extérieur de l’objet et qui ne vont donc pas polluer l’espace global.
Initialiser des objets avec __init__()
Dans la leçon précédente, nous avons défini une première classe Utilisateur()
et avons créé deux objets pierre
et mathilde
à partir de cette classe comme cela :
Ici, lors de leur création, les deux objets pierre
et mathilde
disposent des mêmes attributs avec les mêmes valeurs telles que définies dans la classe.
Ce type de comportement est, en pratique, rarement voulu. Généralement, on voudra que les objets disposent déjà d’attributs avec des valeurs qui leur sont propres dès leur création.
Pour réaliser cela, nous allons modifier notre classe et lui ajouter une fonction spéciale appelée __init__()
(deux underscores avant et deux après le nom) qui permet “d’initialiser” ou de “construire” nos objets.
En fait, cette fonction __init__()
va être automatiquement exécutée dès qu’on va instancier la classe. Cette fonction va pouvoir recevoir des arguments qu’on va lui transmettre durant l’instanciation et qui vont nous permettre de définir des valeurs propres à chaque instance.
L’idée générale est la suivante : notre fonction __init__()
va être construite de telle sorte à ce que les arguments passés soient utilisés comme valeur d’initalisation pour les attributs d’une instance. On va passer les argument lors de l’instanciation, via Utilisateur()
dans notre cas et ces arguments vont être transmis à __init__()
.
Prenons immédiatement un exemple qu’on va expliquer ensuite pour bien comprendre comment cela se passe :
Notre classe Utilisateur()
est désormais composée d’une variable anciennete
et de deux fonctions __init()__
et getNom()
.
Observons de plus près notre fonction __init__()
. Comme vous pouvez le voir, celle-ci accepte trois paramètres en entrée qu’on a ici nommé self
, nom
et age
.
Il est maintenant temps de vous expliquer ce que signifie ce self
qui était déjà présent dans notre dernière définition de classe. Pour cela, il faut retourner à notre définition des méthodes.
Si vous vous rappelez bien, je vous ai dit au début de cette partie que quasiment tout en Python était avant tout un objet et qu’en particulier les fonctions étaient des objets de “type” fonction. Les fonctions d’une classe ne font pas exception : ce sont également avant tout des objets.
Ces objets fonctions de classes définissent les méthodes correspondantes de ses instances. Schématiquement, les fonctions des classes deviennent des méthodes pour les objets créés à partir de cette classe.
Ce que vous devez absolument comprendre ici est qu’une des particularités des méthodes est que l’objet qui l’appelle est passé comme premier argument de la fonction telle que définie dans la classe. Ainsi, lorsqu’on écrit pierre.getNom()
par exemple, l’objet pierre
est passé de manière implicite à getNom()
.
C’est la raison pour laquelle nos fonctions de classe possèdent toujours un paramètre de plus que d’arguments qui leur sont fournies lorsqu’elles sont appelées en tant que méthode : l’objet qui les appelle prendra la place de ce paramètre. Par convention, on appelle ce paramètre self
qui signifie “soi-même” pour bien comprendre que c’est l’objet lui même qui va être passé en argument.
Notez ici qu’en Python “self” ne signifie rien et qu’on pourrait tout aussi bien utiliser un autre nom, à la différence de nombreux autres langages orienté objet où self
est un mot clef réservé.
Revenons en maintenant à notre fonction __init__()
. Lorsqu’on instancie notre classe, c’est-à-dire lorsqu’on crée un nouvel objet à partir de cette classe, la fonction __init__()
est automatiquement appelée si elle est présente dans la définition de la classe.
Cette fonction va également recevoir l’objet qui est en train d’être créé en premier argument. Cet objet va donc remplacer le “self”. Ici, notre fonction __init__()
sert à effectuer deux affectations : self.user_name = nom
et self.user_age = age
.
Ces deux affectations signifient littéralement “crée un attribut de données user_name
qui sera propre à l’instance et affecte lui la valeur passée en argument nom
” et “crée un attribut de données user_age
qui sera propre à l’instance et affecte lui la valeur passée en argument age
”.
Lorsqu’on écrit pierre = Utilisateur("Pierre", 29)
, les deux arguments passés “Pierre” et “29” vont être transmis avec l’objet à __init__()
qui va les utiliser pour créer deux attributs de données user_name
et user_age
spécifique à l’objet pierre
créé.
Variables de classe et attributs de données d’un objet
De manière générale, il est considéré comme une bonne pratique de ne créer des variables de classe que pour définir des variables qui devraient avoir des valeurs constantes à travers les différents objet de la classe lors de leur création.
Dans l’exemple précédent, par exemple, on peut imaginer que notre classe Utilisateur
nous sert à créer un nouvel objet de type “Utilisateur” dès qu’une personne s’enregistre sur notre site.
Dans ce cas, l’ancienneté de l’utilisateur au moment de la création de l’objet, c’est-à-dire au moment de son inscription sera toujours égale à 0 et il fait sens de définir une variable de classe anciennete
.
Retenez également qu’on évitera de créer des variables de classe avec des données altérables comme des listes ou des dictionnaires sauf dans des cas très précis. En effet, une variable de classe “appartient” à tous les objets de la classe par défaut.
Si la variable contient des données altérables, alors n’importe quel objet va pouvoir modifier ces données. Or, si un objet modifie les données d’une telle variable de classe, le contenu de la variable sera modifié pour tous les objets de la classe.
Cela est dû au fait qu’une variable de classe est en fait “partagée” par tous les objets de la classe. On dit que chaque objet de la classe accède à la variable par référence.