
Par analogie avec un objet familier comme un crayon, qui possède des caractéristiques (taille, modèle, couleur, ...) et une utilité (écrire, dessiner), un objet Python est un bloc de code qui possède lui aussi des caractéristiques (ses variables) et une utilité (son ou ses fonctions).
Pour créer un objet, on recourt à une entité appelée classe.
Rappel :
Une variable locale n'est vue et utilisable que par l'entité qui l'a définie (ex.: une fonction).
Une variable définie dans l'espace global du script (i.e. en dehors de toute fonction) est une variable globale, utilisable à travers l'ensemble du script et pouvant être accédée en lecture seule à l'intérieur des fonctions utilisées dans le script. Pour modifier une variable globale depuis une fonction, il faut la définir dans cette dernière comme suit : global nom-de-la-variable
Les différents espaces de noms :
Au lancement de l'interpréteur, un espace de noms global est créé. On y trouvera les variables globales. A l'import d'un module, à la définition d'un fonction ou d'une classe, à la création d'un objet, un espace de noms spécifique est créé.
Une variable de classe (ou attribut de classe) appartient à l'espace de noms de la classe s'y rapportant.
Une variable d'instance (ou attribut d'instance) appartient à l'espace de noms de l'objet. Ces derniers ne peuvent accéder qu'en lecture aux variables de classe.
Important !
La programmation orientée objet, du fait de l'encapsulation, permet d'éviter l'emploi de variables globales. En effet, ces dernières comportent le risque d'être modifiées ou redéfinies à n'importe quel endroit du programme, risque accrû lorsque le programme est volumineux ou lorsque plusieurs développeurs travaillent ensemble sur le même projet.
Du fait de leur encapsulation dans l'objet, les attributs sont notés : nom_objet.nom_attribut
L'instruction age = pierre.age
signifie "extraire de l'objet pierre la valeur de son attribut age et assigner cette valeur à la variable age".
Il n'y a pas de conflit entre la variable age et l'attribut age de l'objet pierre car l'objet pierre contient son propre espace de noms, indépendant de l'espace de nom global où se trouve la variable age.
Outre le fait de regrouper dans un même ensemble (l'objet) un certain nombre de données (les attributs), il convient également d'y placer les algorithmes destinés à traiter ces données : ce sont les méthodes, à savoir des fonctions particulières encapsulées dans l'objet.
Une méthode comporte toujours au moins un paramètre : self qui sera listé en premier et qui référence l'instance (cela permet de désigner l'objet auquel la méthode sera associée).
class Utilisateur():
statut = 'Inscrit'
age = 0
def donnerNom(self, n):
self.nom = n
pierre = Utilisateur()
luc = Utilisateur()
print(pierre.statut)
print(pierre.age)
pierre.donnerNom('Pierre Durand')
print(pierre.nom)
# on crée une classe Utilisateur
# cette classe possède deux attributs : statut et age
# leur valeur vaudra pour chaque objet à leur création
# cette classe possède une méthode : donnerNom()
# l'argument self désigne l'objet en cours (ici, pierre ou luc)
# deux nouvelles instances de la classe Utilisateur sont créées
# ces deux variables deviennent des objets de type Utilisateur
# les deux objets héritent des attributs de leur classe : ici, statut et age
# ainsi que de leurs méthodes : ici, donnerNom()
class Individu():
age = 0
pierre = Individu()
luc = Individu()
benoit = pierre
print(pierre.age) # retourne 0
print(luc.age) # retourne 0
print(benoit.age) # retourne 0
print(pierre == luc) # retourne False
print(pierre == benoit) # retourne True
Les deux objets (pierre et luc) restent distincts quand bien même ils fassent partie de la même classe et qu'ils aient des contenus similaires. Ils pointent à des emplacements mémoire différents.
En revanche, par l'instruction benoit = pierre, on assigne le contenu de pierre à benoit. Autrement dit, les deux variables référencent le même objet et pointent au même emplacement mémoire. Les variables benoit et pierre sont des alias l'une de l'autre.
Dans l'exemple précédent, les objets pierre et luc disposent lors de leur création d'attributs ayant des valeurs identiques : statut = 'Inscrit' et age = 0
Pour initialiser automatiquement l'objet que l'on crée, on place une méthode particulière appelée constructeur. Elle est notée __init__ et s'exécute automatiquement lorsque l'on instancie un nouvel objet à partir de la classe.
class Utilisateur():
anciennete = 0
def __init__(self, nom, age):
self.nom = nom
self.age = age
def bienvenue(self):
print('Salut, je suis', self.nom)
thierry = Utilisateur('Thierry COCHET', 26)
sophie = Utilisateur('Sophie NAUDE', 34)
print('Ancienneté de Thierry :', thierry.anciennete)
print('Ancienneté de Sophie :', sophie.anciennete)
print('Age de Thierry :', thierry.age)
print('Age de Sophie :', sophie.age)
thierry.bienvenue()
sophie.bienvenue()
# la valeur de cet attribut vaudra pour tous les objets lors de leur création
# création du constructeur
# les paramètres nom et age sont initialisés lors de l'instanciation de l'objet
# on crée l'objet en passant deux arguments : nom et age
# retourne 0
# retourne 0
# retourne 26
# retourne 34
# dans thierry.bienvenue(), l'objet thierry est passé implicitement à la fonction (self)
# retourne 'Salut, je suis Thierry COCHET'
# retourne 'Salut, je suis Sophie NAUDE'
Les destructeurs sont appelés lorsqu'un objet Python doit être nettoyé. Il inverse les opérations qu'un constructeur effectue.
Leur utilisation n'est pas obligatoire car le ramasse-miettes de Python (garbage collector) gère automatiquement la mémoire.
class Personne():
def __init__(self, nom, prenom):
self.nom = nom
self.prenolm = prenom
def __del__(self):
print('Destructeur appelé')
thierry = Personne('COCHET', 'Thierry')
# création du destructeur
Le grand intérêt des classes est de permettre l'encapsulation. Cela consiste à enfermer le code dans l'objet. Le monde extérieur ne peut avoir accès à l'objet qu'à travers des procédures bien définies : l'interface de l'objet.
L'héritage est un mécanisme permettant de créer une nouvelle classe basée sur une classe existante, en ajoutant de nouveaux attributs et de nouvelles méthodes en plus de la classe existante.
La classe enfant hérite des attributs et méthodes de la classe parente, autrement dit elle peut les utiliser.
L'héritage est utile lorsque l'on veut créer des classes très similaires : on écrit alors dans la classe parente tout le code commun aux classes enfant. Ensuite on écrit le code spécifique à chaque enfant dans des classes différentes, ce qui évite de dupliquer le code. On parle alors de dérivation. Ce procédé permet de créer toute une hiérarchie de classes allant du général au particulier.
Attention !
1) Si une classe dérivée hérite de toutes les propriétés de sa classe parente, cela ne signifie pas que les propriétés des instances de la classe parente soient automatiquement transmises aux instances de la classe fille. En conséquence, dans la méthode constructeur d'une classe dérivée, il convient de toujours prévoir un appel à la méthode constructeur de la classe parente. Cet appel se fait via la fonction super().
2) S'il existe plusieurs attributs portant le même nom, Python commence à rechercher ce nom dans l'espace local. Si l'attribut est trouvé, la recherche s'arrête. Sinon Python examine l'espace de noms de la structure parente, puis de la structure grand-parente, et ainsi de suite jusqu'au niveau principal du programme.
3) Il ne faut pas confondre héritage et composition / agrégation.
Une composition consiste à avoir plusieurs classes indépendantes (ex.: Individu() et DroitMaladie()). Fonctionnellement, les instances de DroitMaladie ne sauraient exister sans une instance Individu associée. En termes de bases de données, cela se traduirait par une relation de 1 à n entre la table Individu et la table DroitMaladie avec mise en place d'un contrôle d'intégrité référentielle.
Une agrégation consiste également à avoir plusieurs classes indépendantes (ex.: Region(), Departement(), Canton() et Commune()) mais chaque classe peut exister l'une sans l'autre : on pourrait très bien concevoir la suppression des cantons sans pour autant remettre en cause l'existance du département et de la commune.
class Personne():
def __init__(self, matricule, nom, prenom):
self.matricule = matricule
self.nom = nom
self.prenom = prenom
def afficher(self):
print('Agent n°', self.matricule, self.nom, self.prenom)
class Convention(Personne):
def __init__(self, matricule, nom, prenom, statut, salaire):
super().__init__(matricule, nom, prenom)
self.statut = statut
self.salaire = salaire
def afficherConvention(self):
print('Statut :', self.statut)
print('Salaire :', self.salaire)
p = Convention('16404', 'COCHET', 'Thierry', 'Cadre', 8634.97)
p.afficher()
p.afficherConvention()
### création de la classe parent
### création de la classe enfant de Personne
# la fonction super() appelle les méthodes de la classe parente
# création de l'instance p de la classe Convention
# on passe en arguments : matricule, nom, prenom, statut, salaire
# on appelle la méthode afficher() de la classe parent
# on appelle la méthode afficherConvention de la classe enfant
L'héritage à plusieurs niveaux consiste en une succession d'héritages simples : la classe enfant hérite de la classe mère, la classe petit-enfant hérite de la classe enfant, et ainsi de suite.
class Marque():
def __init__(self, marque, pays):
self.marque = marque
self.pays = pays
def getMarque(self):
return self.marque, self.pays
class Modele(Marque):
def __init__(self, marque, pays, modele):
super().__init__(marque, pays)
self.modele = modele
def getModele(self):
return self.modele
class Motorisation(Modele):
def __init__(self, marque, pays, modele, motorisation):
super().__init__(marque, pays, modele)
self.motorisation = motorisation
def getMotorisation(self):
return self.motorisation
voiture = Motorisation('Ford', 'USA', 'Fiesta', 'Essence')
print(voiture.getMarque(), voiture.getModele(), voiture.getMotorisation())
# création de la classe parent
# création de la classe enfant de Marque
# création de la classe enfant de Modele
# création de l'instance voiture de la classe Motorisation
Toutes les classes héritent de la classe object de manière implicite.
Les fonctions isinstance() et issubclass() sont utilisées pour vérifier les héritages.
if isinstance(voiture, Modele):
print('voiture est une instance de la classe Modele')
if issubclass(Motorisation, Marque):
print('Motorisation est une sous-classe de la classe Marque')
On parle d'héritage multiple lorsqu'une classe enfant peut hériter de plusieurs classes parentes différentes. L'héritage va se faire selon l'ordre des classes parentes indiquées et cela de manière récursive
class Tantieme_appart():
def __init__(self, nbTantieme):
self.nbTantieme = nbTantieme
def getAppart(self):
return self.nbTantieme
class Tantieme_parking():
def __init__(self, nbTantieme):
self.nbTantieme = nbTantieme
def getParking(self):
return self.nbTantieme
class Tantieme_cave():
def __init__(self, nbTantieme):
self.nbTantieme = nbTantieme
def getcave(self):
return self.nbTantieme
class Proprio(Tantieme_appart, Tantieme_parking, Tantieme_cave):
# code
# création de la première classe parent
# création de la deuxième classe parent
# création de la troisième classe parent
# création de la classe enfant héritant des trois parents
On entend par membre de classe, les variables et les fonctions de la classe.
- membre privé : c'est un membre auquel on ne peut accéder que depuis l'intérieur de la classe.
- membre protégé : c'est un membre auquel on ne peut accéder que depuis l'intérieur de la classe ou depuis une classe enfant.
- membre public : c'est un membre auquel on peut accéder depuis n'importe quelle instance (ou objet) de la classe ou d'une classe enfant.
Ces concepts de visibilité ne sont pas implémentés dans Python : une convention de nom a été mise en place afin d'indiquer aux autres développeurs que tel membre ne doit à être accéder qu'à l'intérieur de la classe, par exemple.
- pour un membre privé : __nom-du-membre
- pour un membre protégé : _nom-du-membre
Le polymorphisme permet de définir dans la classe enfant des méthodes portant le même nom que celles de la classe parent. En effet, si une méthode héritée de la classe parent ne convient pas tout à fait, il est possible de la redéfinir dans la classe enfant et donc d'en modifier le comportement.
Lorsque dans la définition d'une classe, on souhaite faire appel à une méthode définie dans une autre classe, il suffit de l'invoquer directement en lui transmettant la référence de l'instance comme premier argument.
class Personne():
def __init__(self, nom):
self.nom = nom
def affiche(self):
print('Je suis une personne :', self.nom)
class Eleve(Personne):
def __init__(self, nom, numEleve):
super().__init__(nom)
self.numEleve = numEleve
def affiche(self):
print('Je suis un élève :', self.numEleve)
class Professeur(Personne):
def __init__(self, nom, numProf):
super().__init__(nom)
self.numProf = numProf
def affiche(self):
def Personne.affiche(self)
print('Je suis un professeur :', self.numProf)
### 1ère méthode
individu = []
eleve = Eleve('Marc KAGE', 'ELV0076')
individu.append(eleve)
professeur = Professeur('Michèle BOYER', 'PRF0031')
individu.append(professeur)
for i in individu:
i.affiche()
### 2ième méthode
def MontreMoi(obj):
obj.affiche()
MontreMoi(eleve)
MontreMoi(professeur)
# création de la classe parent
# création de la classe Eleve enfant de Personne
# création de la classe Professeur enfant de Personne
# exemple d'appel à une méthode définie dans une autre classe
#####
# création de la liste individu
# création de l'instance eleve de la classe Eleve
# ajout des valeurs de l'objet eleve à la liste individu
# création de l'instance professeur de la classe Professeur
# ajout des valeurs de l'objet professeur à la liste individu
# affichage du contenu de la liste individu
# qui se trouve contextualisé (élève ou prof)
#####
# création d'une fonction pouvant accepter n'importe quel objet
# appel de la fonction avec l'objet en argument
Surcharger une méthode signifie la redéfinir dans la classe enfant. Elle remplace alors la méthode héritée.
Dans le cas des variables, définir une variable de même nom dans la classe enfant revient à créer une variable locale à la classe enfant en plus de celle globale qui est définie dans la classe de base. Les deux variables continuent à exister à part entière et à être différentes.
Surcharger un opérateur revient à redéfinir sa signification en fonction de sa classe.
Par exemple, l'opérateur + permet d'additionner deux objets de type numérique, il permet également de concaténer deux objets de type chaîne.
La surcharge de l'opérateur est obtenue en définissant une méthode spéciale dans la définition de sa classe.
class Repere():
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return "({0}, {1})".format(self.x, self.y)
def __add__(self, resultat):
resultX = self.x + resultat.x
resultY = self.y + resultat.y
return Repere(resultX, resultY)
gps1 = Repere(12, 45)
gps2 = Repere(37, 24)
resultGPS = gps1 + gps2
print(resultGPS)
# __str__ permet d'obtenir une représentation lisible de l'objet
# si l'expression est de la forme x+y
# Python l'interprète comme : x.__add__(y)
# retourne (49, 69)
Itérer signifie répéter. En POO, itérer sur un objet signifie parcourir l'objet attribut par attribut pour accéder à leur valeur. Les itérateurs sont implicitement utilisés chaque fois que nous manipulons des collections de données comme les listes, les tuples ou les chaînes (qui sont des objets itérables).
La méthode habituelle pour parcourir une collection est d'utiliser la boucle for. On peut également utiliser la méthode iter() qui retourne un élément à la fois.
nom = 'Thierry COCHET'
iterNom = iter(nom)
print(next(iterNom))
print(next(iterNom))
sport = ['vélo', 'natation', 'marche']
iterSport = iter(sport)
print(next(iterSport))
print(next(iterSport))
# iter() retourne un objet itérateur
# next() va accéder aux éléments de l'objet un par un : retourne T
# retourne h
# retourne vélo
# retourne natation
Pour itérer de manière particulière (en partant de la fin, en sautant des caractères, ...), il suffit de définir une méthode __iter__() qui renvoie un objet disposant de la méthode __next__().
class SauteMouton():
def __init__(self, madonnee):
self.madonnee = madonnee
self.position = 0
def __iter__(self):
return self
def __next__(self):
if self.position > len(self.madonnee):
raise StopIteration
self.position += 3
return self.madonnee[self.position]
nom = SauteMouton('Thierry COCHET')
print(next(nom))
print(next(nom))
print(next(nom))
print(next(nom))
# retourne e
# retourne y
# retourne O
# retourne E
Les générateurs permettent de créer des itérateurs : l'instruction yield sera utilisée à la place de l'instruction return.
La métaprogrammation consiste à écrire des programmes qui manipulent des données décrivant elles-mêmes des programmes.
Une métaclasse est une classe dont les instances sont également des classes, alors qu'une classe instancie des objets.
La plupart du temps, le recours aux métaclasses est inutile.
Les décorateurs permettent d'envelopper (wrapper) une autre fonction afin d'en modifier temporairement le comportement.
Avec un décorateur, les fonctions sont prises comme argument dans une autre fonction, puis appelées à l'intérieur de la fonction wrapper.
import time
def Temps_execution(mafonction):
def calculer(*args, **kwargs):
# départ chrono
debut_chrono = time.time()
mafonction(*args, **kwargs)
# stop chrono
stop_chrono = time.time()
print("Le temps d'exécution de la fonction {} est {}: ".format(mafonction.__name__, stop_chrono - debut_chrono))
return calculer
@Temps_execution
def factorial(nombre):
f = 1
for i in range(2, nombre + 1):
f *= i
print(f)
factorial(70)