Prérequis : cours sur la POO
À l’issue de ce chapitre, vous saurez :
- identifier les grands principes de la programmation orientée objet (encapsulation, héritage, polymorphisme) ;
- expliquer pourquoi Python autorise des contournements du modèle objet classique ;
- distinguer attribut d’instance et attribut de classe ;
- comprendre le mécanisme de l’héritage sur un exemple simple.
Note : les notions d’héritage, de polymorphisme et de généricité présentées dans cette page sont hors programme de terminale NSI. Elles sont proposées à titre de culture informatique et d’approfondissement.
Python se détourne parfois du paradigme objet en autorisant beaucoup de contournements du modèle.
Les principes de la programmation objet
La notion d’objet en programmation prolonge celle de type abstrait algébrique. Un objet regroupe données et traitements mais d’autres idées sont généralement aussi associées à la POO (programmation orientée objet) sans lui être exclusives :
Une classe est un modèle de création d’instances ; elle peut aussi être un modèle de création de classes (type abstrait, interface). Une classe abstraite spécifie seulement les opérations permises sur les données de l’objet, sans définir comment elles fonctionnent.
Les objets « discutent » entre eux ; la présentation usuelle de la POO parle même d’envois de messages. C’est cette « discussion » qui forme le programme qui s’exécute.
Persistance : les données d’un objet continuent d’exister même lorsque l’objet n’est pas « actif », qu’aucune de ses méthodes n’est en cours d’exécution.
Encapsulation : Les données d’un objet lui appartiennent et peuvent (dans la plupart des langages) être protégées des interventions extérieures à l’objet. L’encapsulation permet de garantir le fonctionnement d’un objet dans tout environnement qui l’utilise. Les opérations permises sur les données de l’objet forment l’interface de l’objet.
Héritage : une classe peut déclarer vérifier une ou plusieurs interface(s) - ce qui organise le code et facilite la maintenance - ou disposer automatiquement des méthodes d’autre(s) classe(s). Ainsi des méthodes déjà définies peuvent être réutilisées, sans réécrire plusieurs fois les mêmes codes.
Polymorphisme : une classe peut manipuler des données dont le type n’est pas précisé dans la définition de la classe, par exemple une classe Pile peut accepter des éléments de type Réel ou Caractères ou d’un quelconque autre type.
Généricité : grâce en particulier à l’héritage et au polymorphisme, l’étude d’un problème conduit à une solution abstraite qui facilite la résolution de problèmes similaires.
En Python, les types sont des classes
La classe détermine les attributs et les méthodes applicables à un objet.
La classe list a bien une méthode append mais pas la classe tuple.
objet = [1, 2, 3]
print(type(objet))
objet.append(4)
print(objet)
objet = (1, 2, 3)
print(type(objet))
objet.append(4)
print(objet)
En Python, tout est dynamique !
On peut ajouter dynamiquement des attributs et des méthodes à une classe existante ou à des objets déjà créés. On peut même créer une classe pour créer des objets vides et les remplir après.
Encapsulation non garantie
En Python, un attribut (ou une méthode) dont le nom commence par deux tirets bas est considéré comme privé(e), donc normalement non utilisable de l’extérieur de l’objet. Cependant aucun mécanisme n’empêche d’y accéder.
Des objets déformés par rapport au modèle de leur classe
Ce qui suit, montre ce qu’il est possible de faire avec les objets en Python.
Attention : tout ce qui est possible n’est pas nécessairement conseillé ! Ce qui suit est destiné à permettre de comprendre le fonctionnement des objets en Python. Dans la pratique, il est plutôt conseillé d’utiliser les objets de Python conformément au paradigme objet qui consiste à utiliser les classes pour créer des objets similaires.
class ObjetVide:
"""Cette classe définit des objets sans attribut ni méthode """
pass
On crée deux instances d’objets vides :
un_objet = ObjetVide()
autre_objet = ObjetVide()
Chaque objet définit un espace de nommage distinct. Dans cet espace on peut définir des variables et leur affecter des valeurs. On crée ainsi des attributs d’instance.
un_objet.nom = "Objet 1"
un_objet.description = "C'est mon premier objet Python !"
Les espaces de nommage de deux objets (même s’ils sont instances de la même classe) sont indépendants.
autre_objet.nom = "Objet 2"
autre_objet.description = "C'est le deuxième."
autre_objet.prix = 10000
On vérifie en affichant les attributs des deux objets.
print(un_objet.nom, ":", un_objet.description)
print(autre_objet.nom, ":", autre_objet.description,
"Valeur :", autre_objet.prix)
Ajouter des méthodes à une classe
On peut ajouter une méthode à la classe ObjetVide.
ObjetVide.affiche = lambda self : print(self.nom)
Cette méthode devient immédiatement utilisable pour tous les objets de la classe.
un_objet.affiche()
La méthode peut aussi être appelée par son nom dans sa classe. Il faut alors utiliser comme paramètre le nom de l’objet.
ObjetVide.affiche(autre_objet)
Attributs de classe
On peut aussi définir des attributs de classe.
ObjetVide.numero = 12
Tous les objets de la classe en profitent, mais doivent partager cet attribut.
print(un_objet.numero)
ObjetVide.numero += 1
print(un_objet.numero)
print(autre_objet.numero)
Les objets peuvent aussi le redéfinir.
autre_objet.numero += 1
print(un_objet.numero)
print(autre_objet.numero)
Attention : le caractère dynamique rend rapidement incompréhensible ce mélange d’attributs de classe et d’attributs d’instance.
Il convient donc de limiter les attributs de classe au cas où cela est strictement nécessaire.
Il est aussi plus prudent de ne définir des attributs d’instance qu’à l’intérieur d’une méthode d’initialisation d’un objet.
Héritage
Complément hors programme. L’héritage n’est pas exigible au baccalauréat ; il est présenté ici parce qu’il est omniprésent dans les bibliothèques Python.
L’héritage est largement utilisé dans les classes prédéfinies dans les bibliothèques Python. Quand une classe B hérite d’une classe A, toutes les méthodes de la classe A sont disponibles dans la classe B qui peut éventuellement en redéfinir certaines.
Cette architecture permet de définir des comportements généraux dans des classes générales, puis de redéfinir une partie des traitements dans des classes plus spécialisées.
Exemple d’héritage : polynômes et fonctions affines
On propose une classe Polynome puis une sous-classe Affine qui
hérite des méthodes de la classe Polynome.
class Polynome:
"""Construit un polynome par la liste de ses coefficients donnés par degré décroissant"""
def __init__(self,coefs):
self.coefs = coefs
def eval(self,x):
px = 0
for c in (self.coefs):
px = px * x + c
return (px)
def __add__(p,q):
""" pour l'addition, on a besoin de coefficients ordonnés par degrés croissant """
pr = list(reversed(p.coefs))
qr = list(reversed(q.coefs))
if len(pr) <= len(qr):
cr = list(map((lambda x,y :x+y), pr, qr))+ qr[len(pr):]
else:
cr = list(map((lambda x,y :x+y), pr, qr))+ pr[len(qr):]
return(Polynome(list(reversed(cr))))
def __repr__(self):
r = "(lambda x: "
d = len(self.coefs)
for i in range(d):
c = self.coefs[i]
p = d - i - 1
r += (("+" if c>=0 else "") + str(c) + ("*x**" + str(p) if p >= 2 else "" if p == 0 else "*x") + " ") if c!=0 else ""
return(r + ')')
On peut alors construire des polynômes en donnant la liste de leurs coefficients par degré décroissant, puis les additionner et les afficher. On a pris soin de
générer, avec la méthode __repr__ un affichage exécutable en tant que code Python ce qui permet d’évaluer un polynôme en un point et de comparer avec le
résultat donné par la méthode eval.
p = Polynome([1,0,-1])
q = Polynome([7, 3, -4, 1])
print(p)
print(q)
print(p+q)
print(eval(repr(p+q))(5))
print((p+q).eval(5))
On peut maintenant définir la sous-classe Affine, en ne redéfinissant que l’initialisation et en profitant de toutes les méthodes héritées. Une classe peut
hériter d’une ou plusieurs classes - notées entre parenthèses au moment de la création de la classe.
class Affine(Polynome):
def __init__(self,a,b):
self.coefs = [a,b]
On peut alors utiliser la classe Affine avec toutes ses méthodes héritées de la classe Polynome.
f = Affine(2,4)
g = Affine(-1, 6)
print(f)
print(g)
print(f+g)
f.eval(12)
Activité : Consulter la documentation de la classe standard tuple et les méthodes disponibles sur python.org
. Observer la classe Couple et la manière dont la
méthode d’addition est modifiée.
class Couple(tuple):
def __add__ (t1, t2):
return(t1[0]+t2[0], t1[1]+t2[1])
c = Couple((1,2))
d = Couple((3,4))
c + d
- En Python, tout est objet : les types de base (
int,list,tuple…) sont eux-mêmes des classes. - Python autorise l’ajout dynamique d’attributs et de méthodes, ce qui contourne le modèle objet classique. Cette souplesse est à utiliser avec prudence.
- Les attributs de classe sont partagés par toutes les instances ; les attributs d’instance sont propres à chaque objet. Il est conseillé de définir les attributs d’instance dans
__init__. - L’encapsulation (convention
__attribut) n’est pas strictement appliquée en Python : il reste possible d’accéder aux attributs privés. - L’héritage (hors programme) permet à une classe fille de réutiliser les méthodes d’une classe mère, tout en pouvant les redéfinir.