Découvrez ma formation 100% gratuite intulée « Python pour les Djangopreneurs pressés » et apprenez suffisament de Python pour démarrer Django dans la semaine.

Programmation orientée objet en Python : les bases essentielles pour débuter

Développeur placepython

Par Thierry Chappuis

Débuter la programmation orientée objet (POO), que ce soit avec le langage de programmation Python ou avec un autre langage, est souvent perçu comme une étape difficile dans l’apprentissage de la programmation. L’usage des classes renferme en effet des mécaniques variées qui peuvent nous déstabiliser lorsqu’on brûle les étapes

Et vous, vous souvenez-vous de votre premier contact avec la programmation orientée objet ? Pour ma part, je m’en souviens comme si c’était hier. J’utilisais alors C++ dans sa version standardisée en 1998. Il avait le don de rendre cet apprentissage de la POO syntaxiquement et conceptuellement complexe. J’avoue avoir mis un moment avant de trouver les clés et à en comprendre l’intérêt. Je raisonnais toutefois à partir de ce que je connaissais et des programmes jouets que j’avais eu l’occasion de réaliser jusqu’alors.

N’hésitez pas vous aussi à me faire part de votre premier contact avec la programmation orientée objet en commentaire.

La bonne nouvelle est que Python a été conçu pour éliminer une grande partie de la complexité syntaxique ressentie avec beaucoup de langages. Cela permet d’introduire chaque concept en douceur et d’avancer progressivement. Cela permet de se concentrer sur l’essentiel et d’éliminer un certain nombre de barrières.

Dans mes cours et mentorats, j’ai l’habitude d’introduire très tôt les notions d’objet et de classe, souvent même dès la première heure et demie d’apprentissage. L’enjeu est de démystifier ces notions et de démontrer que la programmation orientée objet ne doit pas nécessairement être une chose compliquée.

Comme première publication du blog, cet article a pour ambition de vous emmener sur ce chemin avec pour unique pré-requis les deux notions de variable et de fonction. Nous n’allons traiter que la surface de ce qu’offre la POO, mais une surface suffisamment large pour servir de base à de nombreux programmes.

Accrochez-vous, c’est parti !

Qu’est-ce que la programmation orientée objet ?

Tout d’abord essayons de répondre à une première question naturelle. Définissons ce qu’est la programmation orientée objet. Pour cela, faisons une analogie avec le monde réel. Imaginez qu’on veuille créer un programme qui reproduit le parcours d’un client au sein d’une application de boutique. C’est un cas d’usage très classique sur le web. Voilà ce que cela peut donner si je décris mon programme à l’aide d’une séquence d’actions:

boutique = rechercher_boutique(nom=”Le p’tit pythonista”)
client = creer_un_compte_utilisateur(boutique=boutique)
propositions = rechercher_produit(boutique=boutique)

panier, ajout_reussi = ajouter_au_panier(
    boutique=boutique, 
    client=client, 
    choix=propositions
)

if ajout_reussi:
    commande = passer_commande(
        boutique=boutique, 
        client=client, 
        panier=panier
    )
    envoyer_email_confirmation(
        boutique=boutique, 
        client=client, 
        commande=commande
    )
    afficher_resume_commande(
        boutique=boutique, 
        client=client, 
        commande=commande
    )
else:
    notifier_erreur(
        boutique=boutique, 
        client=client, 
        commande=commande
    )
...

Les données manipulées par notre programme exemple sont identifiées à l’aide des noms boutique, client, propositions, panier et commande. En Python, ces noms sont des variables et les valeurs qui leurs sont associées sont des structures de données plus ou moins complexes dont nous ne voyons pas la définition ici. Pour l’heure, supposons qu’il s’agit de dictionnaires ou de listes sans plus de précision.

Pour pouvoir agir sur ces données, les fonctions utilisées dans l’exemple les reçoivent en arguments. De plus, elles retournent parfois de nouvelles données en résultat, comme c’est le cas pour ajouter_au_panier ou passer_commande. Ces arguments et résultats sont les outils que nous offrent Python pour permettre aux fonctions de collaborer et de travailler sur des données communes.

Nous observons que lorsque le nombre de données à manipuler augmente, il devient nécessaire de passer un grand nombre d’arguments à chaque fonction pour lui permettre de travailler. Nous voyons également que certaines données sont utilisées en argument de toutes les fonctions, tandis que d’autres sont seulement envoyées à certaines d’entre elles.

L’exemple plus haut ne nous montre qu’une partie du programme, celle où notre client passe une commande. Ce que nous ne voyons pas, c’est la définition de la structure des données qui se trouvent dans boutique, client, proposition ou commande. Nous ne voyons pas non plus la définition des fonctions utilisées.

Comment structurer notre programme pour ranger les définitions de ces structures de données et de ces fonctions de telle manière à ne pas tout mélanger ? Comment grouper tout ce qui concerne la boutique ? Comment grouper tout ce qui concerne le client ? Comment assurer que mon programme reste lisible et extensible sur le long terme, même lorsque la quantité de données et de fonctions à gérer est multipliée par 5 ou 10 ? Comment puis-je éviter d’avoir à passer une dizaine d’arguments à chaque fois que j’appelle une fonction ?

C’est ces problèmes que propose de résoudre la programmation orientée objet

Dans le style de programmation orienté objet, on arrête d’envisager le programme comme une séquence de fonctions à exécuter dans un ordre déterminé. Au lieu de cela, on va se focaliser sur les données boutique, client, panier, commande. On a donc une inversion de perspective. Ce ne sont plus les actions qui sont au centre. Ce sont maintenant les données. Ces données vont maintenant travailler ensemble en échangeant des messages, comme illustré sur ce diagramme.

Diagramme de séquence uml illustrant les échanges de messages entre différents objets

Étudier ce type de diagramme n’est pas indispensable pour apprendre la programmation orientée objet et donner un cours sur les diagrammes de séquence n’est pas l’objectif de cet article. Il permet toutefois d’imaginer comment fonctionne notre nouveau programme. Ici, l’utilisateur de l’application (nommé l’acheteur) est représenté par la figurine. Les objets boutique, client, produit et panier sont représentés par les rectangles.

Le temps s’écoule du haut vers le bas. L’utilisateur et les objets de notre application interagissent et échangent des messages.

Les concepts et mécanismes que nous devons étudier sont par conséquent:

  1. Comment créer des objets ?
  2. Comment les faire interagir en échangeant des messages ?

En Python, tout est objet

Vous avez peut-être déjà entendu cette phrase qui dit qu’en Python, tout est objet. Cela signifie que, qu’on le veuille ou non, un programme Python fonctionne en manipulant des objets:

# L'âge de personne est un objet de type 
# entier ou int
personne_age = 33
# La constante pi est un objet de type 
# nombre à virgule flottante ou float
pi = 3.1415
# La flag est_valide est un objet de type 
# booléen ou bool 
est_valide = True
# Le message est un objet de type chaîne 
# de caractère ou str
message = "Rendez-vous ce soir à 18h pour le WePynaire"
# prenoms est une liste de chaîne de caractères 
# représentant ici des prénoms
prenoms = [
    "Paul", "Jean", "Philippe", "Antoine"
]

Il est possible d’envoyer des messages à certains de ces objets à l’aide de ce qu’on appelle leurs méthodes. Les termes messages ou méthodes peuvent être utilisés comme synonymes dans le contexte de la programmation orientée objet. Toutefois, Python utilise plutôt la terminologie de méthode.

# On demande à une liste de trier ses données 
# à l’aide du message sort
prenoms.sort() 
# On demande à une chaîne de caractères de transformer 
# le texte en minuscules à l’aide de la méthode lower()
message.lower() # retourne le même message en minuscule

Le langage propose bien sûr d’autres types d’objets que les entiers, les nombres à virgule flottante, les booléens, les chaînes de caractère ou les listes. 

Même une fonction (native ou personnalisée) ou une classe (intégrée ou personnalisée) sont des objets:

>>> type(print)
<class 'builtin_function_or_method'>

>>> def une_fonction():
...     print("Je suis la fonction une_fonction")
... 
>>> type(une_fonction)
<class 'function'>

>>> class UneClasse:
...     """Représente un type d'objet quelconque."""
... 
>>> type(UneClasse)
<class 'type'>

Une fonction est ainsi un objet de type builtin_function_or_method, une fonction personnalisée est un objet de type function, et une classe est un objet de type type.

Quand je vous dis que tout est objet en Python, cela signifie vraiment tout !

Une chose est certaine toutefois. Il est fort probable que les objets que manipulent vos programmes ne figurent pas parmi les types natifs offerts par le langage. En effet, pas de Personne, de Boutique, de Client, de Produit, de Panier, d’Email, de NavigateurWeb, de GestionnaireDeClient, etc.

La bonne nouvelle est que chacun de ces types d’objets peut être créé à partir de type natifs. Une Personne peut ainsi finalement être vue comme l’union de différentes données natives comme une chaîne de caractères prénom, une chaîne de caractère nom, une chaîne de caractère adresse, un datetime date_de_naissance, une chaîne de caractères email, etc.

Lorsqu’on programme dans un style orienté objet, notre mission est donc d’identifier les types d’objets à manipuler dans notre programme et de leur donner vie. C’est le sujet de la suite de cet article.

Commençons par discuter la relation en type d’objet et objet. Nous commencerons à discuter cette question à l’aide d’une représentation graphique, puis nous enchaînerons sur Python.

Se représenter les objets et les types d’objets graphiquement

Intéressons-nous dans un premier temps aux objets eux-mêmes. Nous allons utiliser un langage graphique pour représenter visuellement les objets et les types d’objets. Ce langage graphique est appelé UML. UML est un acronyme anglophone signifiant Unified Modeling Langage et a été introduit au milieu des années 90s pour modéliser et documenter les fonctionnalités et les rouages internes des programmes informatiques écrits à l’aide d’objets. Les différentes représentations graphiques introduites par UML seront utilisées pour nous permettre d’identifier les objets utiles à la résolution d’un problème, ainsi que les relations qui les lient.

Diagramme d'objets de type Personne

Sur la figure ci-dessus, nous découvrons qu’un objet (en orange) est représenté en UML à l’aide d’un rectangle et d’un nom souligné. Nous voyons également qu’une classe (famille d’objets) telle que Personne (en jaune) est représentée avec un rectangle et un nom non souligné. Les objets représentés sur ce diagramme, paul, jean, philippe et antoine, ont donc tous en commun d’être des personnes. En langage technique, on dit que paul, jean, philippe et antoine sont des objets de type Personne, ou des instances de la classe Personne. La classe est l’outil utilisé par le langage de programmation pour décrire ce qui caractérise et ce que peut faire un objet. Nous verrons comment définir une classe en Python plus loin dans ce didacticiel, mais pour l’heure, regardons différentes variantes de ce diagramme d’objets.

Diagramme d'objets uml représentant des personnes

Une représentation alternative de paul, jean, philippe et antoine sur un diagramme d’objets est illustrée sur le diagramme ci-dessus. Dans cette représentation, chaque objet est associé explicitement à la classe Personne à l’aide du symbole :Personne, ce qui permet éventuellement de ne pas représenter le rectangle de la classe Personne sur le diagramme.

Diagramme d'objets uml avec des objets anonymes

Il est également possible d’omettre la partie de l’identifiant donnant un nom à l’objet et ainsi de l’anonymiser. C’est ce que nous voyons sur le diagramme ci-dessus. Un peu comme dans la salle d’attente de notre médecin, nous voyons quatre instances de la classe Personne (quatre personnes), mais nous ignorons leurs noms.

Formes servant à fabriquer des biscuits de Noël

Un peu comme l’étoile métallique représentée sur la photo ci-dessus sert à fabriquer une multitude de biscuits de Noël avec une forme prédéterminée, la classe Personne décrite sur les diagrammes d’objets plus haut peut être vue comme une usine à fabriquer des personnes. 

Ainsi, en programmation orientée objet, on commence par fabriquer le moule, l’usine, avant de l’utiliser pour façonner les objets nécessaires au fonctionnement de notre programme. En d’autres termes, avant de donner vie à Paul, Jean, Philippe ou Antoine, nous devons définir auprès de l’interpréteur Python ce qu’est une Personne aux yeux de notre projet. 

Le langage de programmation n’a en effet aucune idée de ce qu’est une personne, ni de ce que peut faire une personne. 

Nous devons par conséquent consacrer une partie de notre activité de programmation à lui expliquer, non seulement comment créer des personnes comme dans notre exemple spécifique, mais de manière plus générale comment créer tous les objets dont il va avoir besoin pour mener à bien sa mission. C’est ce que nous allons voir par la pratique à la section suivante dans laquelle nous allons essayer de concrétiser les concepts de classe, d’instanciation et d’objet.

Définition d’une classe et création d’objets en Python

J’ai demandé une fois à un étudiant pourquoi il n’avait pas créé de classe dans un programme qu’il me présentait. Ce dernier m’avait répondu qu’il n’avait pas trouvé que cela méritait l’effort de créer une classe. Mon objectif ici est de démontrer que ce n’est pas nécessairement un effort.

Et vous, avez-vous déjà renoncé à utiliser une classe pour les mêmes raisons ? N’hésitez pas à le dire en commentaire.
Dans l’exemple ci-dessous, nous définissons une classe Personne ayant pour objectif de représenter des personnes au sein de notre logiciel.

class Personne:
   """Représente une personne au sein du programme."""

Nous pouvons maintenant faire de même avec les classes pour notre boutique en ligne:

class Boutique:
    """Représente une boutique de e-commerce permettant
    de rechercher des produits et faire des achats."""

class Client:
    """Représente un client ayant créer un compte au sein d’une
    Boutique."""

class Produit:
    """Représente un produit au catalogue de la boutique gérée par
    le logiciel."""

class Panier:
    """Représente un panier d’achat représentant une commande provisoire
    en attente de validation."""

class Commande:
    """Représente une commande validée et payée par le client."""

Voilà, l’effort est terminé ! Le mot clé class, un nom et une chaîne de documentation. 

Bien entendu, nous allons devoir apprendre à Python ce qu’est une Personne, une Boutique, un Client ou encore un Produit. Mais ceci doit également être fait lorsqu’on programme dans le style procédural vu en début de cet article. 

Ce que nous venons de voir ci-dessus est par conséquent l’unique surcoût généré par la nécessité de définir une classe d’objets en Python. Très modéré, non ?

Une fois notre classe Personne définie, nous pouvons lancer l’utiliser pour fabriquer autant d’objets que nécessaires. A l’aide de notre classe Personne, nous pouvons maintenant créer nos quatres héros Paul, Jean, Philippe et Antoine. Nous ne sommes toutefois pas limités dans le nombre d’objets. Notre classe peut aussi bien servir à créer un objet unique ou à remplir un stade de foot.

# Création de plusieurs objets de type Personne

paul = Personne()
jean = Personne()
philippe = Personne()
antoine = Personne()

Rien ne m’empêche non plus d’utiliser mon nouveau type Personne avec d’autres types natifs comme les listes ou les dictionnaires. Voici un exemple avec une liste:

# 4 personnes réunies au sein d’une liste
personnes = [
    Personne(),
    Personne(),
    Personne(),
    Personne()
]

Vous pouvez même le faire au sein d’une boucle ou d’une compréhension:

# 4 personnes réunies au sein d’une liste
personnes = [
    Personne() for _ in range(4)
]

Dans les morceaux de code ci-dessus, la classe Personne représente l’usine à objets qui nous permet de créer les personnes paul, jean, philippe et antoine, ou les personnes au sein de notre liste. 

Une fois cette classe définie au sein de notre programme Python, l’expression Personne() permet de créer un ou plusieurs objets de ce type Personne. 

Sur le plan de la terminologie, on parle indifféremment de créer un objet de type Personne, créer une instance de la classe Personne ou d’instancier la classe Personne. Les termes instance ou objet sont à considérer comme des synonymes. Vous pouvez ainsi associer la présence des parenthèses () au sein de cette expression à l’action créer un(e) ou instancier la classe

En généralisant, l’expression Boutique représente la classe Boutique et Boutique() signifie créer/instancier une boutique, Produit représente la classe Produit et Produit() signifie créer/instancier un produit, Commande représente la classe Commande et Commande() signifie créer/instancier une commande, ou encore Panier représente une classe Panier et Panier() signifie créer/instancier un panier. Ce processus de création d’un nouvel objet à partir d’une classe est appelé l’instanciation et termes objet et instance sont souvent utilisés comme des synonymes.

Dans le code ci-dessus, paul, jean, philippe et antoine ne sont pas directement les objets, ils sont des étiquettes donnant un nom aux différents objets créés, ce qu’on appelle en python des variables. Les variables et les objets sont les deux concepts les plus fondamentaux en python. En fait, ce sont les deux concepts qui résument tout Python.

De la même manière qu’une étiquette identifie l’objet valise dans l’image ci-dessous, une variable identifie un objet et lui donne un nom permettant de le manipuler. Comme une étiquette d’aéoroport peut identifier de la même manière un sac de sport, un sac de randonnée, un bagage encombrant ou ‘importe quel type d’objet à transporter en soute, une variable python peut référencer n’importe quel objet, indépendamment de son type.

Etiquette permettant d'identifier une valise à l'aéroport

Comme une étiquette pourrait être détachée d’une valise puis posée sur une autre (au-delà de l’aspect technique de pouvoir décoller et recoller l’étiquette), une variable peut référencer plusieurs objets et des objets éventuellement de type différent au cours de la vie d’un programme python. Le lien entre une variable et un objet est généralement réalisé par le symbole =, également appelé opérateur d’affectation. Il est même possible à plusieurs un objet en utilisant plusieurs variables pour le référencer. Le code ci-dessous fait pointer les variables guido et createur_de_python sur la même instance de la classe Personne:

# Ici, un seul objet est crée avec la classe Personne
# Les deux variables désignent le même objet
guido = Personne()
createur_de_python = guido

On pourrait résumer cette situation à l’aide de la représentation graphique suivante:

Illustration de la mémoire de l'interpréteur python: mémoire des noms et mémoire des objets

Comme on peut le voir, variables et objets sont liés, mais vivent séparément. Il sont lié ensemble par ce qu’on appelle une référence ou parfois un pointeur. Schématiquement, j’ai représenté ces références un fil par des flèches qui connectent la variable à objet. Le diagramme ci-dessus est juste une image mentale pour se préprésenter le code que nous venons de voir. Deux variables ont été créés, mais un unique objet.

On peut vérifier que les objets que nous venons de créer sont bien de la classe Personne en affichant leurs type à l’aide de la fonction type():

>>> print(f"Le type de l'objet guido est { type(guido) }")
Le type de l'objet guido est <class '__main__.Personne'>

>>> print(f"Le type de l'objet guido est { type(createur_de_python) }")
Le type de l'objet createur_de_python est <class '__main__.Personne'>

>>> print(
...     "guido et createur_de_python "
...     "sont la même personne: "
...     f"{ guido is createur_de_python }"
... )
guido et createur_de_python sont la même personne: True

On constate qu’en définissant simplement la classe Personne, il devient soudain possible de demander à Python de créer des personnes et d’associer ces objets à des noms.

Des objets vides

Les classes créées définies dans la section précédente étant complètement vides, les objets ainsi créés ne contiennent absolument aucune information !

Python reconnaît les types Personne, Boutique, Client, Produit, etc., mais il ne leur donne aucune signification. Les objets créés sont littéralement vidés de toute donnée permettant un fonctionnement spécifique. C’est un peu comme si on avait enlevé le moteur et toute l’électronique d’une voiture. 

On a la carrosserie, sans la fonctionnalité.

Les personnes ainsi créées n’ont ainsi pas d’âge, pas de numéro de téléphone, pas d’adresse email, pas de profession. Ce code suffit toutefois à donner vie à nos objets. Ils existent dans la mémoire vive de notre programme et nous pouvons les manipuler à notre guise.On peut vérifier l’absence d’information spécifique au sein de des objets créés ci-dessus à l’aide de la fonction vars() qui renvoie un dictionnaire contenant les attributs spécifiques de chaque objet. Dans le cas présent, vars() se contente de retourner des dictionnaires vides que nous reconnaissons avec l’expression {}.

>>> print(f"Les attributs de paul sont: { vars(paul) }" )
Les attributs de paul sont: {}
>>> print(
...     f"Les attributs des instances de Boutique sont: { Boutique() }")
... )
Les attributs des instances de Boutique sont: {}

type() et vars() font partie des fonctions natives fournies par Python pour explorer la nature profonde des objets que vous créez. Leur usage, démontré ci-dessus, est relativement intuitif et il est documenté avec l’ensemble des autres fonctions natives de Python dans la documentation officielle.On peut également faire un test explicite pour vérifier que nos objets sont bien de type Personne. Ci-dessous, nous vérifions avec une instruction conditionnele que l’objet paul est bien une instance de la classe Personne. Pour ce faire, nous utilisons la fonction isinstance(objet, type de l’objet) également documentée ici:

SI paul est une instance de la classe personne FAIRE:
    ...
FIN de la condition

Ce qui donne en Python:

if isinstance(paul, Personne):
    ...

Vocabulaire à retenir: être une instance de la classe Personne signifie que l’objet a été créé à l’aide de la classe Personne avec l’expression Personne(). L’exécution de la classe Personne à l’aide de parenthèses déclenche la construction du nouvel objet.

Analyse orientée objet d’un problème plus complet

L’objectif de cette section est d’analyser un problème dans lequel une école que nous connaissons toutes et tous souhaite gérer les mentors actifs sur différents parcours d’apprentissage, ainsi que les étudiants qu’ils accompagnent sur ces parcours. Chaque étudiant peut s’inscrire à un seul parcours et se voit attribuer par l’école un mentor pour l’accompagner dans la réalisation de ses projets. Chaque projet est également associé à une série de cours que l’étudiant peut suivre ou pas pour se former.

Imaginons qu’on désire développer une application pour simuler le fonctionnement de cette école. Relisons ce problème une nouvelle fois avec notre casquette d’analyste orienté objet:

L’objectif de cette section est d’analyser un problème dans lequel une école que nous connaissons toutes et tous souhaite gérer les mentors actifs sur différents parcours d’apprentissage, ainsi que les étudiants qu’ils accompagnent sur ces parcours. Chaque étudiant peut s’inscrire à un seul parcours et se voit attribuer par l’école un mentor pour l’accompagner dans la réalisation de ses projets. Chaque projet est également associé à une série de cours que l’étudiant peut suivre ou pas pour se former. On désire développer une application pour simuler le fonctionnement de cette école.

post-it sur une fenêtre modélisant le domaine fonctionnel du problème posé

La photo ci-dessus illustre les concepts importants du problème décrit plus haut collectés sur la fenêtre de mon jardin à l’aide de posts-it autocollants. L’usage de cette technique est très pratique, car elle permet facilement d’ajouter, d’enlever, de déplacer, de grouper. On a ainsi identifié les concepts d’école, de mentor, de parcours d’apprentissage, d’étudiant, de projet, de cours, d’application. Cette liste n’est pas exhaustive, mais elle constitue un excellent point de départ.

Avec nos connaissances actuelles, nous pouvons déjà expliquer à notre langage de programmation préféré quels sont les classes d’objets que nous allons devoir manipuler au sein de notre programme.

Dans le contexte très international du développement information, il est possible (et fortement recommandé) de renommer chacune des classes identifiées ci-dessus en anglais. Au vu du contexte didactique qui nous occupe ici, j’ai décidé de ne pas le faire. Voici donc notre code Python pour démarrer:

class Ecole:
    """Représente un établissement d’enseignement géré par l’app."""
 
class Mentor:
    """Représente un mentor engagé par l’école pour encadrer des
    étudiants."""
 
class Parcours:
    """Représente un parcours de formation proposé par l’école."""
 
class Etudiant:
    """Représente un étudiant qui s’engage sur un parcours de formation."""
 
class Projet:
    """Représente les projets proposés sur les parcours."""
 
class Cours:
    """Représente les cours à suivre pour aider à la réalisation des
    projets."""
 
class Application:
    """Représente l’application implémentée par le logiciel."""

Nous pouvons dès lors utiliser ces classes pour représenter les différents objets à manipuler dans notre programme. Créons ensemble une école fictive appelée PlacePython, quelques mentors stars dont les noms ne devraient pas vous laisser indifférents, quelques parcours offerts par notre toute nouvelle institution et nos quatre compagnons qui viennent décider à l’instant de se lancer dans l’aventure.

# Création de l'école PlacePython
placepython = Ecole()

# Création de nos mentors
guido_vanrossum = Mentor()
adrian_holovaty = Mentor()
armin_ronacher = Mentor()
thierry_chappuis = Mentor()

# Création de quelques parcours
accelerateur_django = Parcours()
python_gagnant = Parcours()
objectif_data = Parcous()

# Création de quelques étudiants
paul = Etudiant()
jean = Etudiant()
philippe = Etudiant()
antoine = Etudiant()

Facile? Même si les objets que nous venons de créer ne stockent ni information, ni comportement ou action, vous faites déjà de la programmation orientée objet.

Un objet vide, sans information spécifique le décrivant, n’est pas très intéressant ni très utile dans la pratique. 

Ajoutons des attributs à nos objets !

Un attribut sert à ajouter à associer un morceau d’information spécifique à un objet. Dans notre exemple simple d’une instance de Personne, cet objet peut être décrit par un prénom, un nom ou une adresse email. On pourrait imaginer d’autres informations à enregistrer au sujet d’une personne, comme son age, son numéro, sa profession, son sexe. 

Une instance de Ecole peut, elle, être décrite par son nom, son adresse de contact, son catalogue de formations, etc. Une instance de Mentor peut être décrite par son nom, les domaines dans lesquels il a de l’expérience, les projets sur lesquels il encadre des étudiants, etc. 

En fonction du besoin, ce sera à vous, analyste-développeur, d’identifier les attributs pertinents dont a besoin votre application pour représenter et manipuler l’objet.

Syntaxe de création d’un attribut

Pour ajouter des informations à un objet, il suffit d’utiliser la syntaxe objet.nom_de_l_attribut = valeur-de-l-attribut. Ainsi, on peut créer des personnes avec un prénom, un nom, un âge, une taille, une adresse à l’aide d’un code comme ci-dessous:

# 1. Création de l'objet paul vide de toute info
>>> class Personne:
...     """Représente une personne physique."""
... 
>>> paul = Personne() 

# 2. Ajout d'attributs spécifiques décrivant l'objet paul
>>> paul.prenom = "Paul"
>>> paul.nom = "Exemple"
>>> paul.age = 37
>>> paul.taille = 1.78

Nous pouvons constater que l’attribut d’un objet peut référencer n’importe quel autre type d’objet python, comme un texte (chaîne de caractères), un entier, un nombre à virgule flottante ou encore un dictionnaire.

Un attribut peut également référencer un autre objet personnalisé de programme. Regardez par exemple comment Paul peut se voir attribuer une adresse :

Nous pouvons constater que l'attribut d'un objet peut référencer n'importe quel autre type d'objet python, comme un texte (chaîne de caractères), un entier, un nombre à virgule flottante ou encore un dictionnaire.
Un attribut peut également référencer un autre objet personnalisé de programme. Regardez par exemple comment Paul peut se voir attribuer une adresse :
 
>>> class Adresse:
...     """Représente une adresse postale."""
... 
>>> paul.adresse = Adresse()
>>> 
>>> paul.adresse.rue = "chemin des reptiles"
>>> paul.adresse.rue_numero = "10"
>>> paul.adresse.ville = "Fribourg"
>>> paul.adresse.code_postal = "1700"
>>> paul.adresse.pays = "Suisse"

Nous voyons que dès qu’une donnée est une composition d’objet, il est possible de structurer cette donnée à l’aide d’attributs.

Alors ? Vous ne me dites pas que j’aurais pu accomplir exactement la même chose avec un dictionnaire ?

>>> paul = {
...     "prenom": "Paul",
...     "nom": "Exemple",
...     "age": 37,
...     "taille": 
...     
...     "adresse": {
...         "rue": "chemin des reptiles",
...         "rue_numero": "10",
...         "ville": "Fribourg",
...         "code_postal": "1700",
...         "pays": "Suisse",
...     },
... }

Merci d’avoir posé la question. Vous avez parfaitement raison. Jusqu’ici, ce que j’ai fait avec des classes, des objets et des attributs peut être fait avec un dictionnaire. Ma réponse est “pour quel avantage ?”. Nous allons en revanche voir que l’approche à base de classes propose, elle, des avantages.

On peut vérifier l’existence de ces attributs sur l’objet une personne à l’aide de la fonction vars() de python. Cette fonction affiche tous les attributs de paul et de paul.adresse sous forme d’un dictionnaire. On peut ainsi constater que toutes les informations définies plus haut sont contenues au sein de l’objet qui joue maintenant le rôle de boîte à informations.

>>> vars(paul)
{'prenom': 'Paul', 'nom': 'Exemple', 'age': 37, 'taille': 1.78, 'adresse': <__main__.Adresse object at 0x102ce0c70>}
>>> vars(paul.adresse)
{'rue': 'chemin des reptiles', 'rue_numero': '10', 'ville': 'Fribourg', 'code_postal': '1700', 'pays': 'Suisse'}

Par l’intermédiaire de ses attributs, un objet est ainsi lié à d’autres objets, chacun étant une instance d’une classe définie par python ou par vous-même. Notre objet paul est ainsi lié à plusieurs objets de type chaîne de caractère (via ses attributs prenom, nom par exemple) et à un objet de type Adresse.

Chaque objet dans ce réseau est lié à une classe permettant sa création:

Diagramme d'objets uml avec classes

Ce diagramme montre en jaune l’objet paul, en orange les objets associés à ses attributs et en bleu les classes utilisées pour créer ces objets. Pour y voir plus clair, on peut se focaliser sur paul et son attribut adresse:

Digramme d'objets uml représentant une personne et son adresse

Il est ainsi possible d’accéder à la ville dans laquelle réside notre ami paul via un appel d’attributs en chaîne. paul.adresse permet de récupérer l’objet référencé par l’attribut adresse de l’objet paul, et paul.adresse.ville permet de récupérer l’objet de type chaîne de caractères référencé par l’attribut ville de l’objet référencé par l’attribut adresse de l’objet paul:

relation entre une personne et son adresse

Cette stratégie qui permet de créer un objet à partir d’autres objets est ce qu’on appelle la composition. C’est le principe le plus important et le plus puissant de la programmation orientée objet. Avec la composition, on peut bâtir des objets simples, comme on peut bâtir des cathédrales.

>>> paul.adresse.ville
'Fribourg'

Créer plusieurs objets d’un même type avec les mêmes attributs

Dans une véritable application, on désire ne pas se limiter à la création seul objet par classe. Ainsi, la création des attributs peut vite devenir fastidieuse, comme illustrée dans le code ci-dessous:

paul = Personne() 
paul.prenom = "Paul" 
paul.nom = "Exemple"
paul.age = 37 
paul.adresse = Adresse()
paul.adresse.ville = "Fribourg"
paul.adresse.pays = "Suisse"

jean = Personne() 
jean.prenom = "Jean" 
jean.nom = "Exemple2"
jean.age = 32 
jean.adresse = Adresse()
jean.adresse.ville = "Paris"
jean.adresse.pays = "France"

Beaucoup de duplication de code, n’est-ce pas ?

Ceci n’est d’ailleurs pas spécifique à l’usage de classes. Nous serions dans une situation identique avec des dictionnaires:

paul = {
    "prenom": "Paul",
    "nom": "Exemple",
    "age": 37,
    "adresse", {
        "ville": "Fribourg",
        "pays": "Suisse",
    },
}

paul = {
    "prenom": "Jean",
    "nom": "Exemple2",
    "age": 32,
    "adresse", {
        "ville": "Paris",
        "pays": "France",
    },
}

Pratiquement identique, avec quelques limitations par rapport à l’approche à base d’objets.

7 lignes par objet, c’est beaucoup de code à taper à chaque fois qu’on désire créer un une instance de notre classe Personne. Ce ne sont toutefois pas le nombre de lignes qui comptent. Le réel problème, ici, est le nombre d’erreurs qu’il est possible de faire en créant la structure de chaque objet à la main. 

Que se passera dans mon code si une personne ne possède pas de nom, ou d’adresse ?

Alors que faites-vous lorsque vous repérez du code répétitif qui peut se présenter comme la source de nombreuses erreurs ? Eh oui, le dev étant très économe de ses efforts et désireux d’alléger sa charge mentale, il va très vite créer une ou plusieurs fonctions pour se simplifier la vie:

def personne_initialiser(
    personne, prenom, nom, age, adresse
):
    """Initialise une nouvelle personne à partir 
    des données qui la décrivent."""
    personne.prenom = prenom
    personne.nom = nom
    personne.age = age
    personne.adresse = adresse
 
def adresse_initialiser(adresse, ville, pays):
    """Initialise une adresse à partir de la ville 
    et du pays."""
    adresse.ville = ville
    adresse.pays = pays

Ainsi, la création de nos objets devient:

paul = Personne()
personne_initialiser(
    paul, 
    "Paul", 
    "Exemple", 
    37, 
    adresse_initialiser(
        "Fribourg",
        "Suisse"
    )
)

jean = Personne()
personne_initialiser(
    jean, 
    "Jean", 
    "Exemple2", 
    32, 
    adresse_initialiser(
        "Paris",
        "France"
    )
)

Le code n’est pas nécessairement plus court à cause de mon formatage sur plusieurs lignes. Je n’ai toutefois plus aucun risque de faire une erreur de structure. Les fonctions créées ci-dessus ont des paramètres obligatoires. S’ils ne reçoivent pas d’argument lors de l’exécution, une erreur sera immédiatement levée. Si par contre chaque paramètre reçoit un argument, nos fonctions se chargent de créer la structure pour nous.

Essayer d’afficher la représentation de notre paul dans le terminal:

>>> print(vars(paul))
{'prenom': 'Paul', 'nom': 'Exemple', 'age': 37, 'adresse': <__main__.Adresse object at 0x102ce0c70>}

Bon, la représentation dictionnaire ainsi que la représentation par défaut de l’adresse ne sont pas très engageantes. Continuons sur notre lancée et créons des fonctions d’affichage:

def personne_formatter_en_texte(personne):
    """Transforme une personne en une représentation
    textuelle."""
    return (
        f"{personne.prenom.title()} "
        f"{personne.nom.upper()}"
        f"{personne.age} ans "
        f"[{adresse_formatter_en_texte(personne.adresse)}]"
    )

def adresse_formatter_en_texte(adresse):
    """Transforme une adresse en une représentation 
    textuelle."""
    return f"{adresse.ville.title()} en {adresse.pays.title()}"

Vérifions que notre fonction personne_formatter_en_texte fait le job:

>>> print(personne_formatter_en_texte(paul)
Paul EXEMPLE, 37 ans [Fribourg en Suisse]

C’est tout de même plus présentable que le dictionnaire affiché plus haut!

Notre problème est maintenant que nous avons plusieurs outils séparés pour travailler avec nos objets de type Personne et Adresse: une classe Personne et une classe Adresse, une fonction personne_initialiser() et une fonction adresse initialiser(), puis une fonction personne_formatter_en_texte() et une autre nommée adresse_formatter_en_texte.Où vais-je ranger tout cela ? Comment faire pour conserver le lien qui existe entre notre classe Personne et les fonctions qui agissent sur des personnes ? Même question pour la classe Adresse et les fonctions qui agissent sur les adresses.
Les classes nous offrent la possibilité de définir les fonctions qui travaillent sur nos objets au sein même de la classe. Cela crée ainsi un lieu naturel où définir nos fonctions. Le code de notre classe Personne devient ainsi:

class Personne:
    """Représente une personne physique."""

    def initialiser(personne, prenom, nom, age, adresse):
        """Initialise une nouvelle personne à partir 
        des données qui la décrivent."""
        personne.prenom = prenom
        personne.nom = nom
        personne.age = age
        personne.adresse = adresse

    def formatter_en_texte(personne):
        """Transforme une personne en une représentation
        textuelle."""
        return (
            f"{personne.prenom.title()} "
            f"{personne.nom.upper()}"
            f"{personne.age} ans "
            f"[{personne.adresse.formater_en_texte()}]"
        )

Et celui de notre classe Adresse:

class Adresse:
    """Représente une adresse postale."""

    def initialiser(adresse, ville, pays):
        """Initialise une nouvelle adresse à
        partir de la ville et du pays."""
        adresse.ville = ville
        adresse.pays = pays

    def formatter_en_texte(personne):
        """Transforme une adresse en une représentation
        textuelle."""
        return f"{adresse.ville.title()} en {adresse.pays.title()}"

Au passage, nous avons pu raccourcir le nom de nos fonctions en éliminant les préfixes personne_ et adresse_. En effet, le contexte offert par la classe clarifie si la méthode travaille sur une personne ou un objet.

Exécuter les fonctions définies au sein d’une classe sous formes de messages (méthodes)

Comment utiliser nos nouvelles classes avec les fonctions définies à l’intérieur ? Python nous offre deux syntaxes:

# 1. Utilisation de Personne.initialiser en tant que fonction
paul = Personne()
# On exécute la fonction Personne.initialiser
# en envoyant l'objet paul en premier argument
Personne.initialiser(
    paul, "Paul", "Exemple", 37, Adresse()
)

# 2. Utilisation de la méthode paul.initialiser
paul = Personne()
# On envoie le message "initialiser" à l'objet 
# Paul, sans passer l'objet paul en argument
paul.initialiser(
    "Paul", "Exemple", 37, Adresse()
)

Je vous présente la première option de manière théorique ici. On ne l’utilise jamais !

Notre objet paul n’est que partiellement initialisé. On a pas terminé l’initialisation de l’adresse. Remédions à cela:

paul = Personne()
paul.initialiser(
    "Paul", "Exemple", 37, Adresse()
)
paul.adresse.initialiser(“Fribourg”, “Suisse”)

La situation s’améliore, mais c’est encore un peu fastidieux. Heureusement, Python nous permet de fusionner les deux premières lignes. Pour cela, il nous demande d’utiliser un nom spécial pour notre fonction initialiser. Renommons initialiser en __init__:

class Personne:
    """Représente une personne physique."""

    def __init__(personne, prenom, nom, age, adresse):
        """Initialise une nouvelle personne à partir 
        des données qui la décrivent."""
        personne.prenom = prenom
        personne.nom = nom
        personne.age = age
        personne.adresse = adresse

    ...

class Adresse:
    """Représente une adresse postale."""

    def __init__(adresse, ville, pays):
        """Initialise une nouvelle adresse à
        partir de la ville et du pays."""
        adresse.ville = ville
        adresse.pays = pays

    ...

Créer un objet devient alors encore plus facile:

paul = Personne("Paul", "Exemple", 37, Adresse(“Fribourg”, “Suisse”))

Bang ! Nous créons maintenant notre objet paul en une seule ligne. En effet, la méthode __init__ de chaque classe est appelée implicitement à la création d’un objet. En renommant notre fonction initialiser() en __init__(), nous avons ainsi permis ce raccourci très pratique et …un peu magique je vous l’accorde.

Transformer un objet en texte avec la méthode __str__

Qu’en est-il de l’affichage de Paul ? Il me suffit d’envoyer le message “formater_en_texte” à notre objet Paul pour le transformer en text:

>>> print(paul.formatter_en_text())
Paul EXEMPLE, 37 ans [Fribourg en Suisse]

Là aussi, Python nous offre un raccourci. Transformer un objet en texte est un besoin tellement courant que Python nous offre une seconde méthode spéciale. Renommons nos fonctions formatter_en_texte des classe Personne et Adresse en __str__:

class Personne:
    ...
    def __init__(personne, prenom, nom, age, adresse):
        ...

    def __str__(personne):
        ...

class Adresse:
    ...
    def __init__(adresse, ville, pays):
        ...

    def __str__(adresse):
        ...

Notre code d’affichage devient maintenant encore plus lisible, la méthode __str__ étant exécutée automatiquement lorsque notre objet est passé en argument de la fonction native print() ou de la fonction nature str():

>>> print(paul)
Paul EXEMPLE, 37 ans [Fribourg en Suisse]

Je vous recommande de définir des fonctions __init__ et __str__ dans chacune de vos classes.

Self, un paramètre pas si spécial

Jusqu’ici, chaque fonction que nous avons définie au sein de notre classe Personne commence avec le paramètre personne. De la même manière, chaque fonction définie au sein de notre classe Adresse commence avec le paramètre adresse.

Ce paramètre sert à recevoir la personne ou l’adresse sur lesquelles travaillent nos fonctions La seule condition est que ce paramètre soit placé en premier pour la syntaxe de l’appel par message/méthode fonctionne. 

Pour se rappeler que ce premier paramètre est important, la pratique courante lui donne toujours le même nom. C’est en effet une bonne pratique de nommer ce premier paramètre self, plutôt que personne ou adresse. Python ne l’impose en aucun cas. Il s’agit simplement d’une convention. Cette dernière est toutefois tellement répandue que cela en devient presque une obligation.

Réécrivons une dernière fois nos deux classes Personne et Adresse pour tenir compte de cette convention. J’en profite pour introduire des modifications à la classe Personne. On y ajoute une méthode capable de calculer l’age de manière dynamique à partir de la date de naissance:

class Personne:
    """Représente une personne physique."""

    def __init__(self, prenom, nom, date_naissance, adresse):
        """Initialise une nouvelle personne à partir 
        des données qui la décrivent."""
        self.prenom = prenom
        self.nom = nom
        self.date_naissance = naissance
        self.adresse = adresse

    def __str__(self):
        """Transforme une personne en une représentation
        textuelle."""
        return (
            f"{self.prenom.title()} "
            f"{self.nom.upper()}"
            f"{self.age} ans "
            f"[{self.adresse.formater_en_texte()}]"
        )

    def calculer_age(self):
        """Calcule l’âge de la personne à partir de sa date
        de naissance."""
        return (datetime.today() - self.date_naissance).days / 365.25

 
class Adresse:
    """Représente une adresse postale."""

    def __init__(self, ville, pays):
        """Initialise une nouvelle adresse à
        partir de la ville et du pays."""
        self.ville = ville
        self.pays = pays

    def __str__(personne):
        """Transforme une adresse en une représentation
        textuelle."""
        return f"{self.ville.title()} en {self.pays.title()}"

Synthèse et prochaines étapes

Félicitations pour votre persévérance !

J’espère que ce parcours a été enrichissant. Bien qu’il reste encore des concepts à explorer concernant les classes, les objets, les attributs, les méthodes et la composition, l’essentiel est désormais acquis.

Pour consolider vos connaissances, n’hésitez pas à réaliser 3 à 4 projets avant de vous aventurer plus loin dans la programmation orientée objet.

Thierry Chappuis
Je m’appelle Thierry. J’ai découvert le langage de programmation Python en 1999 et je suis heureux de pouvoir partager un peu de mon expérience avec Python et Django à travers de ce blog. Dites-moi ce que vous avez pensé de cet article en laissant un commentaire ci-dessous.

4 Commentaires

  1. Gaëtan

    Quand on travaille avec Django l’orientée objet fait encore plus sens pour quiconque a déjà travaillé avec les modèles et des méthodes de modèle.

    Superbe article qui explique de manière pédagogique et imagé l’utilisation et l’intérêt de l’OOP, la fonction isinstance est également très utile dans la boite outil d’un dev Python !

    J’attends avec impatience tes prochains billets.

    Réponse
    • Thierry Chappuis

      En effet, le développement Django s’appuie fortement, à différents niveau sur la programmation orientée-objet. Django détourne même le concepte d’héritage, pas traité dans ce premier article, pour faire de la composition en l’aide de l’héritage multiple. C’est ce qu’on appelle les mixins. On en trouve tant au niveau des modèles et des formulaires ou des vues fondées sur les classes.

      Définition, avoir une bonne connaissance de la programmation orientée objet et de ses mécanismes permet de pouvoir appréhender Django à un niveau avancé.

      Réponse
  2. Etienne

    Très ludique! J’ai apprécié. Dommage qu’il n’y aie pas plus d’articles de cette qualité sur ta plateforme!

    Réponse
    • Thierry Chappuis

      Merci pour les encouragement. C’est en cours. Une nouvelle version du site est cours. Une grande partie des contenus jusqu’ici distribués sur le canal Telegram trouveront leur place sur le site.

      Réponse

Soumettre un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *