Les paradigmes de programmation
La programmation représente le processus par lequel un développeur utilise une syntaxe spécifique, des structures de données et des algorithmes pour indiquer à un ordinateur comment exécuter une tâche. Un programme constitue un ensemble d'instructions créées pour obtenir un résultat particulier.
Il existe différentes manières d'accomplir une même tâche. La categorization des caractéristiques de ces approches constitue ce qu'on appelle un paradigme de programmation. Chaque paradigme définit une approche différente pour résoudre des problèmes. La plupart des langages supportent un seul paradigme, bien que certains langages permettent d'en combiner plusieurs.
Les deux paradigmes fondamentaux sont la programmation procédurale et la programmation orientée objet.
1. Programmation procédurale
La programmation procédurale utilise une liste d'instructions que l'ordinateur exécute séquentiellement. Ce paradigme repose sur des procédures contenant des étapes de calcul. Ces programmes s'exécutent de haut en bas, divisant un problème complexe en sous-problèmes plus simples jusqu'à ce qu'ils puissent être résolus en quelques opérations.
Prenons l'exemple d'une sauvegarde de base de données en trois étapes : connexion, sauvegarde et vérification.
def connecter_bd():
print("Connexion à la base de données...")
def sauvegarder_bd(nom_bd):
print(f"Exportation de la base {nom_bd}...")
print("Compression et déplacement vers le répertoire cible...")
def tester_sauvegarde():
print("Importation dans la base de test pour vérification")
def principaux():
connecter_bd()
sauvegarder_bd('ma_bd')
tester_sauvegarde()
if __name__ == '__main__':
principaux()
L'inconvénient majeur de cette approche apparaît lors des modifications. Si une variable initiale change, toutes les parties dépendant de cette valeur doivent être adaptées. Cette dépendance linéaire rend la maintenance de plus en plus complexe au fur et à mesure que le programme grandit.
Cependant, pour des scripts simples ou des tâches uniques, la programmation procédurale reste appropriée. Pour des projets complexes nécessitant évolutions et maintenance, la approche orientée objet s'avère plus adaptée.
2. Programmation orientée objet
2.1 Concepts fondamentaux
La programmation orientée objet utilise des classes et des objets pour modéliser le monde réel. Cette approche simplifie la maintenance et l'extension des programmes tout en améliorant l'efficacité du développement. Le code devient également plus compréhensible pour les autres développeurs, facilitant le travail d'équipe.
Les caractéristiques essentielles de la POO comprennent :
Classe : Une classe constitue le plan ou le原型 d'un ensemble d'objets partageant des attributs et des méthodes communs.
Objet : Un objet représente une instance concrète d'une classe. Une classe doit être instanciée pour être utilisée, et peut générer plusieurs objets avec des attributs différents.
Encapsulation : Les données et les opérations internes d'une classe sont transparentes pour l'utilisateur extérieur. La classe fonctionne comme un contenant regroupant les données et les méthodes.
Héritage : Une classe peut générer des sous-classes qui héritent automatiquement des attributs et méthodes de la classe parente.
Polymorphisme : Cette caractéristique permet à une même interface de comportements différents. Une classe de base peut être spécialisées en plusieurs sous-classes, chacune implémentant différemment les méthodes héritées.
Le polymorphisme permet d'abstraire les caractéristiques communes de plusieurs objets concrets. Un même message envoyé à différents objets produit des comportements distincts. Par exemple, un directeur ordonnant à tous ses employés de commencer à travailler n'a pas besoin de différencier les commerciaux des techniciens. Seul compte que ce soient des employés.
<![CDATA[class Chien:
"""Simulation simplifiée d'un chien"""
def __init__(self, nom):
self.nom = nom
def aboyer(self):
print(f"{self.nom} : ouaf ouaf ouaf....")
c1 = Chien("minette")
c2 = Chien("bilou")
c3 = Chien("roky")
c1.aboyer()
c2.aboyer()
c3.aboyer()
]]>
L'utilisation de classes rend la création d'instances très pratique.
Principes essentiels
Tout dans l'univers peut être classé. Tout peut être traité comme un objet. Chaque objet appartient à une catégorie et possède des attributs.
Quel que soit le style de programmation adopté, deux principes doivent guider le développement :
- Éviter la duplication du code
- Prévoir la modification du code
Les programmes professionnels évoluent constamment, nécessitant des corrections ou des fonctionnalités nouvelles. Le code doit donc être lisible et extensible.
Copier-coller du code à plusieurs endroits pose problème : toute modification ultérieure exige de corriger chaque occurrence. Les fonctions permettent de résoudre ce problème en centralisant le code réutilisable.
Exemple pratique : développement d'un jeu
Imaginons un jeu de type Counter-Strike simplifié. Nous avons besoin de deux camps : terroristes et forces de l'ordre. Chaque personnage possède des attributs communs comme les points de vie et l'arme.
Approche procédurale avec variables indépendantes :
<![CDATA[# Premier personnage
nom = 'Alex'
role = 'terroriste'
arme = 'AK47'
points_vie = 100
# Deuxième personnage
nom2 = 'Jack'
role2 = 'policier'
arme2 = 'B22'
points_vie2 = 100
]]>
Cette méthode devient rapidement ingérable. Ajoutons quatre personnages :
<![CDATA[# Personnage 1
nom = 'Alex'
role = 'terroriste'
arme = 'AK47'
points_vie = 100
argent = 10000
# Personnage 2
nom2 = 'Jack'
role2 = 'policier'
arme2 = 'B22'
points_vie2 = 100
argent2 = 10000
# Personnage 3
nom3 = 'Rain'
role3 = 'terroriste'
arme3 = 'C33'
points_vie3 = 100
argent3 = 10000
# Personnage 4
nom4 = 'Eric'
role4 = 'policier'
arme4 = 'B51'
points_vie4 = 100
argent4 = 10000
]]>
L'utilisation de dictionnaires améliore légèrement la situation :
<![CDATA[personnages = {
1: {'nom': 'Alex', 'role': 'terroriste', 'arme': 'AK47', 'points_vie': 100, 'argent': 15000},
2: {'nom': 'Jack', 'role': 'policier', 'arme': 'B22', 'points_vie': 100, 'argent': 15000},
3: {'nom': 'Rain', 'role': 'terroriste', 'arme': 'C33', 'points_vie': 100, 'argent': 15000},
4: {'nom': 'Eric', 'role': 'policier', 'arme': 'B51', 'points_vie': 100, 'argent': 15000},
}
print(personnages[1])
print(personnages[2])
]]>
Plusieurs limitations persistent : absence de validation des attributs, impossibilité de différencier les fonctionnalités selon le rôle, et absence de contrôle sur l'accès aux données.
Implémentation orientée objet
<![CDATA[class Role:
def __init__(self, nom, role, arme, points_vie=100, argent=15000):
self.nom = nom
self.role = role
self.arme = arme
self.points_vie = points_vie
self.argent = argent
def tirer(self):
print("Tir en cours...")
def estre_touche(self):
print("Aïe... J'ai été touchée...")
def acheter_arme(self, nom_arme):
print(f"Achat de {nom_arme}")
p1 = Role('Alex', 'policier', 'AK47')
p2 = Role('Jack', 'terroriste', 'B22')
]]>
Les avantages immédiats sont évidents : réduction du code et meilleure lisibilité permettant d'identifier rapidement les attributs et méthodes de chaque personnage.
2.2 Syntaxe des classes
<![CDATA[class Animal:
print("Bonjour, je suis un animal!")
a = Animal()
]]>
Pour ajouter des paramètres configurables, nous utilisons la méthode __init__ :
<![CDATA[class Chien:
def __init__(self, nom, race):
self.nom = nom
self.race = race
def saluer(self):
print(f"Bonjour, je suis un chien, mon nom est {self.nom}")
c = Chien("Medor", "Berger")
c.saluer()
]]>
Le paramètre self représente l'instance elle-même. Lors de l'instanciation, Python transmet automatiquement l'instance comme premier argument.
<![CDATA[class Role:
def __init__(self, nom, role, arme, points_vie=100, argent=15000):
self.nom = nom
self.role = role
self.arme = arme
self.points_vie = points_vie
self.argent = argent
p1 = Role('Alex', 'policier', 'AK47')
p2 = Role('Jack', 'terroriste', 'B22')
]]>
La méthode __init__ s'exécute automatiquement lors de la création d'une instance. Python appelle cette méthode en lui passant l'instance comme premier argument.
<![CDATA[# Création de personnage
# python exécute en réalité : Role.__init__(p1, 'Alex', 'policier', 'AK47')
# self.nom = nom lie les données à l'instance
]]>
Les méthodes d'une classe accèdent aux attributs via self :
<![CDATA[class Role:
def __init__(self, nom, role, arme, points_vie=100, argent=15000):
self.nom = nom
self.role = role
self.arme = arme
self.points_vie = points_vie
self.argent = argent
def acheter_arme(self, nom_arme):
print(f"{self.nom} a acheté {nom_arme}")
p1 = Role('Alex', 'policier', 'AK47')
p1.acheter_arme("B21")
# Équivalent à : Role.acheter_arme(p1, "B21")
]]>
Synthèse
L'instanciation d'une classe transforme un plan abstrait en un objet concret. L'encapsulation, première caractéristique de la POO, permet de regrouper les données et les comportements au sein d'une même entité.
Introduction à la programmation fonctionnelle
Les fonctions constituent le mécanisme d'encapsulation natif de Python. En décomposant un problème complexe en fonctions plus simples, nous pratiquons la programmation procédurale. Les fonctions servent d'unités de base dans cette approche.
La programmation fonctionnelle, bien que relevant du paradigme procédural, s'apparente davantage au calcul scientifique.
Au niveau matériel, le processeur exécute des instructions arithmétiques et logiques. Le langage assembleur se rapproche le plus de la machine. Le calcul au sens mathématique s'éloigne davantage du hardware.
Les langages de bas niveau comme le C sont plus proches de la machine avec une efficacité d'exécution élevée. Les langages de haut niveau comme Lisp offrent une abstraction plus importante mais une execution moins rapide.
La programmation fonctionnelle représente un paradigme à haut niveau d'abstraction. Un langage fonctionnel pur n'utilise pas de variables : une fonction avec les mêmes entrées produit toujours les mêmes sorties, on parle alors de fonction sans effet secondaire. Les langages permettant l'utilisation de variables introduisent des états internes variables, produisant des effets secondaires.
Python supporte partiellement la programmation fonctionnelle. Comme Python autorise l'utilisation de variables, il ne constitue pas un langage fonctionnel pur.