Vous êtes sur la page 1sur 133

Université MAPON

KINDU - RD.CONGO

Cours de Programmation Orienté Objet


<< Bac 2 Informatique >>

Cyrille KESIKU
cyrillekesiku2@gmail.com

Novembre 2023

1
Chapitre 1. Introduction à la Programmation Orientée Objet

1.1. Concepts fondamentaux de la POO .


1.2. Avantages de la POO dans le développement logiciel.
1.3. Python en tant que langage de programmation orientée objet.
1.4. Installation et configuration de l'environnement Python.

2
1.1. Concepts fondamentaux de la POO .

La programmation orientée objet, également connue sous le nom de


programmation par objet, est un paradigme de programmation
informatique qui consiste à créer et à assembler des briques logicielles
appelées objets.

Un objet peut représenter un concept, une idée ou toute entité du


monde physique, comme une voiture, une personne ou même une
page d'un livre.

3
1.1.1. Complexité des problèmes

La mission principale de l'équipe de développement de logiciel est de


créer l'impression de simplicité pour protéger l'utilisateur des
complexités extérieures inattendues.

De plus, la taille du logiciel n'est probablement pas sa principale vertu.


Par conséquent, nous nous efforçons d'écrire moins de code, mais
parfois, nous devons faire face à une quantité considérable de
spécifications.

4
Le logiciel développé doit présenter les caractéristiques suivantes :

- Extensibilité : faculté d’adaptation d’un logiciel aux changements de


spécification. La simplicité de la conception : il sera toujours plus facile
de modifier une architecture simple qu'une architecture complexe.

Plus les modules d'une architecture logicielle sont autonomes, plus il


est probable qu'une simple modification n'affecte qu'un seul module
ou un nombre limité de modules, plutôt qu'une réaction en chaîne sur
tout le système.

5
- Utilisation : la capacité d'un logiciel à être utilisé entièrement ou
en partie pour de nouvelles applications. Le concept à garder est
de "ne pas réinventer la roue!" Programmez moins pour
programmer mieux.

C'est la dernière étape du développement de l'objet en


programmation! Il ajoute une sémantique qui facilite la
programmation.

6
1.1.2. Classe

Une classe est un ensemble d’objets partageant certaines propriétés.


Il s’agit d’un concept abstrait, comme par exemple les plans d’une
maison.

Exemple: La classe personne , une personne a :


- Le nom
- Le prenom
- L'âge
- Le sexe
- etc…
Une personne peut, marcher, parler manger (Actions)

7
1.1.3 Objet

Un objet est une définition de caractéristiques distinctes qui a un


but spécifique.

Il s'agit d'un élément tangible qui contient les caractéristiques de


sa classe, comme une maison qui suit les plans déterminés
auparavant. On dit aussi que l’objet est une instance de la classe.

Un objet est une définition de caractéristiques distinctes qui a un


but spécifique.

8
Il s'agit d'un élément tangible qui contient les caractéristiques de
sa classe, comme une maison qui suit les plans déterminés
auparavant. On dit aussi que l’objet est une instance de la classe.

Exemple : dans la classe personne on a les objets comme,

1. Kasilu Jeans 28 ans maculin


2. Maya Rebecca 47 ans Feminin

9
Chaque objet peut faire des actions qu’on appelle méthodes.
Alors un objet peut avoir une ou plusieurs méthodes.

Exemple :

l’objet (Kasilu Jeans 28 ans maculin) , de la classe personne


peut :
- Parler ()
- Marcher()
- Manger()
- etc …

10
1.1.4. Encapsulation

L’encapsulation permet de définir des niveaux de visibilité des éléments


de la classe. Ces niveaux de visibilité définissent ce qu’on appelle la
portée (ou encore le périmètre) de la l’attribut/méthode. La portée est
ainsi définie par méthode et par attribut et indique les droits à leur
accès.

11
Il existe trois niveaux de visibilité :

● Publique : les attributs publics sont accessibles à tous


● Protégée : les attributs protégés sont accessibles seulement dans la
classe elle-même et aux classes dérivées
● Privée : les attributs privés sont accessibles seulement par la classe
elle-même

12
1.1.5. Héritage des classes

Les relations entre les objets sont un avantage de la programmation


orientée objet (POO). Ces relations sont créées par les développeurs
et forment l'architecture d'une application.

il est possible d'assimiler une classe à une autre classe qui


correspond à une notion plus abstraite ou plus générale. Le
mécanisme qui permet de mettre en place ce type de relation est
appelé héritage.

13
Exemple :

Classe : Personne{
Nom
Prenom La classe Étudiant peut hériter de la
Sexe classe Personne ses attributs et ses
Date naissance méthodes pour éviter la réécriture du
} code.
Classe : Etudiant{
Nom
Prenom
Sexe
Date naissance

}
14
1.6. Le polymorphisme

Le polymorphisme est un mécanisme important dans la


programmation objet. Il permet de modifier le comportement d’une
classe fille par rapport à sa classe mère.

Le polymorphisme permet d’utiliser l’héritage comme un mécanisme


d’extension en adaptant le comportement des objets.

15
1.2. Avantages de la POO dans le développement logiciel.

Dans le domaine de la programmation orientée objet, les programmes


informatiques sont basés sur l'interaction entre des "objets". Un objet
possède des "variables d'instance" et des "méthodes".

Valorise le record de variables d'instance associé à l'objet. Les fonctions


qui impliquent un aspect spécifique de l'objet sont appelées méthodes.

Plusieurs avantages peuvent être énumérés tel que :


- La réutilisation de code
- Facilitation dans le travail d'équipe
- Etc …

16
1.3. Python en tant que langage de programmation
orientée objet.

En tant que langage de programmation orienté objet Python apporte


de grands avantages de la modularité, l'abstraction, la productivité et
ré-utilisabilité. Ce langage est beaucoup plus utilisée dans le monde
pour plusieurs raisons :

- Sa flexibilité élevée
- Un langage hybrid
- Intégration de plusieurs librairies facilitant la production logiciel
- et bien d’autres

17
1.4. Installation et configuration de l'environnement Python.

Dans ce cours nous allons utiliser la distribution ANACONDA avec


Python 3.9.13.

En plus nous optons pour les éditeurs de code :

• VS-CODE et
• jupyter notebook

18
Chapitre 2 : Création de Classes et d'Objets

2.1. Création de classes.


2.2. Instanciation d'objets.
2.3. Attributs d’objet
2.4. Méthodes d’objet
2.5. Méthodes spéciales (constructeurs, destructeurs, `__str__`).
2.6. Attributs de classe et Méthodes de classe.
2.7. Encapsulation : utilisation des méthodes getter et setter.

19
2.1. Création de classes.

Les noms des classes en Python commencent par convention par une
lettre en majuscule.

Selon le principe de l'écriture en dromadaire, les différents mots sont


également indiqués par une lettre majuscule.

Cette convention a changé au fil du temps, ce qui permet de trouver


des classes plus anciennes dont les noms commencent par une lettre
minuscule.

20
Voici une classe en python:
Python :

class personne:
pass

Un nom et un bloc spécifiant la définition d'une classe sont utilisés


pour définir une classe.
Il est nécessaire d'utiliser le mot-clé pass si on ne veut rien spécifier
dans le bloc.

21
2.2. Instanciation d'objets.

Création de l’objet nouvelle_personne,

Python:
nouvelle_personne = Personne()

On dit que l’objet nouvelle_personne est une instance de la classe


Personne() . Un nouveau type de langage est défini par une classe. La
POO est d'abord un modèle de programmation qui permet d'ajouter des
nouveaux types d'objet au langage.

22
On peut vérifier si nouvelle_personne est l’instance de classe
Personne.

Python:
print(isinstance(nouvelle_personne, Personne))
Ou
print(type(nouvelle_personne))

23
2.3. Attributs d’objet

Un objet peut posséder des attributs. Ce sont des proprieties de l’objet.


Ces attributs definissent l’etat interne de la classe.

Python:

import numpy as np

#Création de la classe Calcul_matriciel


class Calcul_matriciel:
pass
# Création de l'objet objet_matriciel
objet_matriciel = Calcul_matriciel()

# ajout d'un attribut x avec comme valeur matrice 3x3 dans l'objet.
objet_matriciel.x = np.array([[2,7,8],[6,9,4],[1,2,4]])

24
Dans l’exemple precedent nous avons creer un objet dans lequel
nous avons ajouter un attribute.

On peut ajouter autant d’attributs dans un objet comme on peut aussi


creer plusieurs objets de la classe Calcul_matriciel et en affecter un
ou plusieurs attributs.

On peut affichier la valeur de l’attribut x de notre objet.

Python:

print(objet_matriciel.x)

25
Exercice1 :
• Ajouter un attribut det_x avec comme valeur le déterminant de la matrice x dans
l’objet ‘’objet_mariciel’’

• Ajouter un deuxième attribut valeurs_propres avec comme valeur, les valeurs propres
de la x dans l’objet ‘’objet_mariciel’’

• Ajouter un troisième attribut vecteurs_propres avec comme valeur, les vecteurs


propres de x dans l’objet ‘’objet_mariciel’’
Résolution
Python:

import numpy as np

# ajout des attributs dans l'objet objet_matriciel.


objet_matriciel.x = np.array([[2,7,8],[6,9,4],[1,2,4]])

objet_matriciel.det_x = np.linalg.det(objet_matriciel.x)
objet_matriciel.valeurs_p = np.linalg.eig(objet_matriciel.x)[0]
objet_matriciel.vecteur_p = np.linalg.eig(objet_matriciel.x)[1]

print(objet_matriciel.x,'\n')
print(objet_matriciel.det_x,'\n')
print(objet_matriciel.valeurs_p,'\n')
print(objet_matriciel.vecteur_p,'\n')
Un attribut peut être supprimée aussi en utilisant le mot del.

Par exemple:

Python

del objet_matriciel.det_x
print(objet_matriciel.det_x,'\n')
2.4. Méthodes d'objet.

Les méthodes représentent les comportements des objets. Elles sont


décrites dans la classe en suivant les mêmes règles d’écriture que les
fonctions.

Exemple de la méthode calcul_produit_matriciel:


Python

class Calcul_matriciel:

#Création de la première méthode


def calcul_produit_matrice(self):
return np.dot(self.x , self.y)

NB. Le paramètre self dans la création de la méthode, représente l’objet


On peut alors créer un nouveau objet et afficher le résultat da
la méthode créée

Python:

#Création de l'objet objet_matriciel1


objet_matriciel1 = Calcul_matriciel()

# Ajoutons les matrices x et y dans notre objet


objet_matriciel1.x = np.array([[2,7,8],[6,9,4],[1,2,4]])
objet_matriciel1.y = np.array([[4,8,8],[6,4,4],[9,2,4]])

# Appeler la méthode calcul_produit_matrice


print(objet_matriciel1.calcul_produit_matrice())
2.5. Méthodes spéciales (constructeurs, destructeurs, `__str__`).

Un constructeur est une méthode spéciale qui est appelée au moment


de la création de l’objet. Il permet de garantir que l’objet est dans un
état cohérent dès sa création. En Python, le constructeur
s’appelle __init__()
est prend comme premier paramètre l’objet en cours de création.
Python:
## le constructeur
import numpy as np
class Calcul_matriciel:
# le constructeur
def __init__(self,x=0,y=0):
self.x = x
self.y = y
def calcul_produit_matrice(self):
return np.dot(self.x , self.y)
Python:

# Création de deux matrices


x = np.array([[2,7,8],[6,9,4],[1,2,4]])
y = np.array([[4,8,8],[6,4,4],[9,2,4]])
#Création de l'objet objet_matriciel2
objet_matriciel2 = Calcul_matriciel(x , y)
print(objet_matriciel2.calcul_produit_matrice())

2.6. Attributs de classe et Méthodes de classe.

2.6.1. Attributs de classe


Une classe peut également posséder des caractéristiques.
Il suffit de les déclarer dans le corps de la classe pour cela.
Les attributs de classe peuvent être obtenus depuis la classe elle-même et
peuvent être utilisés par tous les objets. Si un objet modifie un attribut de
classe, tous les autres objets peuvent voir cette modification.
Python :

## attribut des classes


import numpy as np
class Calcul_matriciel:

determinant = 0 # Attribut de classe

# le constructeur
def __init__(self , x=0 , y=0):
self.x = x
self.y = y
Calcul_matriciel.determinant = np.linalg.det(x)
def calcul_produit_matrice(self , x , y):
return np.dot(self.x , self.y)
Python:

# Création de deux matrices


x = np.array([[2,7,8],[6,9,4],[1,2,4]])
y = np.array([[4,8,8],[6,4,4],[9,2,4]])

#Création de l'objet objet_matriciel2


objet_matriciel2 = Calcul_matriciel(x , y)

print(f' Le determinant de la première matrice x \


est {objet_matriciel2.determinant}')

2.6.2. Méthodes de classe

Tout comme il est possible de déclarer des attributs de classe, il est


également possible de déclarer des méthodes de classe.
Pour cela, on utilise le décorateur @classmethod
Comme une méthode de classe appartient à une classe, le premier
paramètre correspond à la classe. Par convention, on appelle ce
paramètre cls pour préciser qu’il s’agit de la classe et pour le distinguer
de self.
Python :

## Méthodes de classes

import numpy as np

class Calcul_matriciel:
determinant = 0
numero_objet = 0
# le constructeur
def __init__(self , x=0 , y=0):
self.x = x
self.y = y
Calcul_matriciel.determinant = np.linalg.det(x)
Calcul_matriciel.numero_objet += 1
Python :

def calcul_produit_matrice(self , x , y):


return np.dot(self.x , self.y)

@classmethod # Le décorateur
def affichiernumero(cls):
return cls.numero_objet

# Création de deux matrices


x = np.array([[2,7,8],[6,9,4],[1,2,4]])
y = np.array([[4,8,8],[6,4,4],[9,2,4]])

#Création de l'objet objet_matriciel2


objet_matriciel = Calcul_matriciel(x , y)
objet_matriciel1 = Calcul_matriciel(x , y)
objet_matriciel2 = Calcul_matriciel(x , y)

print(f' Le determinant de la première matrice x est


{objet_matriciel2.determinant}\n Le numéro de l\'objet est:
{Calcul_matriciel.affichiernumero()}')
2.7. Encapsulation : utilisation des méthodes getter et setter.

l'encapsulation est l'un des principes fondamentaux de la POO. Il


consiste à restreindre l'accès à certains éléments d'une classe (le
plus souvent ses attributs).

L'objectif de l'encapsulation est de ne laisser accessible que le


strict nécessaire pour que la classe soit utilisable.
Dans plusieurs langages de programmation l’encapsulation se fait de plusieurs
formes, une méthode ou un attribut peut être:

- Privée
- Publique
- Protégée

Python ne dispose pas de la notion de modificateurs d'accès, tels que private,


protected et public, pour restreindre l'accès aux attributs et aux méthodes d'une
classe.

En Python, la distinction se fait entre les membres publics et non publics de la


classe.
Si on souhaite signaler qu'un attribut ou une méthode donnée n'est pas
publique, on doit utiliser la convention Python bien établie qui consiste à
préfixer le nom par un trait de soulignement (_).

Il faut noter qu'il ne s'agit que d'une convention. Elle n'empêche pas les
autres programmeurs d'accéder aux attributs en utilisant la notation par
points, comme dans obj._attr.

Cependant, c'est une mauvaise pratique que d'enfreindre cette convention.


Les méthodes Getter et Setter ?

Les méthodes Getter et Setter sont très répandues dans de nombreux


langages de programmation orientés objet. En guise de définition
approximative, on peut dire que les méthodes Getter et Setter sont :

- Getter : Une méthode qui permet d'accéder à un attribut dans


une classe donnée.
- Setter : Une méthode qui permet de définir ou de modifier la valeur
d'un attribut dans une classe.
Python :

# Méthodes getter() et setter()


import numpy as np
class Calcul_matriciel:
def __init__(self, x , y):
self._x = x
self._y = y

# Méthode getter()
def get_x(self):
return self._x

# Méthode setter()
def set_x(self , value):
self._x = value
Python

# Création de l’objet matrice1


matrice1 = Calcul_matriciel(x = np.array([[2,7,8],[6,9,4],[1,2,4]]),
y = np.array([[4,8,8],[6,4,4],[9,2,4]]))
# Appel de la méthode get()
matrice1.get_x()

Python :

# Appel de la méthode set()


matrice1.set_x(np.array([[9,8,9],[18,9,14],[8,8,8]]))
matrice1.get_x()
En POO, le modèle getter et setter suggère que les attributs publics ne
doivent être utilisés que lorsque l'on est sûr que personne n'aura jamais
besoin de leur associer un comportement.

Si un attribut est susceptible de modifier son implémentation interne, il


convient d'utiliser les méthodes getter et setter.

L'implémentation du modèle getter et setter nécessite :

- de rendre les attributs non publics


- écrire des méthodes getter et setter pour chaque attribut
Chapitre 3 : Héritage et Polymorphisme

3.1. Compréhension de l'héritage en Python.


3.2. Création de sous-classes et d'héritage multiple.
3.3. Redéfinition de méthodes dans les sous-classes.
3.4. Utilisation de l'opérateur `super()`
3.5. Polymorphisme en Python.
3.6. Interfaces et classes abstraites.
3.1. Compréhension de l'héritage en Python.

Le mécanisme de l'héritage en POO est essentielle pour comprendre


comment les classes et les objets sont organisés dans un langage de
programmation orienté objet.

L'héritage permet de créer de nouvelles classes en utilisant les


caractéristiques et le comportement des classes existantes, ce qui
favorise la réutilisation du code et la structuration du programme.
Concepts de base

 Classe de base (classe mère ou parent) : Une classe qui définit les attributs et les méthodes de
base que d'autres classes vont hériter.

 Classe dérivée (classe enfant ou sous-classe) : Une classe qui hérite des attributs et des
méthodes d'une classe de base. Elle peut également ajouter de nouveaux attributs et méthodes ou
modifier ceux hérités.

 Héritage simple : Une classe enfant peut hériter des caractéristiques d'une seule classe de base.
Python ne prend pas en charge l'héritage multiple directement.

 Méthode d'initialisation (__init__) : La méthode spéciale __init__ est utilisée pour initialiser les
attributs d'une classe. Lorsqu'une classe enfant hérite de la classe de base, elle peut appeler la
méthode __init__ de la classe parent pour initialiser ses propres attributs.

 Méthode super() : La fonction super() permet d'appeler une méthode d'une classe parent à partir
de la classe enfant, ce qui est utile pour initialiser les attributs hérités.
Exemple héritage :

Créons une classe de base Equation avec des classes enfants pour
résoudre différentes équations mathématiques, notamment des équations
linéaires et quadratiques. Chacune des classes enfant héritera de la classe
de base Equation et implémentera sa propre méthode pour résoudre
l'équation
Python:

# Classe parent
class Equation:
def __init__(self, coefficients):
self.coefficients = coefficients

def resoudre(self):
pass
Première classe enfants : EquationLinaire

Python:

class EquationLineaire(Equation):
def resoudre(self):
if len(self.coefficients) != 2:
return "L'équation linéaire doit avoir exactement 2 coefficients."
a, b = self.coefficients
if a == 0:
if b == 0:
return "L'équation a une infinité de solutions."
else:
return "L'équation n'a pas de solution."
x = -b / a
return f" La solution de l'équation linéaire est x = {x}"
Deuxième classe enfants : EquationQuadratique
Python :

import math
class EquationQuadratique(Equation):
def resoudre(self):
if len(self.coefficients) != 3:
return "L'équation quadratique doit avoir exactement 3 coefficients."
a, b, c = self.coefficients
discriminant = b**2 - 4*a*c
if discriminant > 0:
x1 = (-b + math.sqrt(discriminant)) / (2*a)
x2 = (-b - math.sqrt(discriminant)) / (2*a)
return f"Les solutions de l'équation quadratique sont x1 = {x1} et x2 = {x2}"
elif discriminant == 0:
x = -b / (2*a)
return f"L'équation quadratique a une solution double : x = {x}"
else:
return "L'équation quadratique n'a pas de solution réelle."
# Utilisation des classes
coefficients_lineaire = [2, -4]
coefficients_quadratique = [1, -3, 2]

equation_lineaire = EquationLineaire(coefficients_lineaire)
equation_quadratique = EquationQuadratique(coefficients_quadratique)

print(equation_lineaire.resoudre())
print(equation_quadratique.resoudre())

Dans cet exemple, Equation est la classe de base avec une méthode resoudre,
qui est une méthode abstraite (elle ne fait rien). Les sous-classes
EquationLineaire, EquationQuadratique héritent de Equation et implémentent
leur propre logique pour résoudre des équations linéaires et
quadratiques respectivement.
3.2. Création de sous-classes et d'héritage multiple

L'héritage multiple est un mécanisme qui permet à une classe enfant de


dériver des attributs et des méthodes de plusieurs classes parentes. Cela
signifie qu'une classe peut hériter des caractéristiques de plusieurs sources
différentes.

L'héritage multiple offre une flexibilité considérable lors de la création de


classes complexes et réutilisables, mais il nécessite une gestion prudente
pour éviter des problèmes potentiels.
 Définition d'une classe avec héritage multiple :
Pour définir une classe avec héritage multiple, on énumère les noms
des classes parentes séparés par des virgules dans la déclaration de la
classe enfant.

class ClasseParent1:
# Attributs et méthodes de la classe parent 1

class ClasseParent2:
# Attributs et méthodes de la classe parent 2

class SousClasse(ClasseParent1, ClasseParent2):


# Attributs et méthodes spécifiques à la sous-classe
 Ordre de résolution des méthodes (MRO - Method Résolution Order)

Lorsque vous appelez une méthode dans une classe enfant, Python suit un
ordre de résolution des méthodes pour déterminer quelle classe parente
est consultée en premier.

Cet ordre est appelé l'ordre de résolution des méthodes (MRO). La


fonction mro() peut être utilisée pour afficher l'ordre de résolution des
méthodes pour une classe donnée.
 Gestion des conflits :
L'héritage multiple peut entraîner des conflits potentiels lorsque deux
classes parentes ont des attributs ou des méthodes avec le même nom.
Pour résoudre ces conflits, Python suit l'ordre MRO. En général, la classe
parente qui apparaît en premier dans la liste d'héritage aura la priorité
en cas de conflit.

 Complexité et clarté du code :


L'héritage multiple peut rendre le code plus complexe à gérer en raison
de la gestion des conflits potentiels et de la nécessité de suivre l'ordre
MRO.

Il est important de maintenir le code aussi clair que possible pour


faciliter la compréhension et la maintenance.
Exemple de l’heritage multiple:

# Classe parent1
class Adresse:
def __init__(self, rue, ville, code_postal):
self.rue = rue
self.ville = ville
self.code_postal = code_postal

def obtenir_adresse(self):
return f"{self.rue}, {self.ville}, {self.code_postal}"

# Classe parent2
class Contact:
def __init__(self, email, telephone):
self.email = email
self.telephone = telephone

def obtenir_contact(self):
return f"Email: {self.email}, Téléphone: {self.telephone}"
# Classe fille
class Personne(Adresse, Contact):
def __init__(self, nom, prenom, rue, ville, code_postal, email, telephone):
# Appel des constructeurs des classes parentes
Adresse.__init__(self, rue, ville, code_postal)
Contact.__init__(self, email, telephone)
self.nom = nom
self.prenom = prenom

def obtenir_info_personne(self):
return f"Nom: {self.nom}, Prénom: {self.prenom},
{self.obtenir_adresse()}, {self.obtenir_contact()}"

# Utilisation des classes


personne = Personne("KALUNGA", "Jules", " 54 Kiseni", "Kindu", "085",
"jules@example.com", "598555")
print(personne.obtenir_info_personne())
3.3. Redéfinition de méthodes dans les sous-classes.

La redéfinition de méthodes dans les sous-classes en Python, également


appelée "méthode d'overriding" ou "méthode de surcharge", est un concept
essentiel de la programmation orientée objet.

Elle permet à une sous-classe de fournir sa propre implémentation d'une


méthode héritée de la classe parente. Cela permet à la sous-classe de
personnaliser ou de modifier le comportement de la méthode héritée tout
en conservant la structure générale de la classe parente.
Lorsque on appel enfant.methode_parent(), la méthode de la classe
enfant est exécutée, remplaçant la méthode de la classe parente. Cela
montre la redéfinition de la méthode methode_parent dans la sous-
classe.

La redéfinition de méthodes est utile pour les cas où une sous-classe a


besoin d'un comportement spécifique ou d'une implémentation
différente de la méthode héritée de la classe parente, tout en
maintenant la même interface (nom de la méthode) pour un code plus
propre et modulaire.
Exemple
class Vehicule:
def __init__(self, marque, modele):
self.marque = marque
self.modele = modele

def description(self):
return f"Véhicule de marque {self.marque}, modèle {self.modele}"

class Voiture( Vehicule ):


def __init__(self, marque, modele, nombre_portes):
super().__init__(marque, modele)
self.nombre_portes = nombre_portes

def description(self):
return f"Voiture de marque {self.marque}, modèle {self.modele},
{self.nombre_portes} portes"
class Moto(Vehicule):
def __init__(self, marque, modele, cylindree):
super().__init__(marque, modele)
self.cylindree = cylindree

def description(self):
return f"Moto de marque {self.marque}, modèle {self.modele}, cylindrée
{self.cylindree}cc"

# Utilisation des classes


vehicule = Vehicule("Toyota", "Camry")
voiture = Voiture("Honda", "Civic", 4)
moto = Moto("Kawasaki", "Ninja", 600)

print(vehicule.description()) # Appelle la méthode de la classe de base


print(voiture.description()) # Redéfinition de la méthode dans la sous-classe Voiture
print(moto.description()) # Redéfinition de la méthode dans la sous-classe Moto
3.4. Utilisation de l'opérateur `super()`

L'opérateur super() en Python est utilisé pour appeler des méthodes ou


des constructeurs de la classe parente à partir de la classe enfant. Cela est
couramment utilisé dans le contexte de la redéfinition de méthodes dans
les sous-classes, lorsque vous souhaitez étendre ou personnaliser le
comportement de la méthode de la classe parente tout en utilisant une
partie de sa logique. L'opérateur super() facilite l'appel des méthodes de
la classe parente.
Procédure

Appeler le constructeur de la classe parente :


On peut utiliser super() pour appeler le constructeur de la classe parente
à partir de la sous-classe. Cela est utile lorsqu’o a besoin d'initialiser les
attributs hérités de la classe parente dans la sous-classe.
class ClasseParent:
def __init__(self, nom):
self.nom = nom

class ClasseEnfant(ClasseParent):
def __init__(self, nom, age):
super().__init__(nom) # Appel du constructeur de la classe parente
self.age = age
Appeler des méthodes de la classe parente :

On peut utiliser super() pour appeler des méthodes de la classe parente


dans la sous-classe. Cela est utile pour réutiliser la logique de la
méthode de la classe parente tout en y apportant des modifications ou
en ajoutant un comportement supplémentaire.

class ClasseParent:
def methode_parent(self):
print("Méthode de la classe parent")

class ClasseEnfant(ClasseParent):
def methode_enfant(self):
super().methode_parent() # Appel de la méthode de la classe parente
print("Méthode de la classe enfant")
Appel de méthodes parentes avec des arguments :

Vous pouvez également utiliser super() pour appeler des méthodes


parentes avec des arguments, si nécessaire.

class ClasseParent:
def methode_parent(self, argument):
print(f" Méthode de la classe parent avec {argument}")

class ClasseEnfant(ClasseParent):
def methode_enfant(self, argument):
# Appel de la méthode de la classe parente avec l'argument
super().methode_parent(argument)
print(f" Méthode de la classe enfant avec {argument}")
Exemple
class Personne:
def __init__(self, nom, age):
self.nom = nom
self.age = age

def afficher_info(self):
print(f"Nom: {self.nom}, Âge: {self.age}")

class Etudiant(Personne):
def __init__(self, nom, age, programme):
super().__init__(nom, age) # Appel du constructeur de la classe parente
self.programme = programme

def afficher_info(self):
super().afficher_info() # Appel de la méthode de la classe parente
print(f" Programme: {self.programme}")
# Création d'instances des classes
personne = Personne("Alice", 30)
etudiant = Etudiant("Bob", 25, "Informatique")

# Appel des méthodes pour afficher les informations


print("Informations de la personne :")
personne.afficher_info()

print("\n Informations de l'étudiant :")


etudiant.afficher_info()
3.5. Polymorphisme en Python.

Le polymorphisme en Python est un concept clé de la programmation


orientée objet qui permet à des objets de différentes classes de
répondre de manière cohérente à des opérations similaires, même si
ces objets ont des implémentations spécifiques différentes.

En d'autres termes, le polymorphisme permet à des objets de


différentes classes d'être traités de la même manière s'ils partagent une
interface commune. Le polymorphisme est généralement réalisé grâce
à l'héritage, aux interfaces, à la redéfinition de méthodes et à la
surcharge d'opérateurs.
 Héritage et redéfinition de méthodes : Les classes dérivées (sous-
classes) peuvent redéfinir les méthodes héritées de leurs classes parentes
pour fournir une implémentation spécifique. Lorsque vous appelez une
méthode sur un objet de la classe parente ou de la classe enfant, la
méthode appropriée est exécutée en fonction du type réel de l'objet.

 Interfaces et classes abstraites : Python n'a pas de concept d'interfaces


comme certains autres langages, mais il est courant d'utiliser des classes
abstraites en utilisant le module abc pour définir des méthodes abstraites.
Les classes qui implémentent ces méthodes abstraites peuvent être de
types différents, mais elles peuvent être traitées de la même manière si
elles respectent l'interface.
 Surcharge d'opérateurs : Python permet la surcharge d'opérateurs,
ce qui signifie que des objets de différentes classes peuvent réagir
de manière différente lorsque des opérateurs tels que +, -, *, ==, etc.
sont utilisés sur eux.
Exemple
import math

class Forme:
def aire(self):
pass

class Cercle(Forme):
def __init__(self, rayon):
self.rayon = rayon

def aire(self):
return math.pi * self.rayon ** 2

class Carre(Forme):
def __init__(self, cote):
self.cote = cote

def aire(self):
return self.cote ** 2
class Triangle(Forme):
def __init__(self, base, hauteur):
self.base = base
self.hauteur = hauteur

def aire(self):
return 0.5 * self.base * self.hauteur

# Fonction pour calculer et afficher l'aire de différentes formes


def afficher_aire(forme):
print(f" Aire de la forme : {forme.aire()} unités carrées")

# Utilisation du polymorphisme
cercle = Cercle(5)
carre = Carre(4)
triangle = Triangle(3, 6)

afficher_aire(cercle) # Appel de la méthode aire de Cercle


afficher_aire(carre) # Appel de la méthode aire de Carre
afficher_aire(triangle) # Appel de la méthode aire de Triangle
3.6. Interfaces et classes abstraites.
En Python, les interfaces ne sont pas une caractéristique native du langage,
mais on peut utiliser des classes abstraites pour définir des contrats que les
classes dérivées doivent respecter. Les classes abstraites sont généralement
mises en œuvre en utilisant le module abc (Abstract Base Classes).

 Création d'une interface (classe abstraite) : Vous pouvez créer une


interface en définissant une classe abstraite avec des méthodes
abstraites, c'est-à-dire des méthodes qui n'ont pas d'implémentation.
Pour ce faire, importez le module abc et utilisez le décorateur
@abstractmethod pour marquer les méthodes abstraites.
 Implémentation des méthodes abstraites : Les classes dérivées (sous-
classes) doivent implémenter les méthodes abstraites de l'interface pour
être conformes au contrat.

 Utilisation de l'interface : Vous pouvez créer des objets de sous-classes


qui respectent l'interface définie par la classe abstraite.
Exemple:
from abc import ABC, abstractmethod

# Définition de l'interface (classe abstraite)


class FormeGeometrique(ABC):
@abstractmethod
def calculer_aire(self):
pass

# Implémentation de classes concrètes qui respectent l'interface


class Cercle(FormeGeometrique):
def __init__(self, rayon):
self.rayon = rayon

def calculer_aire(self):
return 3.14159 * self.rayon ** 2
class Carre(FormeGeometrique):
def __init__(self, cote):
self.cote = cote

def calculer_aire(self):
return self.cote ** 2

class Triangle(FormeGeometrique):
def __init__(self, base, hauteur):
self.base = base
self.hauteur = hauteur

def calculer_aire(self):
return 0.5 * self.base * self.hauteur
# Fonction qui calcule et affiche l'aire de n'importe quelle forme géométrique
def afficher_aire(forme):
aire = forme.calculer_aire()
print(f" L'aire de la forme géométrique est : {aire} unités carrées")

# Utilisation de l'interface
cercle = Cercle(5)
carre = Carre(4)
triangle = Triangle(3, 6)

# Appel de la fonction afficher_aire avec différentes formes géométriques


afficher_aire(cercle) # Appel de la méthode calculer_aire de Cercle
afficher_aire(carre) # Appel de la méthode calculer_aire de Carre
afficher_aire(triangle) # Appel de la méthode calculer_aire de Triangle
Exercices
Dans cette série d’exercices nous allons exploiter le fichier retaurant dont
l’exemple d’un document est le suivant :

{'address': {'building': '7114', 'coord': {'type': 'Point', 'coordinates': [-


73.9068506, 40.6199034]}, 'street': 'Avenue U', 'zipcode': '11234'}, 'borough':
'Brooklyn', 'cuisine': 'Delicatessen', 'grades': [{'date': {'$date': 1401321600000},
'grade': 'A', 'score': 10}, {'date': {'$date': 1389657600000}, 'grade': 'A', 'score':
10}, {'date': {'$date': 1375488000000}, 'grade': 'A', 'score': 8}, {'date': {'$date':
1342569600000}, 'grade': 'A', 'score': 10}, {'date': {'$date': 1331251200000},
'grade': 'A', 'score': 13}, {'date': {'$date': 1318550400000}, 'grade': 'A', 'score':
9}], 'name': "Wilken'S Fine Food", 'restaurant_id': '40356483'}

1. Comment pourriez-vous utiliser l'héritage pour créer des classes spécifiques pour différentes catégories
de restaurants (par exemple, "Boulangerie", "Pizzeria", "Restaurant asiatique") tout en partageant
certaines fonctionnalités de base ?
2. Comment pourriez-vous mettre en œuvre le polymorphisme pour garantir que différentes
classes de restaurants partagent une méthode commune, par exemple, pour afficher les détails
du restaurant, tout en permettant à chaque classe de fournir sa propre implémentation si
nécessaire ?

3. Comment utiliseriez-vous l'héritage pour créer une classe de base pour les inspections des
restaurants (à partir des éléments de la liste "grades" dans le dictionnaire) tout en permettant
aux classes dérivées de représenter différentes inspections (par exemple, "InspectionQualite",
"InspectionSécurité") ?

4. Comment mettriez-vous en œuvre le polymorphisme pour garantir que chaque classe


d'inspection peut être traitée de manière cohérente, bien que chaque type d'inspection ait des
attributs et des méthodes spécifiques ?

5. Comment utiliseriez-vous l'héritage pour créer une classe de base pour les coordonnées (à partir
de l'élément "coord" dans le dictionnaire) et permettre à des sous-classes spécifiques de gérer
différents types de coordonnées (par exemple, "CoordonneesPoint", "CoordonneesGPS") tout en
partageant certaines méthodes de base pour afficher les coordonnées ?
Chapitre 4 : Gestion des Exceptions et des Erreurs

4.1. Traitement des erreurs et des exceptions en Python.


4.2. Utilisation des blocs `try`, `except`, `else` et `finally`.
4.3. Création de classes d'exception personnalisées.
4.4. Gestion des erreurs liées à la POO.
4.5. Utilisation du mécanisme de gestion des erreurs pour
améliorer la robustesse des programmes.
4.1. Traitement des erreurs et des exceptions en Python.

Le traitement des erreurs et des exceptions en Python est un


mécanisme qui permet de gérer les situations exceptionnelles ou les
erreurs qui peuvent survenir lors de l'exécution d'un programme.

Les points clés de traitement des erreurs peut inclure plusieurs astuces.
Pour gérer les exceptions, on utilise des blocs try, except, else, et
finally

Le bloc try contient le code susceptible de provoquer une exception et


Le bloc except est utilisé pour spécifier comment traiter une exception
particulière en cas de levée.
Le bloc else est exécuté si aucune exception n'a été levée dans le bloc
try. Le bloc finally est exécuté, que des exceptions aient été levées ou
non, et il est souvent utilisé pour effectuer des opérations de
nettoyage.

On peut utiliser l'instruction raise pour lever manuellement une


exception, ce qui permet de signaler des erreurs spécifiques.

Il est possible aussi de gérer plusieurs types d'exceptions en utilisant


plusieurs clauses except. La gestion des exceptions permet de rendre
le code plus robuste en anticipant et en gérant les erreurs potentielles,
évitant ainsi les arrêts inattendus du programme.
4.2. Utilisation des blocs `try`, `except`, `else` et `finally`.

4.2.1. try :

Le bloc try est utilisé pour encapsuler le code qui peut potentiellement
générer une exception. C'est le bloc où on met le code qu’on souhaite
surveiller pour des erreurs. Si une exception est levée pendant l'exécution
de ce bloc, le contrôle est transféré au bloc except correspondant.

try:
# Code susceptible de provoquer une exception
except ExceptionType:
# Gérer l'exception ici
4.2.2. except :

Le bloc except est utilisé pour gérer une exception spécifique lorsque
elle est levée. Vous spécifiez le type d'exception à gérer après le mot-
clé except.

On peut également avoir plusieurs blocs except pour gérer différents


types d'exceptions.
try:
# Code susceptible de provoquer une exception
except ExceptionType1:
# Gérer l'exception de type 1
except ExceptionType2:
# Gérer l'exception de type 2
4.2.3. else (optionnel) :

Le bloc else est exécuté si aucune exception n'est levée dans le bloc try.
Cela permet d'ajouter du code qui doit être exécuté en l'absence
d'erreurs.
try:
# Code susceptible de provoquer une exception
except ExceptionType:
# Gérer l'exception ici
else:
# Code à exécuter si aucune exception n'est levée
4.2.4. finally (optionnel) :

Le bloc finally est utilisé pour spécifier du code qui doit être exécuté
qu'une exception soit levée ou non.
C'est souvent utilisé pour des opérations de nettoyage, telles que la
fermeture de fichiers ou de connexions réseau.
try:
# Code susceptible de provoquer une exception
except ExceptionType:
# Gérer l'exception ici
else:
# Code à exécuter si aucune exception n'est levée
finally:
# Code à exécuter quoi qu'il arrive
Exemple
try:
# Tentative d'ouverture et de lecture d'un fichier texte
file = open("mon_fichier.txt", "r")
content = file.read()
except FileNotFoundError:
print("Le fichier n'a pas été trouvé.")
except IOError as e:
print(f" Erreur d'E/S : {e}")
else:
print("Lecture du fichier réussie.")
print(f" Contenu du fichier : {content}")
finally:
try:
file.close() # Tentative de fermeture du fichier, même en cas d'erreur
except NameError:
pass # Si le fichier n'a pas pu être ouvert, file n'existe pas

print("Le programme se poursuit après la gestion des erreurs.")


4.3. Création de classes d'exception personnalisées.

En Python, on peut créer des classes d'exception personnalisées en


héritant de la classe de base Exception ou d'une autre classe d'exception
existante.
class MonExceptionPersonnalisee(Exception):
def __init__(self, message="Une exception personnalisée s'est produite"):
self.message = message
super().__init__(self.message)

# Utilisation de la classe d'exception personnalisée


try:
raise MonExceptionPersonnalisee("Ceci est un message d'erreur personnalisé")
except MonExceptionPersonnalisee as e:
print(f" Exception personnalisée attrapée : {e.message}")
L'utilisation de classes d'exception personnalisées est utile lorsqu’on
souhaite signaler des erreurs spécifiques à son application et fournir des
informations détaillées sur ces erreurs.

Cela permet également d'améliorer la lisibilité du code en distinguant les


types d'erreurs de manière plus explicite.

Exemple :

class SoldeInsuffisantException(Exception):
def __init__(self, solde, montant_retrait):
self.solde = solde
self.montant_retrait = montant_retrait
message = f"Solde insuffisant : Le solde actuel est de {solde}, mais
{montant_retrait} a été demandé."
super().__init__(message)
class CompteBancaire:
def __init__(self, solde):
self.solde = solde

def retirer(self, montant):


if montant <= self.solde:
self.solde -= montant
else:
raise SoldeInsuffisantException(self.solde, montant)
# Utilisation de la classe d'exception personnalisée
compte = CompteBancaire(1000)

try:
compte.retirer(1500)
except SoldeInsuffisantException as e:
print(e)

try:
compte.retirer(500)
except SoldeInsuffisantException as e:
print(e)

print("Le programme se poursuit après la gestion des exceptions.")


4.4. Gestion des erreurs liées à la POO.

La gestion des erreurs en programmation orientée objet (POO) en Python


suit les mêmes principes que la gestion des erreurs dans le code classique.
Cependant, il existe quelques considérations spécifiques à la POO.
Comme:

4.4.1. Gestion des erreurs dans le constructeur :

Les constructeurs (__init__ méthodes) sont souvent utilisés pour initialiser


les objets. Si une erreur survient lors de l'initialisation de l'objet, on peut
lever une exception pour signaler l'erreur.
Par exemple, si des valeurs incorrectes sont passées en argument au
constructeur, on peut lever une exception ValueError.
class MonObjet:
def __init__(self, valeur):
if valeur < 0:
raise ValueError("La valeur doit être positive.")
self.valeur = valeur

4.4.2.Gestion des erreurs dans les méthodes :

Les méthodes de classe peuvent également générer des erreurs.


On peut utiliser des assertions (assert) pour vérifier les préconditions, ou
lever des exceptions appropriées si les opérations échouent.
class MonObjet:
def diviser(self, x, y):
assert y != 0, "Division par zéro non autorisée."
return x / y
4.4.3. Gestion des erreurs d'attributs :

Si un attribut n'existe pas sur un objet, une exception AttributeError


est levée. On peut utiliser l'opérateur getattr ou des conditions pour
gérer les attributs manquants.

class MonObjet:
def __init__(self):
self.attribute = 42

obj = MonObjet()
try:
valeur = obj.attribut_inexistant
except AttributeError:
print("L'attribut n'existe pas.")
4.4.4. Gestion des erreurs personnalisées :

On peut créer des exceptions personnalisées qui sont spécifiques à sa


classe pour gérer des erreurs particulières. Héritez de la classe de base
Exception ou d'une autre classe d'exception appropriée.

class MonExceptionPersonnalisee(Exception):
def __init__(self, message):
super().__init__(message)

class MonObjet:
def operation_risquee(self):
if erreur:
raise MonExceptionPersonnalisee("Une erreur s'est produite.")
4.4.5. Utilisation des blocs try, except, else, et finally :

Vous pouvez utiliser ces blocs pour gérer les erreurs liées à la POO
de manière similaire à la gestion des erreurs dans le code traditionnel. Par
exemple, dans le constructeur, vous pouvez placer du code
potentiellement problématique dans un bloc try et gérer les erreurs dans
le bloc except.
class MonObjet:
def __init__(self, valeur):
try:
if valeur < 0:
raise ValueError("La valeur doit être positive.")
self.valeur = valeur
except ValueError as e:
print(f"Erreur : {e}")
4.5. Utilisation du mécanisme de gestion des erreurs pour
améliorer la robustesse des programmes.

En anticipant et en gérant les erreurs de manière appropriée, on peut éviter


des arrêts inattendus et garantir que son programme continue de fonctionner
même en présence d'erreurs.

On peut utiliser ce mécanisme de gestion d’erreur pour améliorer la robustesse


des programmes en Python comme suit:

4.5.1. Anticiper les erreurs :

Avant d'écrire du code, identifiez les endroits où des erreurs pourraient


survenir. Cela peut inclure des erreurs de saisie utilisateur, des
exceptions lors de l'accès à des fichiers, des erreurs de calcul, etc.
1.4.5.2. Utiliser des blocs try/except :

Entourez le code potentiellement problématique avec des blocs try/except


pour intercepter les exceptions. Cela permet au programme de continuer à
s'exécuter plutôt que de planter en cas d'erreur.

4.5.3. Choisir le type d'exception approprié :

Utilisez des exceptions appropriées en fonction du type d'erreur.


Python a de nombreuses exceptions intégrées telles que ValueError,
TypeError, FileNotFoundError, etc. Vous pouvez également créer des
exceptions personnalisées si nécessaire.
4.5.4. Fournir des informations utiles :

Lorsque vous interceptez une exception, fournissez des informations


utiles dans les messages d'erreur. Cela peut aider les utilisateurs ou les
développeurs à comprendre rapidement ce qui s'est mal passé.

4.5.5. Nettoyer les ressources :

Utilisez le bloc finally pour garantir que les ressources telles que
les fichiers sont correctement fermées, quel que soit le résultat. Cela
évite les fuites de ressources
4.5.6. Gérer les erreurs de manière appropriée :

Décidez de la manière dont vous souhaitez gérer les erreurs. Vous pouvez
les ignorer, afficher un message d'erreur, enregistrer les erreurs dans un
journal, ou prendre des mesures spécifiques pour corriger le problème.

4.5.7. Pratiquer la gestion des erreurs défensives :

Les programmes robustes intègrent souvent la gestion des erreurs dès le


début du développement. Utilisez des assertions pour vérifier les
préconditions et assurez-vous que votre programme se comporte de
manière prévisible, même en cas de données incorrectes.
Chapitre 5 : Conception Avancée en POO

5.1. Principes de conception SOLID (Single Responsibility Principle, Open/Closed


Principle, Liskov Substitution Principle, Interface Segregation Principle,
Dependency Inversion Principle).
5.2. Utilisation de diagrammes UML pour la modélisation des classes.
5.3. Design patterns courants en Python (Singleton, Factory, Strategy, Observer,
etc.). 5.4. Mise en œuvre d’un mini projet réel utilisant la POO
5.1. Principes de conception SOLID

Les principes de conception SOLID sont un ensemble de cinq principes


fondamentaux de conception logicielle qui visent à améliorer la
maintenabilité, la flexibilité et la robustesse du code.

Ces principes ont été formulés par Robert C. Martin et sont couramment
utilisés pour guider la conception de logiciels orientés objet.
5.1.1. Principe de Responsabilité Unique (Single Responsibility
Principle - SRP)

Le Principe de Responsabilité Unique (Single Responsibility Principle -


SRP) est le premier des principes SOLID de conception logicielle.

Il stipule qu'une classe ne devrait avoir qu'une seule raison de


changer, c'est-à-dire qu'elle devrait avoir une seule responsabilité
bien définie.

En d'autres termes, une classe ne devrait faire qu'une seule chose et la


faire bien.
Le SRP vise à éviter les classes surchargées de responsabilités
multiples, ce qui les rend fragiles et difficiles à maintenir.

Lorsqu'une classe a plusieurs responsabilités, un changement dans


l'une d'entre elles peut affecter d'autres parties du code, ce qui
augmente le risque d'erreurs et de régressions.
Exemple:
class GestionnaireFichier:
def __init__(self, nom_fichier):
self.nom_fichier = nom_fichier

def lire_fichier(self):
with open(self.nom_fichier, 'r') as fichier:
contenu = fichier.read()
return contenu

def analyser_contenu(self):
contenu = self.lire_fichier()
lignes = contenu.split('\n')
nombre_lignes = len(lignes)
print(f" Le fichier contient {nombre_lignes} lignes.")

# Utilisation de la classe
gestionnaire = GestionnaireFichier('exemple.txt')
gestionnaire.analyser_contenu()
Dans exemple précédant, la classe GestionnaireFichier a deux
responsabilités : lire un fichier et analyser son contenu.

Cela rend la classe moins maintenable, car un changement dans l'une de


ces responsabilités peut avoir un impact sur l'autre. Pour respecter le SRP,
nous devrions séparer ces responsabilités en deux classes distinctes.

Par exemple, nous pourrions avoir une classe LecteurFichier pour lire le
fichier et une classe AnalyseurContenu pour analyser son contenu.

Cela rendrait le code plus propre, plus modulaire et plus facile à maintenir,
en suivant le principe de base du SRP.
class LecteurFichier:
def __init__(self, nom_fichier):
self.nom_fichier = nom_fichier

def lire_fichier(self):
with open(self.nom_fichier, 'r') as fichier:
contenu = fichier.read()
return contenu

class AnalyseurContenu:
def __init__(self, contenu):
self.contenu = contenu

def analyser_nombre_lignes(self):
lignes = self.contenu.split('\n')
nombre_lignes = len(lignes)
return nombre_lignes
# Utilisation des classes
lecteur = LecteurFichier('exemple.txt')
contenu = lecteur.lire_fichier()

analyseur = AnalyseurContenu(contenu)
nombre_lignes = analyseur.analyser_nombre_lignes()

print(f" Le fichier contient {nombre_lignes} lignes.")


5.1.2. Principe Ouvert/Fermé (Open/Closed Principle - OCP)

Le principe OCP stipule que les entités logicielles (comme les classes, les
modules ou les fonctions) doivent être ouvertes à l'extension mais fermées
à la modification.

En d'autres termes, une classe (ou un module) devrait pouvoir être


étendue pour ajouter de nouvelles fonctionnalités sans avoir à modifier
son code source existant.
Les principaux objectifs du principe OCP sont les suivants :
Extension : Il devrait être possible d'ajouter de nouvelles fonctionnalités
au système sans modifier le code source existant. Cela favorise la
réutilisation du code et la facilité d'extension.

Fermeture : Le code source existant, une fois testé et validé, ne doit pas
être modifié, car cela peut introduire des bogues ou des régressions.

Pour respecter le Principe OCP, les concepteurs de logiciels utilisent


généralement des techniques telles que l'héritage, les interfaces, la
composition et la polymorphie pour étendre le comportement existant
sans modifier le code source original.
Exemple:
class Forme:
def aire(self):
pass

class Rectangle(Forme):
def __init__(self, largeur, hauteur):
self.largeur = largeur
self.hauteur = hauteur

def aire(self):
return self.largeur * self.hauteur

class Cercle(Forme):
def __init__(self, rayon):
self.rayon = rayon

def aire(self):
return 3.14159 * self.rayon * self.rayon
Dans l’exemple précèdent, la classe Forme est ouverte à l'extension, car
nous pouvons ajouter de nouvelles sous-classes (comme Rectangle et
Cercle) pour étendre son comportement sans modifier la classe Forme
elle-même.

Cette approche respecte le Principe OCP en permettant l'extension sans


modification. En pratique, le Principe OCP favorise la création de
systèmes plus flexibles, extensibles et faciles à maintenir.

Il encourage également la réutilisation du code existant, ce qui peut


réduire le temps et les efforts nécessaires pour développer de nouvelles
fonctionnalités
5.1.3. Principe de Substitution de Liskov (Liskov Substitution Principle -
LSP)

Le Principe de Substitution de Liskov (Liskov Substitution Principle - LSP) est l'un


des principes SOLID de conception logicielle formulés par Barbara Liskov.

Ce principe stipule que les objets de sous-classes doivent pouvoir être utilisés
de manière interchangeable avec des objets de la classe de base sans altérer la
cohérence du programme.

En d'autres termes, si une classe S est une sous-classe d'une classe T, alors un
objet de la classe T peut être remplacé par un objet de la classe S sans que cela
entraîne des erreurs ou une violation des contrats (comportements) établis par
la classe de base.
Les principaux objectifs du principe LSP sont les suivants :

- Compatibilité : Les sous-classes doivent être compatibles avec les


classes de base, ce qui signifie qu'elles doivent respecter les mêmes
contrats (spécifications) que la classe de base.

- Extension : Les sous-classes peuvent étendre le comportement de la


classe de base, mais elles ne doivent pas réduire ni supprimer les
fonctionnalités de la classe de base.
Pour respecter le Principe LSP, les concepteurs de logiciels doivent veiller à
ce que les sous-classes héritent et respectent les contrats de la classe de
base, tout en ajoutant des fonctionnalités supplémentaires si nécessaire.

Cela garantit que les sous-classes peuvent être utilisées de manière


interchangeable avec les classes de base sans perturber le
fonctionnement du programme.
class Oiseau:
def voler(self):
pass

class Aigle(Oiseau):
def voler(self):
return "L'aigle vole très haut."

class Pingouin(Oiseau):
def voler(self):
return "Le pingouin ne peut pas voler, il nage."

Dans cet exemple, les sous-classes Aigle et Pingouin redéfinissent la


méthode voler de la classe de base Oiseau.
Cependant, elles le font de manière compatible avec le contrat de la classe
de base.
Un objet de type Aigle et un objet de type Pingouin peuvent être
utilisés de manière interchangeable avec un objet de type Oiseau sans
altérer la cohérence du programme.

Le Principe LSP favorise la création de hiérarchies de classes bien


conçues, où les sous-classes ajoutent de la valeur tout en respectant les
contrats de la classe de base.

Cela permet une meilleure extensibilité et une utilisation plus flexible


des objets dans un programme.
5.1.4. Principe de Ségrégation d'Interfaces (Interface Segregation
Principle - ISP)

Le Principe de Ségrégation d'Interfaces (Interface Segregation Principle -


ISP) vise à guider la conception des interfaces en évitant les interfaces
monolithiques qui obligent les classes à implémenter des méthodes
qu'elles n'utilisent pas.

Le principe ISP se concentre sur la création d'interfaces spécifiques aux


besoins des classes qui les utilisent, plutôt que de créer une seule
interface générale pour tous.
Les principaux objectifs du principe ISP sont les suivants :

Séparation des préoccupations : Les interfaces doivent être conçues de


manière à séparer les préoccupations en fonction des besoins des classes
qui les implémentent. Cela permet d'éviter la surcharge de responsabilités
pour les classes.

Minimisation des dépendances : Les classes ne doivent pas être


contraintes de dépendre de méthodes qu'elles n'utilisent pas. Cela réduit
le couplage entre les composants logiciels.

Facilité d'extension : Les interfaces doivent être extensibles pour


permettre l'ajout de nouvelles fonctionnalités sans affecter les classes
existantes.
Voici un exemple simplifié pour illustrer le Principe ISP :

Supposons que nous ayons une interface Appareil qui représente des
appareils électroniques et qui contient trois méthodes : allumer, éteindre
et réparer.
from abc import ABC, abstractmethod

class Appareil(ABC):
@abstractmethod
def allumer(self):
pass

@abstractmethod
def éteindre(self):
pass

@abstractmethod
def réparer(self):
pass
Cependant, toutes les classes d'appareils électroniques ne nécessitent pas toutes ces
méthodes. Par exemple, une lampe ne peut pas être réparée. En suivant le Principe ISP,
nous devrions séparer ces méthodes en interfaces distinctes pour les rendre plus
spécifiques aux besoins des classes qui les implémentent :
from abc import ABC, abstractmethod

class Allumable(ABC):
@abstractmethod
def allumer(self):
pass

@abstractmethod
def éteindre(self):
pass

class Réparable(ABC):
@abstractmethod
def réparer(self):
pass
Maintenant, une classe Lampe peut implémenter l'interface Allumable
et une classe Ordinateur peut implémenter les interfaces Allumable et
Réparable, ce qui respecte le Principe ISP en offrant des interfaces
spécifiques aux besoins de chaque classe.

Le Principe ISP favorise la création d'interfaces modulaires et spécifiques,


ce qui améliore la maintenabilité, la flexibilité et la clarté du code.
Il réduit également les dépendances inutiles entre les classes, ce qui
facilite l'extension et la maintenance du logiciel.
5.1.5. Principe d'Inversion de Dépendance (Dependency Inversion
Principle - DIP)

Le principe DIP vise à réduire le couplage entre les modules ou les


composants logiciels en inversant la direction des dépendances.

Contrairement à la dépendance directe, où un module dépend d'un autre


module de niveau inférieur, le DIP préconise une dépendance sur une
abstraction ou une interface commune.
Le principe DIP est basé sur deux concepts clés :

- Haute et basse dépendance :

Les modules de niveau supérieur (comme les modules de haut niveau) ne


doivent pas dépendre des modules de niveau inférieur (comme les modules
de bas niveau).

Les deux niveaux devraient plutôt dépendre d'abstractions. Les détails de


l'implémentation doivent dépendre des abstractions, pas l'inverse.
- Abstractions et interfaces :

Le DIP recommande l'utilisation d'abstractions ou d'interfaces pour


représenter des contrats communs que les modules peuvent suivre. Cela
permet de créer une couche d'abstraction entre les modules de haut niveau
et de bas niveau.

Le DIP encourage également la séparation des préoccupations en évitant


que les modules de haut niveau n'aient besoin de connaître les détails de
mise en œuvre des modules de bas niveau.

Il favorise la flexibilité, la réutilisation et la maintenance du code, car les


composants peuvent être facilement échangés ou modifiés sans affecter le
reste du système.
Supposons que nous ayons une classe LecteurFichier qui lit des fichiers.
Au départ, cette classe dépend directement d'une classe de bas niveau
LecteurDisque pour lire les fichiers.

class LecteurDisque:
def lire_fichier(self, chemin):
# Logique de lecture du fichier depuis le disque
pass

class LecteurFichier:
def __init__(self, lecteur_disque):
self.lecteur_disque = lecteur_disque

def lire_fichier(self, chemin):


return self.lecteur_disque.lire_fichier(chemin)
Cependant, cela enfreint le DIP, car la classe LecteurFichier dépend
directement de la classe de bas niveau LecteurDisque. Pour respecter le
DIP, nous pouvons introduire une abstraction sous la forme d'une
interface LecteurAbstrait

Dans l’exemple suivant , la classe LecteurFichier dépend d'une


abstraction (LecteurAbstrait) au lieu de dépendre directement de la
classe LecteurDisque.

Cette inversion de dépendance respecte le Principe DIP en réduisant le


couplage entre les classes et en permettant une plus grande flexibilité
lors de l'ajout ou du remplacement de lecteurs de fichiers.
from abc import ABC, abstractmethod

class LecteurAbstrait(ABC):
@abstractmethod
def lire_fichier(self, chemin):
pass

class LecteurDisque(LecteurAbstrait):
def lire_fichier(self, chemin):
# Logique de lecture du fichier depuis le disque
pass

class LecteurFichier:
def __init__(self, lecteur):
self.lecteur = lecteur

def lire_fichier(self, chemin):


return self.lecteur.lire_fichier(chemin)
5.2. Utilisation de diagrammes UML pour la modélisation des classes.

L'utilisation de diagrammes UML (Unified Modeling Language) pour la


modélisation des classes est courante dans le développement logiciel.

Les diagrammes de classe UML permettent de représenter visuellement la


structure et les relations entre les classes d'un système. Cfr le cours de Génie
logiciel.
Python et BD Mongo
Pour interagir avec MongoDB en Python, on a besoin de la bibliothèque
officielle de MongoDB appelée "pymongo".

# Installation de pymongo
pip install pymongo
pip install — upgrade httpcore

# Connexion à MongoDB
import pymongo
# Connexion à la base de données

client = pymongo.MongoClient("mongodb://localhost:27017/")
db = client["nom_de_base_de_donnees"]
On doit s’assurer que MongoDB est en cours d'exécution sur la machine locale sur le
port par défaut (27017). Modifiez l'URL de connexion en conséquence si nécessaire.

Création de la base de données, insertion, mise à jour et suppression de


données

# Création de la collection

collection = db["nom_de_votre_collection"]

#Insertion de données
# Exemple d'insertion d'un document
data_to_insert = {
"nom": "John Doe",
"age": 30,
"ville": "Paris"
}
result = collection.insert_one(data_to_insert)
print(f"ID du document inséré : {result.inserted_id}")
#Recherche de données

# Recherche de tous les documents dans la collection

for document in collection.find():


print(document)

# Exemple d'insertion d'un document

data_to_insert = {
"nom": "John Doe",
"age": 30,
"ville": "Paris"
}

result = collection.insert_one(data_to_insert)
print(f "ID du document inséré : {result.inserted_id}")
# Recherche de données

# Recherche de tous les documents dans la


collection
for document in collection.find():
print(document)

# Mise à jour de données

# Mise à jour d'un document


query = {"nom": "John Doe"}
new_data = {"$set": {"ville": "Lyon"}}
collection.update_one(query, new_data)
# Suppression de données

# Suppression d'un document


delete_query = {"nom": "John Doe"}
collection.delete_one(delete_query)

Vous aimerez peut-être aussi