Object Programming Paradigm in a nutshell
Comme on a pu le voir au début de ce document, en python tout est objet
History
Introducing OOP concept and vocabulary
About Class and Objects
Pour rappel, la Programmation Orientée Objet / POO (Object Oriented Programming en anglais) est un paradigme de programmation qui passe par une organisation des données particulière. Depuis son invention dans les années 1980, ce paradigme domine largement dans l'industrie informatique.
Nous n'avons que très peu de temps pour aborder les concepts théorique en regard avec la POO. Sachez toutefois que vous allez manipuler les concepts théoriques lors des cours de modélisation à l'ENSG et que l'apprentissage de ceux ci vous aideront autant pour la représentation de vos problèmes (avec un langage de description de données comme UML par exemple) en base de données, que pour leur traduction en programme informatique Je n'insisterai donc pas sur les détails théorique dans ce cours, et vous pouvez vous référez aux ressources dessous pour en savoir plus.
L'intérêt de ce paradigme, vous allez voir, et qu'il s'accorde beaucoup mieux à une représentation complexe de la réalité par rapport à que ce que nous avons vu jusqu'à présent.
Vous verrez lors du cours de modélisation que le vocabulaire et les concepts généraux vont se recouper avec ce que nous allons voir ici. Seul le niveau d'abstraction utilisé pour décrire votre problème rendra plus ou moins difficile une future traduction informatique / base de données.
Trop de POO tue la POO
Attention toutefois à ne pas vouloir trop vite coller au langage informatique, car il est très difficile de couvrir correctement la description d'un problème en restant à un niveau d'abstraction trop bas (c'est à dire proche de la machine). Repensez à notre résolution de labyrinthe, et voyez comment l'apprentissage de python à modifié votre perception globale du problème. Il y'aura donc un avant et un après votre formation, et il vous faudra régulièrement savoir jongler entre ces différents niveaux d'abstraction pour être efficace dans la discussion, qu'elle soit avec un client ou avec un développeur informatique !
Un Objet est une structure de donnée qui va nous permettre d'organiser nos données selon un certain schéma:
- autour de la descriptions de ces données (critère descriptif)
- et des moyens de traiter ces données (dynamique).
Prenons par exemple un exemple concret : vous même.
A priori vous êtes un humain, et normalement vous partagez un certain nombre de descripteurs ou attributs avec vos autres congénères humains :
- Deux yeux
- Deux bras
- Deux jambes
- Une couleur de cheveux
- Une couleur pour les yeux
- Une couleur de peau
- etc.
Là où je veux en venir, c'est que si nous devions gérer des humains dans un programme classique tel que nous les avons fait jusqu'à présent, il nous faudrait autant de variables décrites ci dessous que de personnes. Sachant qu'une variable doit être unique, imaginez le bazar :
#Gérard
couleurYeuxGerard = "brun"
couleurCheveuxGerard = "brun"
couleurDePeauGerard = "blanche"
nombreJambeDeGerard = 2
nombreOeilDeGerard = 2
nombreBrasDeGerard = 2
#Paul
couleurYeuxPaul = "vert"
couleurCheveuxPaul = "brun"
couleurDePeauPaul = "blanche"
nombreJambeDePaul = 2
nombreOeilDePaul = 2
nombreBrasDePaul = 2
Bon, et maintenant si je doit gérer la classe entière de carthagéo avec ce modèle de représentation de données, il va me falloir un peu de patience...
En plus, vous avez du le remarquer, il y a de nombreuses données redondantes, pourtant nécessaires, car Gérard pourrait bien n'avoir qu'un oeil, une jambe et travailler sur un bateau après tout.
-
Premier constat, il existe une matrice commune qui nous relie, l'espèce humaine.
-
Deuxième constat, d'un point de vue de l'informaticien, il est possible de trouver une matrice originelle à pas mal de choses dans ce monde. Pensez à la fabrication en série, et à l'invention de la reproduction mécanisé : Voiture, Maison, Avion, Animaux, Porte, Chaise, Chanson, SérieTV, Acteur, Réalisateur, Pompier, etc.
Et c'est à partir de cette matrice originelle que nous allons pouvoir généraliser, ou spécialiser un certain nombre de choses à l'aide de deux choses : des attributs et des fonctions.
Oui, différencier la couleur des yeux ou des cheveux en instanciant notre matrice originelle (c'est à dire en produisant un objet reprenant et complétant le plan definis par la matrice originelle) est un bon début, mais si par exemple, je veux aller plus loin et différencier vraiment les humains entre eux, et la manière dont ils interagissent entre eux,il faut que je m'intéresse non seulement aux aspects statique mais également dynamique.
Par exemple, dans le contexte d'une université (la description de vos données dépend donc beaucoup du contexte du problème !), nous voyons qu'une sous-spécialisation de l'être humain générique est tout à fait possible, car dans son interaction avec l'université un étudiant n'a pas tout à fait les même fonctions ni les même droits qu'un professeur, et cela bien que les deux soient des humains !!
Cette matrice originelle est ce que l'on appelle une Classe, elle définit à la fois :
- des critères descriptifs ou attributs,
- ainsi que des fonctions ou interfaces permettant de communiquer avec les autres objets de ce monde.
Instancier une classe revient à définir un ou plusieurs objets qui dérivent de cette classe.
Pour reprendre notre exemple, Gérard et Paul sont deux instances de la matrice originelle Humain.
Mais si Gérard est professeur, et Paul étudiant, et que nous voulons les différencier dans notre programme, alors il nous faudra créer quelque chose qui spécialise encore un peu plus notre Humain, par exemple en définissant :
- une classe
Etudiant(qui possède un numéro étudiant par exemple), - et une classe
Professeur(qui possède lui d'autres attributs administratif spécifique).
Par chance avec la POO nous pourrons également composer les classes entres elles, car un étudiant est un humain, et un professeur est un humain également !
Il est donc tout à fait possible d'établir une hierarchie structurant un peu plus notre programme pour la gestion d'une université, en adoptant soit un héritage , soit une composition entre les classes : Etudiant et Professeur contiennent les attributs d'un être humain, mais aussi des attributs (statique) et des fonctions (dynamique) qui leurs sont spécifiques.
Writing Classes and Objects
En python une classe est définit par le mot clef class et un bloc d'instruction clos avec un début et une fin, comme pour une fonction, une boucle, une condition, etc.
La seule différence avec une fonction, c'est qu'une classe embarque avec elle des fonctions, et des variables (qui représentent les attributs) qui sont caractéristique de la classe que l'on veut représenter.
class Humain(object): # (1)
nbYeux = 2 # (2)
nbBras = 2
nbJambes = 2
def marche(self): # (3)
print( "Je marche !")
#... traitement ...
#fin du bloc classe
- Par convention les classes démarrent avec une majuscule, et le mot clé
objectentre parenthèse est obligatoire - Les variables définies ici correspondent aux attributs de notre classe
- Les fonctions en rapport avec la classe sont définies dans le corps de la classe. Nous verrons par la suite qu'elles peuvent accéder direcement aux attributs de la classe. Seule spécificité comparé à une fonction normale, le mot clef
selfest obligatoire en début de toute vos fonctions.
Pour instancier une classe, donc créer des objets à partir de cette matrice originelle
gerard = Humain() # (1)
paul = Humain () # (2)
print (gerard) # (3)
print (gerard.nbBras) # (4)
print (paul.marche()) # (5)
- A partir de la matrice originelle, on crééé un objet unique dont la référence est relié à la variable gerard
- A partir de la matrice originelle, on créé un nouvel objet unique dont la référence est relié à la variable paul
- Cela nous renvoie à la référence de l'objet, que l'on peut donc stocker, puis rapeller par la suite ! (voir l'exemple des listes qui contiennent des variables pointant sur des listes)
- On peut récupérer la valeur des attributs de notre objet à l'aide l'opérateur
. - De la même façon que nous avons appelé un attribut, nous pouvons également appelé une fonction si elle existe, en utilisant l'opérateur
.suivi du nom de la fonction et de parenthèses()(qui peuvent contenir des arguments comme n'importe quelle fonction ...).
A présent j'aimerais pouvoir modifier les attributs, pour que le nombre de bras, ou de jambes puissent être différents selon les personnes !
Pour cela il faut que j'utilise un constructeur, en fait il s'agit d'une fonction automatiquement appelé à la création de l'objet
Il s'agit de la fonction __init()__ qui prend automatiquement l'argument self qui est une auto-référence désignant l'objet.
self doit être indiqué comme argument dans chacune des fonctions de la classe, c'est obligatoire, c'est ce qui permet à Python de savoir a qui vous faite référence, donc ici à l'objet même !
Depuis les fonctions internes à l'objet, si vous devez faire référence à :
- un attribut de votre classe il faut indiquer
self.nomDeVotreAttribut - une fonction de votre classe avec le mot clef self devant
self.nomDeVotreFonction()
Store and Manipulate Objects
Les Objets, tout comme les chiffres, les caractères, les listes, etc. peuvent également être stockés à la fois dans des variables simple mais aussi comme des éléments dans des collections : listes, dictionnaires, etc.
class Personne(object):
def __init__(self,nom,prenom,age):
self.nom = nom
self.prenom = prenom
self.age = age
self.friends = []
def anniversaire(self):
print( "Bon anniversaire ", self.nom, " !!")
self.age = self.age + 1
def information(self):
print( "Mon nom est ", self.nom, " et mon prénom est ", self.prenom)
print( "Aujourd'hui j'ai ", self.age, " ans.")
def newFriends(self, newFriend):
self.friends.append(newFriend)
# Creation de deux objets a partir de la classe Personne
tomy = Personne("Ungerer","Tomy",75)
gerard = Personne("Mulot","Gerard",55)
# etc...
# On stocke ces objets dans une liste
listePersonne = [tomy,gerard]
# On appelle des fonctions sur les objets contenus dans cette liste
for i in listePersonne:
i.information()
for i in listePersonne:
i.anniversaire()
for i in listePersonne:
i.information()
OOP in the wild, case of Geomatics
QGIS est écrit en C++ mais la plupart des classes sont accessibles via un binding Python. C'est pourquoi vous allez trouver de la documentation dans les deux langages.
Comme nous pouvons nous y attendre pour un programme aussi complexe, le nombre de classes disponibles est important.
Celle-ci sont classés par grandes familles, ce qui permet déjà d'opérer une première navigation avec l'onglet Topics du site. Sauf à vouloir modifier l'interface graphique, par exemple pour créer un plugin QGis, alors nous n'avons pas besoin de toute la partie qui concerne l'usage de la librairie QT. Idem pour la partie Server, ou 3D. Par contre la partie Analysis ou Core peuvent nous intéresser.
Il y a plusieurs façon d'opérer du code Python avec QGIS, avec parfois la manipulation de classes qui peuvent être différentes. Tout dépend donc de votre objectif en terme d'automatisation, les scripts Python pouvant être décrit et/ou appelé :
- soit directement dans QGIS :
- en utilisant la console Python, ce qui revient à utiliser QGIS de façon programmatique, un peu comme le model builder mais avec du code au lieu des boites et des flèches.
- dans un plugin
- dans un algorithme de processing qui étend la classe
QgsProcessingAlgorithm - au démarrage de qgis dans un script
- soit en mode headless, c'est à dire sans interface graphique, en appelant des algorithmes de processing directement depuis la ligne de commande avec qgis_process
- soit comme une application standalone avec
QgsApplication, utilisable avec ou sans interface graphique.
Devant ce foisonnement il est important de savoir lire une documentation, un diagramme de classe, tel qu'on peut le trouver très communément dès lors qu'on accède à une API de logiciels.
Exemple de QgsMapLayer :
Ici on comprend que les classes permettant d'instancier les objets que l'on connait bien (Vecteur avec QgsVectorLayer, Raster avec QgsRasterLayer, etc.) sont hérités d'une seule même Classe QgsMapLayer.
Note
L'héritage est un concept que nous détaillerons plus loin dans ce cours, l'essentiel ici c'est de retenir que les attributs et les fonctions définis dans QgsMapLayer sont accessibles dans QgsVectorLayer car cette dernière est en réalité une spécialisation de QgsMapsLayer ajoutant de nouvelles fonctions.
Si on regarde dans l'API QGIS, on retrouve la documentation qui correspond à la classe QgsVectorLayer dans l'API Python.
L'API officielle est relativement pauvre en diagramme, mais l'excellent cours PyQgis de Marie-Dominique Van Damme à l'ENSG (disponilble ici) en donne une vision plus détaillé.
Par exemple pour cette même classe :
Ce qui donne par exemple le code suivant :
import os # This is is needed in the pyqgis console also
from qgis.core import (
QgsVectorLayer
)
path_to_airports_layer = "testdata/airports.shp"
vlayer = QgsVectorLayer(path_to_airports_layer, "Airports layer", "ogr")
if not vlayer.isValid():
print("Layer failed to load!")
else:
QgsProject.instance().addMapLayer(vlayer)
Warning
Partie en cours de construction
- https://qgis.org/pyqgis/master/index.html
- https://pcjericks.github.io/py-gdalogr-cookbook/index.html
Poo in brief
Une classe Personne qui hérite d'un object, obligatoire pour Python.
La classe Personne est initialisé grâce à la fonction constructeur __init__ qui prend self et 3 paramètres
La classe Personne possède une fonction nommé newFriends qui prend self et 1 paramètre
gerard est une variable qui contient une instance de la classe Personne (cad un objet)
Depuis l'instance de la classe Personne contenu dans la variable gerard (cad un objet), je récupère et apelle la fonction newFriend avec self et la valeur pour un paramètre.
Depuis l'instance de la classe Personne contenu dans la variable gerard (cad un objet), je récupère et écrase l'attribut age avec la valeur passé par l'affectation
A l'aide de ces principes, nous essaierons de réfléchir par la suite à la manière de structurer plus efficacement nos futurs programme.
Application of concepts
A World Of Pirates
Voyons avec un autre exemple d'humain, le Navigateur , et plus spécifiquement, le navigateur pirate !
class Navigateur(object):
def __init__(self,yeux,bras,jambes): # (1)
self.tete = 1 # (2)
self.nbYeux = yeux # (3)
self.nbBras = bras
self.nbJambes = jambes
self.afficheInfo() # (4)
def afficheInfo(self): # (5)
print( "Bonjour ! J'ai ", self.nbYeux, "yeux, ", self.tete, " tête, ", self.nbBras, " bras, et ", self.nbJambes, " jambes ...")
def accident(self,typeAcc): # (6)
if typeAcc == "bras":
self.nbBras = self.nbBras - 1 # (7)
elif typeAcc == "jambe":
self.nbJambes = self.nbJambes - 1
elif typeAcc == "yeux":
self.nbYeux = self.nbYeux - 1
def initGrade(self):
self.grade = "minable" # (8)
print( "je suis un pirate ", self.grade)
def augmenteGrade(self):
if self.grade == "minable":
self.grade = "minus"
elif self.grade == "minus":
self.grade = "chef"
elif self.grade == "chef":
self.grade = "capitaine"
- Notre constructeur initialise et donc personnalise la matrice originelle que représente la classe
Navigateur - Mais j'ai le droit de définir également des attributs par défaut + ! Attention ! Les attributs n'existe que dans la portée de la classe (même fonctionnement que pour les fonctions donc)
- Je transfere l'argument passé à mon constructeur dans mon attribut de classe
- J'appelle une fonction de mon programme à la fin de l'initialisation de l'objet, celle ci affiche des informations sur mon nouvel objet navigateur personnalisé
- Une fonction, même si elle ne prend pas d'argument, doit prendre l'argument par défaut nommé 'self'
- Ici on passe un argument supplémentaire qui est utilisé dans la fonction.
- Je modifie un attribut de mon objet, j'ai le droit du moment que j'utilise self pour indiquer qu'il existe ! + => sinon cela créé une nouvelle variable locale à la fonction !!
- Cette fonction ajoute un attribut grade à mon objet, celui-ci est ensuite accessible normalement comme tout les autres attributs existant et définis dans
__init__
# gerard est un petit navigateur de plaisance, et pour le moment il a tout ses membres :)
gerard = Navigateur(2,2,2)
# il décide de rentrer dans la piraterie
gerard.initGrade()
# Sauf qu'un jour gerard croise un requin au bout d'une planche, le risque du métier, heureusement il s'en tire pas trop mal
gerard.accident("jambe")
gerard.afficheInfo()
# Avec l'experience Gerard fait de grand progrès !
gerard.augmenteGrade()
gerard.augmenteGrade()
gerard.augmenteGrade()
gerard.augmenteGrade()
print( "Gerard est maintenant ", gerard.grade ," ! ")
# Malheureusement, la vie de capitaine n'est pas facile ...
gerard.accident("yeux")
# Pauvre Gerard ...
gerard.afficheInfo()
Exercices
- Essayez de stocker plusieurs pirates dans une liste, appelez un accident sur chacun d'eux à l'aide d'une boucle.
- Construisez une fonction tempete qui prend un objet navigateur en paramètre, et lui applique un accident au hasard. Appelez ensuite cette fonction sur un des pirates de votre liste.
- Essayez maintenant d'appliquer cette fonction sur un navigateur tiré au hasard dans votre liste de pirate !
- Essayez maintenant de faire un autre parcours de vie avec un autre pirate !
- Ajoutez une nouvelle fonction
descendreGrade()
Nous modifions un peu notre programme principal avec de nouveaux éléments, car oui, les pirates ont besoin d'argent, d'honneur et de combats.
# -*- coding: utf-8 -*-
import random
# Definition des classes du monde des pirates !
class Navigateur(object): # <1>
def __init__(self, nom, salaire=10, yeux=2, bras=2, jambes=2, argent=0, force=1, grade="minable"):
self.nom = nom
self.salaire = salaire
self.tete = 1
self.nbYeux = yeux
self.nbBras = bras
self.nbJambes = jambes
self.argent = argent # <2>
self.force = force
self.grade = grade
self.afficheInfo()
def augmenteGrade(self):
if self.grade == "minable":
self.grade = "minus"
elif self.grade == "minus":
self.grade = "chef"
elif self.grade == "chef":
self.grade = "capitaine"
self.force = self.force + 1 # <3>
- La classe Navigateur change un peu, on passe des arguments par défaut pour définir la morphologie et le grade de nos navigateurs. Ainsi on part du principe que le grade par défaut est "minus", et la morphologie normale.
- De nouveaux attributs apparaissent dans notre programme : argent, force et grade.
- A chaque augmentation de grade on augmente la force du navigateur de 1, par exemple cette fonction peut être appellé à chaque fois que son navire gagne un combat, on peut considérer qu'il augmente de grade.
Mais nous le savons bien, un pirate tout seul n'a aucune chance de survie dans ce monde hostile, il lui faut donc un équipage.
Dans le code suivant nous créons un premier équipage en utilisant une liste dans lequel nous stockons plusieurs instances de cette classe Navigateur.
jack = Navigateur("Jack Calico", argent=10, force=10, grade="capitaine")
edward = Navigateur("Edward Drake", argent=2, force=3, grade="minable")
anne = Navigateur("Anne Bonny",argent= 3, force=2, grade="minable")
equipe1 = [jack,edward,anne]
print( equipe1)
Normalement un code bizarre de ce type s'affiche :
Quote
[<main.Navigateur instance at 0x7f632f278908>, <main.Navigateur instance at 0x7f632f2789e0>, <main.Navigateur instance at 0x7f632f278a28>]`
Il s'agit de la référence des objets en mémoire. Pour accéder au contenu de chacun de ces objets navigateurs, il n'y a pas d'autres choix que de faire appel aux méthodes ou aux attributs propre à ces objets.
Pour mieux comprendre la structure qui résulte d'un tel code, on peut se représenter la liste sous cette forme en mémoire. Les valeurs initialisées pour chaque attributs étant commentée ici en rouge.
Si je veux affiche par exemple, le nom de chacun des pirates, je pourrais faire la boucle suivante :
Je peux aussi changer la valeur des attributs directement :
Si je veux uniquement augmenter le grade des minables dans mon équipage, il suffit de filtrer notre liste en fonction d'un attribut :
Si je veux calculer l'argent total que possède cet équipage, encore une fois rien de plus facile. Il suffit d'accéder à l'attribut correspondant lors d'un parcours de liste.
Réaliser cet équipage n'est qu'une première étape, que pouvons nous ajouter à présent ? Il serait intéressant par exemple d'encapsuler notre liste de pirates fraichement construite dans un objet Equipage auquel on pourra rajouter la fonction de calcul des richesses que nous avons déjà réalisé.
Pour cela nous allons faire appel à deux des grandes concepts de la Programmation Orientée Objet, autrement dit la composition et l'héritage
Relation between Objects
Building link using Composition
La programmation orientée objet n'aurait pas eu de succès si il n'y avait pas eu la mise en réseau et l'établissement de règle d'inter-dépendances entre les objets.
C'est ici que se trouve la vrai puissance de la programmation orientée objet, car même si celle-ci n'a jamais eu de définition claire, elle repose sur un ensemble de concepts qui permettent de créer, d'organiser un code complexe de façon modulaire et générique. Nous verrons en quoi plus précisément par la suite.
En sachant cela, nous allons mettre en oeuvre la composition pour créer un monde à la mesure de nos pirates.
La première étape consiste à revoir notre classe Navigateur pour qu'elle intègre une nouvelle classe Equipage
class Equipage(object): # (1)
def __init__(self, marins):
self.marins = marins
def calculDesRichesses(self): #(2)
richesse = 0
for p in self.marins:
richesse = richesse + p.argent
print( richesse)
def rechercheDuPlusRiche(self): #(3)
pass
def rechercheDuPlusFort(self):
pass
def calculForceEquipage(self): #(4)
pass
- La classe a besoin d'une liste de
Navigateurpour que notre méthode marche correctement. - On interroge l'attribut de notre objet qui contient à présent la liste de navigateurs, à savoir
self.marins - Il est intéressant de pouvoir savoir à tout moment quel est le navigateur le plus fort mais aussi le plus riche de la bande. A vous d'écrire le code :)
- Une fonction qui renvoie la force totale de notre équipage, pratique pour comparer deux équipages par la suite (dans la future fonction
combat()par exemple)
Une première idée serait de définir un équipage ainsi :
Seulement voilà, maintenant que nous avons définit un équipage comme un objet à part entière, plus complexe qu'une simple liste, il serait également intéressant de donner une vrai raison d'être à cet équipage, à savoir un bateau qui l'abrite.
Plutôt que de créer un objet Equipage à part, pour ensuite le passer en paramètre lors de l'instanciation d'un Navire, nous préferons ici faire le choix d'une vrai composition. Ainsi l'équipage (et non les navigateurs, qui eux restent libre d'être associés à d'autres bateaux/équipages) devient exclusivement rattaché à un objet Navire. Si le Navire sombre, alors l'objet Equipage sombre avec lui.
class Navire(object): # <1>
def __init__(self, nom, marins):
self.nom = nom
self.equipage = Equipage(marins)
def combat(self, ennemi): # <2>
print( "combat le bateau ennemi ! ")
- La classe navire est responsable d'un équipage et un seul, c'est elle qui créée l'instance de la classe
Equipageaccueillant la liste deNavigateurs. Ainsi la liste de marins passés en paramètres sert ici à instancier la classeEquipagestocké par chaque objetNavire - La classe qui définit les combats, pour déterminer l'issue du combat et calculer l'abordage, il faut prendre en paramètre un navire ennemi. On se base sur une comparaison de force entre les deux équipages pour le moment. Le navire le plus fort l'emporte.
Désormais, c'est l'objet Navire qui récupère la liste de navigateurs pour en faire un objet Equipage.
En terme d'imbrication entre objets (Navire contient Equipage contient une liste de Navigateurs), cela représente le diagramme de classe, puis d'objets suivant :
La liste de navigateurs est passée à navire1, qui lors de sa initialisation construit directement un objet de type Equipage qui prend en paramètre cette même liste (self.equipage = Equipage(marins)). Celle-ci étant stocké dans l'attribut marins de cet équipage, on pourra l'apeller ainsi : navire1.equipage.marin. Comme il s'agit d'une liste, les navigateurs qu'elle contient peuvent être appelé avec la notation tableau classique. Ainsi navire1.equipage.marins[0] renvoie le premier marin de l'équipage, dont on peut également afficher le nom navire1.equipage.marins[0].nom
Autrement dit, chaque imbrication implique un niveau supplémentaire dans la notation pointé.
Au travers de cet exemple, l'utilisateur ne voit qu'une façade de l'objet Navire, mais cela suffit à assurer son fonctionnement et sa réutilisation. Ce sont les signatures des fonctions et procédure qui nous renseigne sur son utilisation. Cela veut dire que des objets peuvent utiliser d'autres objets sans savoir comment sont implémentés ces derniers. Le concept qui désigne cette capacité c'est l'encapsulation.
Si demain je veux changer la façon dont mon Equipage fonctionne dans un Navire, je peux le faire sans que cela affecte le fonctionnement des autres objets qui utilise ce dernier. Il suffit de respecter les signatures des fonctions déjà définies dans la relation entre Navire et Equipage. Et même si nous devions changer ces dernières pour ajouter de nouvelles fonctionnalités, il n'est pas dit que cela change quelque chose dans la façon dont Navire est manipulé.
De nombreuses autres choses découlent de ce concept, mais on retiendra surtout ici que les objets deviennent une donnée et que ceux-ci peuvent être utilisé de façon simple dès lors qu'on connait les fonctions et procédures qui sont exposés par l'objet.
Enfin, pour cette dernière étape, essayons d'être encore plus imaginatif. Les pirates ne se recrutent pas n'importe comment, il faut aller les chercher dans des Tavernes, logique non ?
class Taverne(object):
def __init__(self, listeDeNoms, listeDePrenoms):
self.listDeNoms = listeDeNoms
self.listDePrenoms = listeDePrenoms
def debaucher(self): # <1>
salaire = random.randint(1, 10)
argent = random.randint(1, 20)
force = salaire * 1.5
nomPrenom = " ".join([self.listDeNoms[random.randint(0, len(self.listDeNoms) - 1)],
self.listDePrenoms[random.randint(0, len(self.listDePrenoms) - 1)]])
return Navigateur(nomPrenom, salaire, argent=argent, force=int(force)) # <2>
- En utilisant la liste de noms et prénoms stockés par l'objet
Taverne, on génère un nouvel objetNavigateur, en piochant au hasard un salaire entre 1 et 10 pièce d'or. - Le Navigateur ainsi créé est renvoyé par la fonction
debaucher()
Ne reste plus ici qu'à construire une fonction qui apelle de multiple fois la fonction debaucher() pour constituer équipage. Nous allons voire cela dans les exercices.
Ce schéma UML (diagramme de classe) peut se lire ainsi. Un objet construit avec la classe Taverne doit pouvoir construire des navigateurs en partant de la classe Navigateur. Un objet de type Equipage contient en ensemble d'objets de type Navigateur (dans une liste par exemple). L'objet de type Equipage est ici dépendant et construit par l'objet de type Navire, il n'existe pas en dehors (symbole losange noir). Enfin un objet de type Navire contient un seul objet équipage.
Ainsi on peut en déduire qu'un objet navire contient un objet équipage qui contient lui même un attribut contenant une liste d'objets navigateurs.
Exercices
- Construire/Instancier un objet de type
Tavernestocké dans une variabletaverneAPirateen lui passant les listes ci-dessous :
nomDePirate = ["Bonny", "Jack", "Teach", "Drake", "Morgan", "Nau", "Read"]
prenomDePirate = ["Anne", "Calico", "Edward", "Francis", "Henry", "Jean", "Mary"]
- Ecrire une fonction
buildEquipagequi prend en paramètre un nombre de marin (nbMarins) et un objet taverneAPirate de typeTaverne, et renvoie une liste d'objetsNavigateur.
# Fonction pour construire equipage
def buildEquipage(taverne, nbMarins):
equipage = []
equipage.append(...)
...
return equipage
Comme déjà vu un peu plus haut, on apelle les attributs et les fonctions à l'aide d'une notation pointée. Une fois l'objet taverneAPirate instancié, il est possible d'apeller sa fonction debaucher() ainsi taverneAPirate.debaucher()
- Construire et stocker dans des variables deux objets navires (
navire1etnavire2par exemple) à partir de la classeNavireen leur passant des équipages renvoyés parbuildEquipage(...)
Comme déjà vu un peu plus haut, on apelle les attributs et les fonctions des objets à l'aide d'une notation pointée, et cela de façon hiérarchique.
-
Réaliser les fonctions permettant de calculer un combat entre deux Navires en se basant sur la force des pirates qui les composent. Remplir la fonction
calculForceEquipage()etcombat()qui prend forcément un objet navire en paramètre (pour comparer les deux forces, celle de lui-même, et celle du navire passé en paramètre). -
Penser à payer vos pirates à la fin de chaque combat victorieux! Remplir la fonction
jourDePaye()qui se base sur le salaire de vos pirates. -
Penser à ajouter une ou plusieurs classes de votre choix permettant d'enrichir ce monde de Pirate par de nouvelles aventures.
Building links using Inheritance
L'héritage (ou inheritance) permet plusieurs choses, mais on la retient surtout pour sa capacité à factoriser du code tout en spécialisant une classe de base existante, ce qui permet de garder le comportement initial (attributs et méthodes), tout en lui en ajoutant de nouveaux (attributs et méthodes).
Voyons ce que cela donne avec nos pirates ...
Pour déterminer si une relation d'héritage est possible ou pas entre deux classes, on peut s'aider de cette règle : is-a
- Un PirateZombie is-a Pirate
- Un PirateVolant is-a Pirate
- Un PirateMagicien is-a Pirate
# -*- coding: utf-8 -*-
import random
class Pirate(object): # <1>
def __init__(self,nation):
self.nation = nation
def speak(self):
print( "je suis ", self.nation)
print( "et je suis un pirate normal ...")
PirateMagicien is - a Pirate
class PirateMagicien(Pirate): # <2>
def __init__(self, nation, listFormule = ["Abracadabra"]):
super(PirateMagicien, self).__init__(nation) # <3>
self.formules = listFormule # <4>
def speak(self): <5>
print( "je suis un magicien de nationalité ", self.nation)
def cast(self): # <6>
self.speak()
print( self.formules[random.randint(0,len(self.formules)-1)])
- La classe de Base, qui va servir à la dérivation, celle ci n'a rien de particulier.
- C'est ici que l'on déclare la dérivation, en indiquant bien de quelle classe on hérite, ici
Pirate super()est une fonction spéciale qui permet d'apeller la méthode__init__()dePirate. C'est ainsi que l'on ajoute les comportements de la classePirateà notre classe dérivéePirateMagicien. Pour que cette méthode s'initialise bien, des paramètres doivent lui être passé en entrées, ce qui explique le passage de nation à la méthode__init__()dePirate:__init__(nation)- Il est temps de spécialiser notre pirate en lui ajoutant de nouveaux attributs, ici une liste de formules pour lancer un sort.
- Si je ne suis pas content du comportement de la classe de base, comme par exemple ici la fonction
speak()dePirate, je peux toujours l'écraser (plus connu sous l'appelation 'override') par unspeak()plus adapté comme c'est le cas - Idem, on continue la spécialisation en ajoutant une nouvelle méthode disponible uniquement pour les instances de la classe
PiratesMagiciens*
Le reste du programme avec d'autres types de pirates spécialisés. + L'appel des functions propre à chacun est en bas du programme.
PirateZombie is - a Pirate
class PirateZombie(Pirate):
def __init__(self, nation, vitesse):
super(PirateZombie, self).__init__(nation)
self.vitesse = vitesse
def eat(self):
self.speak()
print("miam miam")
def speak(self):
print( " *Bweahhhh* ", self.nation)
print( " *Bweahhhh* Brain ... ")
PirateVolant is - a Pirate
class PirateVolant(Pirate):
def __init__(self, nation,nomDuBalais):
super(PirateVolant, self).__init__(nation)
self.nomDuBalais = nomDuBalais
def fly(self):
self.speak()
print("Je vole sur mon " + self.nomDuBalais)
def speak(self):
print( "Je suis ", self.nation)
print( "et je suis l'as des as pirates ... ")
unPremierSpecialiste = PirateMagicien("Italien",["Abracadabra","Bazinga"])
unPremierSpecialiste.cast()
unDeuxiemeSpecialiste = PirateZombie("Haitien",10)
unDeuxiemeSpecialiste.eat()
unTroisiemeSpecialiste = PirateVolant("Hollandais","Alactasar")
unTroisiemeSpecialiste.fly()
Ok, comme vous voyez, ça marche plutôt bien, et ça peut nous faire économiser pas mal de code dans certains cas en factorisant les comportements similaires dans une même classe de base.
Toutefois, dans le cadre du développement d'un logiciel plus complexe, on pourrait être tenté de réaliser une hierarchie bien plus grande que celle ci.
Ici pour notre exemples, il n'est pas difficile de trouver un exemple qui montre bien les limites de ce type de hierarchie finalement très statique, ou rigide. Que se passe t il dans mon programme si je décide tout à coup que les Pirates peuvent cumulés plusieurs traits, autrement dit, si je veux pouvoir créer des Pirate qui sont par exemple à la fois Volant et Magicien ? ou Zombie et Volant ? etc. Vais je continuer à étendre ma hierarchie ? Non car je vais perdre tout le bénéfice de la factorisation réalisé au préalable.
Transform Inheritance into Composition
Cette transformation donne un code moins facile à comprendre mais vous allez constater par vous même qu'elle amène aussi beaucoup plus de souplesse pour penser l'architecture de vos programmes.
Si on change notre façon de raisonner en essayant d'externaliser les comportements de notre Pirate initial.
En utilisant la relation has-a propre à l'aggrégation ou à la composition, il est possible de construire des objets complexes à partir de sous blocs plus simple, pensé pour être réutilisable.
Est ce que notre exemple de pirate aux multiples fonctionnalités (volant, zombie, magicien) devient possible ?
# -*- coding: utf-8 -*-
import random
class MagicPower(object):
def __init__(self,power):
self.power = power
def cast(self):
print ("pfscht ")
class ZombiePower(object):
def __init__(self,vitesse):
self.vitesse = vitesse
def eat(self):
print("miam")
class VolantPower(object):
def __init__(self,nomDuBalais):
self.nomDuBalais = nomDuBalais
def fly(self):
print( "Je vole sur mon ", self.nomDuBalais)
Il y a plusieurs façon de voir une composition, tout dépend du problème.
Dans cette première version les classes sont créés directement dans la classe PirateZombieMagicien, elles sont donc liées à celle ci.
Si le PirateZombieMagicien se faisait tuer, alors les définitions des classes qu'il contient sont également perdus.
#Pirate Zombie Magicien has-a MagicPower, has-a ZombiePower
class PirateZombieMagicien(Pirate):
def __init__(self, nation):
super(PirateZombieMagicien, self).__init__(nation)
self.magic = MagicPower(5)
self.zombie = ZombiePower(2.5)
def eat(self):
self.zombie.eat()
def cast(self):
self.magic.cast()
#Pirate Zombie Magicien has-a VolantPower, has-a ZombiePower
class PirateZombieVolant(Pirate):
def __init__(self, nation):
super(PirateZombieVolant, self).__init__(nation)
self.volant = VolantPower("Asclatra")
self.zombie = ZombiePower(2.5)
def eat(self):
self.zombie.eat()
def fly(self):
self.volant.fly()
monpiratecustom1 = PirateZombieMagicien("Hongrie")
monpiratecustom1.cast()
monpiratecustom1.eat()
monpiratecustom2 = PirateZombieVolant("Pérou")
monpiratecustom2.fly()
monpiratecustom2.eat()
Une autre possibilité est envisageable, où cette fois ci la nature des pouvoirs du Pirate est passée en paramètre au moment de sa création.
Il s'agit d'une aggregation, mais le bénéfice est le même que pour la composition, et la flexibilité pour développer des fonctionnalités dans notre programme est d'autant plus grande.
class Pirate(object):
def __init__(self, nation, magicien = None, zombie = None, volant = None):
self.nation = nation
self.magic = magicien
self.zombie = zombie
self.volant = volant
magicien = MagicPower(5)
volant = VolantPower("Patatra")
monpirate = Pirate("BarbeCourte",magicien = magicien, volant = volant)
if monpirate.volant :
monpirate.volant.fly()
if monpirate.magic :
monpirate.magic.cast()
if monpirate.zombie:
monpirate.zombie.eat()
Bon et admettons maintenant que le Pirate magicien possède une barbe de feu, qu'il soit diabolique, et possède un sort capable de lui rendre de la vie ?
Une des solution est de rendre bi-directionelle la relation d'aggregation has-a entre la classe Pirate et la classe MagicPower, comme cela la classe MagicPower qui contient le sort de vie peut avoir accès aux attributs et aux méthodes du Pirate et les modifier.
Exactement ce que l'on veut faire, et donc pour cela il suffit d'enlever la flèche de direction dans le schéma UML.
Voici pour le code source correspondant au diagramme UML :
class MagicPower(object):
def __init__(self,power):
self.power = power
def cast(self):
if self.owner: # (1)
self.owner.vie += 5.0 * self.power # (2)
print( "pfscht +", 5.0 * self.power , " vie")
class Pirate(object):
def __init__(self, nation, vie,magicien = None, zombie = None, volant = None):
self.vie = vie
self.nation = nation
self.magic = magicien
self.zombie = zombie
self.volant = volant
if self.magic: # (3)
self.magic.owner = self # (4)
magicien = MagicPower(2.0)
monpirate = Pirate("LeChuck", 100.0, magicien = magicien)
monpirate.magic.cast()
print (monpirate.vie)
- A l'execution de la fonction, et avant d'appliquer le pouvoir on s'assure bien que l'attribut
self.ownerexiste. En l'occurence à cet instant là qui correspond à la définition de la fonction, il n'existe pas encore, et il faudra attendre l'étape 4 pour que cet attribut soit fixé par la classePirate. Il est donc tout à fait possible de définir des attributs à posteriori pour une classe. Toutefois on voit que nous avons rajouté un lien de dépendance entre ces deux classes, car cette fonction a maintenant besoin d'une classe parente pour pouvoir marcher correctement (cad ajouter de la vie à son propriétaire ici) - On accède à l'objet parent, et on modifie son attribut de vie !
- On teste que le
Pirateen question est bien un magicien - On ajoute un attribut owner à l'objet
MagicPower, qui connait donc maintenant son propriétaire.
Choosing between Composition and Inheritance
Il faut savoir qu'il n'y a pas de meilleur techniques l'une par rapport à l'autre, tout est avant tout une question d'usage. Ainsi n'y a pas de duel entre héritage et/ou composition, il faut seulement savoir que les deux techniques possèdent leurs avantages ou leurs inconvénients, et qu'elle sont bien souvent interchangeables.
L'héritage est la notion la plus facile à comprendre, et la plus facile à mettre en oeuvre, et donc c'est aussi la plus dangereuse. Il existe un débat très vif sur son utilisation dans le cercle des développeurs. Je vous donne dans la suite du document quelques clefs (non exhaustives) pour mieux comprendre quand il faut, et quand il ne faut pas l'utiliser.
Voici ce qu'en dit le très bon learn python the hard way
Quote
On object-oriented programming, Inheritance is the evil forest. Experienced programmers know to avoid this evil because they know that deep inside the Dark Forest Inheritance is the Evil Queen Multiple Inheritance. She likes to eat software and programmers with her massive complexity teeth, chewing on the flesh of the fallen. But the forest is so powerful and so tempting that nearly every programmer has to go into it, and try to make it out alive with the Evil Queen's head before they can call themselves real programmers. You just can't resist the Inheritance Forest's pull, so you go in. After the adventure you learn to just stay out of that stupid forest and bring an army if you are ever forced to go in again.
Info
Voici une proposition de test vue sur stackOverflow, que j'ai traduite ci dessous, et qui permet de detecter avec un peu plus de discernation si vous avez besoin d'un héritage ou plutôt d'une composition dans votre programme :
-
Does
TypeBwant to expose the complete interface (all public methods no less) ofTypeAsuch thatTypeBcan be used whereTypeAis expected? Indicates Inheritance.e.g. A Cessna biplane will expose the complete interface of an airplane, if not more. So that makes it fit to derive from Airplane.
-
Does
TypeBonly want only some/part of the behavior exposed byTypeA? Indicates need for Composition.e.g. A Bird may need only the fly behavior of an Airplane. In this case, it makes sense to extract it out as an interface / class / both and make it a member of both classes.
Source : 'http://stackoverflow.com/questions/49002/prefer-composition-over-inheritance?rq=1[stackoverflow]'
Annexe
- http://fr.wikipedia.org/wiki/Principe_de_substitution_de_Liskov[Barbara Liskov's Liskov Substitution Principle] as a test for 'Should I be inheriting from this type?'
- Les principes de POO http://fr.wikipedia.org/wiki/SOLID_%28informatique%29[SOLID]
Quelques phrases que j'ai trouvé un peu partout sur Internet qui permettent de cloturer ce sujet épineux :
[quote, 'suite sur http://learnpythonthehardway.org/book/ex44.html[learn python the hard way]' ]
Most of the uses of inheritance can be simplified or replaced with composition, and multiple inheritance should be avoided at all costs.
[quote, 'http://berniesumption.com/software/inheritance-is-evil-and-must-be-destroyed[berniesumption]']
All of the pain caused by inheritance can be traced back to the fact that inheritance forces is-a rather than has-a relationships. If class R2Unit extends Droid, then a R2Unit is-a Droid. If class Jedi contains an instance variable of type Lightsabre, then a Jedi has-a Lightsabre.
The difference between is-a and has-a relationships is well known and a fundamental part of OOAD, but what is less well known is that almost every is-a relationship would be better off re-articulated as a has-a relationship.
[quote, 'suite sur http://www.ronaldwidha.net/2009/03/22/a-good-example-of-favouring-composition-over-inheritance/[ronaldwidha.net]' ]
Inheritance doesn’t work in real life During my Computer Science studies, I learned about a cliche example of Student and Teacher classes should inherit from a Person base class. All the common properties and methods should be put in the base class, therefore when new common properties and methods are added, they will not be duplicated in different child classes.In reality, this almost never happens.
CAUTION: Il faut privilégier la composition dès que votre hierarchie de classe vous parait trop complexe ou inadapté.
[NOTE]
Pour aller plus loin dans le débat héritage vs composition :
- http://learnpythonthehardway.org/book/ex44.html
- http://www.copypasteisforword.com/notes/use-inheritance-properly
- http://stackoverflow.com/questions/49002/prefer-composition-over-inheritance?rq=1
- http://www.artima.com/lejava/articles/designprinciples4.html
- http://stackoverflow.com/questions/1020453/whats-the-point-of-inheritance-in-python?rq=1
- http://berniesumption.com/software/inheritance-is-evil-and-must-be-destroyed/
-
http://en.wikipedia.org/wiki/Composition_over_inheritance
==== Composition
=== Duck Typing
La notion de DuckTyping est très très utilisé en Python, et préfigure plus d'une philosophie que d'une réelle contrainte technique. Pour mieux comprendre en quoi consiste cette posture mentale il faut déjà tenter de comprendre pourquoi les informaticiens font référence à un canard...
[quote , Alex Martelli (2000) on comp.lang.python newsgroup] In other words, don't check whether it IS-a duck: check whether it QUACKS-like-a duck, WALKS-like-a duck, etc, etc, depending on exactly what subset of duck-like behaviour you need to play your language-games with.
image::images/ducktyping-square.jpeg[align="center", 250]
Autrement dit si je vois un animal qui vole comme un canard, cancane comme un canard, et nage comme un canard, alors j'appelle cet oiseau un canard !
[source,python] .Pris sur wikipedia > http://en.wikipedia.org/wiki/Duck_typing#History[DuckTyping]
class Duck(object): def quack(self): print("Quaaaaaack!")
def feathers(self):
print("The duck has white and gray feathers.")
class Person(object): def quack(self): print("The person imitates a duck.")
def feathers(self):
print("The person takes a feather from the ground and shows it.")
def name(self):
print("John Smith")
def in_the_forest(animal): # <1> animal.quack() animal.feathers()
def game(): donald = Duck() john = Person() in_the_forest(donald) in_the_forest(john)
game() # <2>
<1> Ici une fonction qui prend en paramètre n'importe quel animal, et essaye voir si il a les capacité de comportements quack() ou feathers()
<2> On voit bien quand on execute game() que Python se contrefiche de savoir quel est la nature de la classe à l'origine de la méthode, pour lui john est un canard car il possède bien les comportement attendus.
En quoi cette technique est intéressante ? Elle permet de filtrer ou d'apeller des classes sans forcément savoir tout de leur comportement, c'est ce que l'on apelle aussi le polymorphisme. Il y'en a absolument partout dans le langage Python, ainsi par exemple la fonction len() est tout à fait capable de calculer la taille de n'importe quel type d'objets : tuple, list, texte sans se soucier de sa nature !
[source,python]
len("How long am I?") 14 len((1, 2, 3, 4, 5)) 5 len(["a", "b", "c"]) 3
Si nous revenons à l'exemple du pirate dans le cadre de l'héritage vu tout à l'heure, le polymorphisme est possible du fait que nous pouvons utiliser n'importe quel fonction speak() ou nation sans forcément savoir si ce qu'il y a dans la liste c'est un PirateZombie ou un PirateVolant. Cette notion est extrement importante car elle permet d'avoir des comportements génériques.
exemple
Le duckTyping va plus loin encore, et permet d'avoir du polymorphisme sans qu'il y ai un héritage derrière. En ce sens son utilisation est encore plus simple et intuitive.
[source,python]
class Mousse(object): def init(self, nom, vie): self.nom = nom self.vie = vie
def blessure(self,vie):
self.vie -= vie
class Pirate(object): def init(self, nom, vie, power): self.vie = vie self.nom = nom self.power = power
def attaque(self, ennemi):
print( "Attaque de puissance ",self.power ," sur ",ennemi.nom)
ennemi.blessure(self.power)
def blessure(self,vie):
self.vie -= vie
print( "Aie ! moi ", self.nom, "je suis blessé de -", vie)
class Equipage(object): def init(self, marins): self.marins = marins
def jourDePaye(self, sommeParMarin):
print ("ajoute de l'argent à la bourse de chacun des marins")
def random(self):
numMarin = random.randint(0,len(self.marins)-1)
return self.marins[numMarin]
class Navire(object): def init(self, nom, marins): self.nom = nom self.equipage = Equipage(marins)
def random(self):
return self.equipage.random()
def combat(self, ennemi):
print( "Attaque de puissance ",self.power ," sur ",ennemi.nom)
ennemi.blessure(self.power)
nombreDeCombat = random.randint(0, len(self.equipage.marins)-1)
print( "nombreDeCombat = ", nombreDeCombat)
for i in range(nombreDeCombat):
#Duels
# Marin 1
pirate1 = navire1.equipage.random()
# Marin 2
pirate2 = navire1.equipage.random()
# Seul les pirates ou tout autre objet ayant une fonction attaque() peut attaquer le pirate 2 !
# Donc lorsque pirate 1 est un mousse il ne se passe rien...
if hasattr(pirate1,"attaque"):
pirate1.attaque(pirate2)
class Taverne(object): def init(self, listeDeNoms, listeDePrenoms): self.listDeNoms = listeDeNoms self.listDePrenoms = listeDePrenoms
def debaucher(self):
salaire = random.randint(1, 10)
force = salaire * 1.5
nomPrenom = " ".join([self.listDeNoms[random.randint(0, len(self.listDeNoms) - 1)],
self.listDePrenoms[random.randint(0, len(self.listDePrenoms) - 1)]])
#La taverne renvoie different type de pirates
des = random.randint(0, 1)
if des == 0:
return Pirate(nomPrenom,120.0, random.randint(0.0,5.0,))
else:
return Mousse(nomPrenom,100.0)
Fonction pour construire equipage
def buildEquipage(nbMarins): equipage = [] for x in range(nbMarins): equipage.append(taverneAPirate.debaucher()) return equipage
Programme principal
if name == "main": nomDePirate = ["Bonny", "Jack", "Teach", "Drake", "Morgan", "Nau", "Read"] prenomDePirate = ["Anne", "Calico", "Edward", "Francis", "Henry", "Jean", "Mary"]
taverneAPirate = Taverne(nomDePirate, prenomDePirate)
navire1 = Navire("Queen Anne's Revenge", buildEquipage(10))
navire2 = Navire("Adventure Galley", buildEquipage(10))
navire1.combat(navire2)
C