L3 MIASHS, Algorithme et Programmation 3, année 2024

Rappel sur les grands paradigmes de programmation

En AP2, nous avons vu que la programmation se décomposait en paradigmes, c’est-à-dire en différentes méthodologies.

Nous allons voir un nouveau paradigme : la programmation orientée objet. Son objectif est de structurer le code en entités complexes qui reflètent une certaine réalité : des objets.

La programmation orientée objet est très utile pour le travail collaboratif et la segmentation du code. Chaque Objet représente un morceau du programme avec son environnement bien isolé. Pour cette raison, les langages fortement orientés objets sont très utilisés pour les gros projets, couplés avec des méthodologies de gestion de projet.

Les contraintes liées à la programmation orientée objet sont plus ou moins poussées selon les langages de programmation. Dans le cas de Python, on y trouve les concepts de base (objet et héritages). Les concepts plus avancés nécessitent des langages fortement typés (polymorphisme, interface abstraite, implémentation concrète).

Dans ce cours, on se limitera aux concepts présents dans l’écosystème Python. Pour en savoir plus, il faut étudier des langages différents (C++, Java par exemple).

Introduction à l’objet en Python : les classes.

Une classe en Python est un bout de code qui décrit le comportement d’un objet. Une instance d’une classe est un objet Python manipulable qui suit le comportement imposé par la classe. Il possède sa mémoire interne et des opérations propres.

Un exemple trop simple

class UneClasseInutile:
    pass
print(UneClasseInutile)
<class 'UneClasseInutile'>

Ici la classe UneClasseInutile ne sert à rien, il s’agit d’une classe vide sans méthodes ni champ (ni rien).

On peut l’instancier en un objet (qui ne sert à rien non plus !) en appelant la classe comme une fonction.

A = UneClasseInutile()
print(A)
<UneClasseInutile object at 0x7fb82c464c10>

Une première méthode

Une méthode est une fonction, qui a en premier argument une variable (qu’on appelle usuellement self) et qui représente l’instance de l’objet.

class EncoreUne:
    def une_methode(self, a):
        print("je suis une méthode!!")
        self.x = a # self représente une instance quelconque.

a = EncoreUne()
a.une_methode(10)
print(a.x)
je suis une méthode!!
10

On remarque qu’une méthode peut également avoir d’autres arguments (ou aucun) mais dans tous les cas elle doit avoir self en premier argument. Ici, a.x est une variable locale à l’instance a.

a.une_methode(5)
print(a.x)
je suis une méthode!!
5

On peut stocker des variables locales à un objet de manière dynamique :

a.y = 123
print(a.y)
123

ou supprimer un champ :

del a.y
print(a.y)
Traceback (most recent call last):
  File "/var/www/cha/check_py_md", line 81, in repl
    exec(code, env)
  File "<string>", line 2, in <module>
AttributeError: 'EncoreUne' object has no attribute 'y'

Dans l’absolu, une classe peut avoir plein de méthodes et plein de variables locales.

Des méthodes particulières

Certaines méthodes sont particulières et permettent de donner un comportement à la classe bien spécifique. Elles ont en général des noms particuliers entourés de __.

class AvecUnInit:
    def __init__(self, x):
        print("Création d'une nouvelle instance!!!")
        self.x = x # le champ x est initialisé à la valeur passée en argument.

A = AvecUnInit(42)
print(A.x)
Création d'une nouvelle instance!!!
42
class AdditionBarycentrique:
    def __init__(self, x):
        self.x = x
    def __add__(self, other):
        return AdditionBarycentrique((self.x+other.x)/2)

A = AdditionBarycentrique(0)
B = AdditionBarycentrique(10)
print((A+B).x)
5.0

On peut également changer la manière dont les instances sont affichées quand on appelle print ou str.

class AdditionBarycentrique:
    def __init__(self, x):
        self.x = x
    def __add__(self, other):
        return AdditionBarycentrique((self.x+other.x)/2)
    def __repr__(self):
        return "B:"+str(self.x)

A = AdditionBarycentrique(0)
B = AdditionBarycentrique(10)
print(A, B, A+B)
B:0 B:10 B:5.0

On verra dans le cours suivant les notions d’héritage et de ducktyping.

Propriété, getter, setter

Il arrive que certaines valeurs d’un objet soient dépendantes de son état interne. Il est alors important que ce champ lui soit réservé. Pour cela, on peut utiliser une propriété qui va retourner un champ dont la valeur est calculée par une fonction. On utilise le décorateur @property.

import math
class rectangle:
    def __init__(self, largeur, longueur):
        self.largeur = largeur
        self.longueur = longueur
    @property
    def surface(self):
        return math.sqrt(self.largeur**2 + self.longueur**2)

R = rectangle(10, 20)
print(R.surface) # Ce n'est pas une fonction, il n'y a pas les parenthèses !
22.360679774997898

Si on essaye de faire une assignation, Python n’est pas d’accord :

R.surface = 123
Traceback (most recent call last):
  File "/var/www/cha/check_py_md", line 81, in repl
    exec(code, env)
  File "<string>", line 1, in <module>
AttributeError: property 'surface' of 'rectangle' object has no setter

Ici, il est possible de modifier les valeurs des champs largeur et longueur.

R.largeur = 12
print(R.surface) # la valeur est toujours correcte
23.323807579381203

On peut empêcher une valeur de changer en lui attribuant une propriété.

class rectangle:
    def __init__(self, largeur, longueur):
        self._largeur = largeur
        self._longueur = longueur
    @property
    def surface(self):
        return math.sqrt(self.largeur**2 + self.longueur**2)
    @property
    def largeur(self):
        return self._largeur
    @property
    def longueur(self):
        return self._longueur

R = rectangle(10, 20)
print(R.longueur, R.largeur, R.surface)
20 10 22.360679774997898

Mais par contre, on ne peut plus modifier les champs :

R.longueur = 21
Traceback (most recent call last):
  File "/var/www/cha/check_py_md", line 81, in repl
    exec(code, env)
  File "<string>", line 1, in <module>
AttributeError: property 'longueur' of 'rectangle' object has no setter

Méthode de classe

Il arrive qu’on veuille plusieurs constructeurs pour une classe. On peut alors utiliser un décorateur @classmethod pour indiquer qu’une fonction définie dans la classe n’est pas une méthode mais une fonction qui ne dépend que de la classe qui l’appelle.

Son premier argument est toujours la classe qui appelle la fonction (on verra pourquoi avec l’héritage !).

class rectangle:
    def __init__(self, largeur, longueur):
        self._largeur = largeur
        self._longueur = longueur
    @property
    def surface(self):
        return math.sqrt(self.largeur**2 + self.longueur**2)
    @property
    def largeur(self):
        return self._largeur
    @property
    def longueur(self):
        return self._longueur
    @classmethod
    def depuis_coordonnées(cls, x1, x2, y1, y2):
        return cls(x2-x1, y2-y1)

Compiled the: mar. 26 nov. 2024 11:43:59 CET