Cette page présente la programmation orientée objet avec Python. D'autres mémos sont disponibles dans les pages suivantes : les bases, les modules et l'environnement.
La documentation officielle : docs.python.org
Classes
En Python, un objet est défini par sa structure (attributs et méthodes) plutôt que par son type (notion de duck-typing).
Si je vois un oiseau qui vole comme un canard, cancane comme un canard, et nage comme un canard, alors j'appelle cet oiseau un canard.
Dans l'exemple suivant, la classe Console
possède un attribut de classe nombre
et deux attributs d'instance marque
et modele
.
Visibilité
Par convention, la visibilité des attributs et des méthodes est indiquée en les préfixant par
_
ou__
. Dans le cas de__
l'attribut n'est accessible qu'en le préfixant du nom de la classe, par exemple :console._Console__int_serie
.
modele
: attribut public_num_serie
: attribut protected__int_serie
: attribut private
La méthode __init__
est le constructeur de la classe. Il faut lui passer l'argument self
qui représente l'instance de la classe.
La méthode __str__
est la représentation textuelle de l'instance.
La méthode __repr__
est la représentation interne de l'instance.
La méthode switch
est une méthode de classe. Elle doit être précédée du décorateur @classmethod
et il faut lui passer l'argument cls
qui représente la classe. Les méthodes de classe sont généralement des méthodes d'usine (constructeurs).
La méthode afficher_nombre
est une méthode statique. Elle doit être précédée du décorateur @staticmethod
et n'a pas accès à la classe. Les méthodes statiques sont généralement des méthodes utilitaires.
class Console:
nombre = 0
def __init__(self, marque, modele):
Console.nombre += 1
self.marque = marque
self.modele = modele
self._num_serie = f"{marque[0]}{modele[0]}nombre"
self.__int_serie = f"FR{marque[0]}{modele[0]}nombre"
def __str__(self):
return f"Console {self.marque} {self.modele}"
def __repr__(self):
return f"Console(marque={self.marque}, modele={self.modele})"
@classmethod
def switch(cls):
return cls(marque="Nintendo", modele="Switch")
@staticmethod
def afficher_nombre():
print(f"Il y a {Console.nombre} consoles")
print(Console.nombre) # 0
console_1 = Console("Sony", "PS4")
console_2 = Console("Microsoft", "Xbox One")
print(console_1.modele) # 'PS4'
print(console_2.modele) # 'Xbox One'
console_1.modele = "PS5"
console_2.modele = "Xbox Series"
print(console_1.modele) # 'PS5'
print(console_2.modele) # 'Xbox Series'
console_3 = Console.switch()
print(str(console_3)) # 'Console Nintendo Switch'
print(repr(console_3)) # 'Console(marque=Nintendo, modele=Switch)'
Console.afficher_nombre() # 'Il y a 3 consoles'
Paramètre self
Les trois lignes suivantes sont équivalentes. En effet lorsqu'on appelle le constructeur de chaîne de caractère
str(console_1)
c'est en faitconsole_1.__str__
qui est appelée et donc la méthode__str__
de la classeConsole
avec l'instance en argument. Ce qui explique pourquoi le paramètreself
doit être présent dans les méthodes d'instance d'une classe.str(console_1) console_1.__str__() Console.__str__(console_1)
Héritage
Toutes les classes héritent de la classe object
.
La classe ConsoleLight
hérite de la classe Console
. La fonction super
permet de faire appel à la classe parente. On surcharge la méthode __repr__
et on spécialise la méthode __str__
(notion de polymorphisme).
class ConsoleLight(Console):
def __init__(self, marque, modele, couleur):
super().__init__(marque, modele)
self.couleur = couleur
def __str__(self):
return f"{super().__str__()} {self.couleur}"
def __repr__(self):
return f"Console(marque={self.marque}, modele={self.modele}, couleur={self.couleur})"
console_4 = ConsoleLight("Nintendo", "Switch", "Corail")
print(str(console_4)) # 'Console Nintendo Switch Corail'
print(repr(console_4)) # 'Console(marque=Nintendo, modele=Switch, couleur=Corail)'
Console.afficher_nombre() # 'Il y a 4 consoles'
Héritage multiple
L'héritage multiple consiste à spécifier plusieurs classes parentes : class ConsoleLight(Console, Machine)
. L'ordre est important puisqu'il détermine l'ordre dans lequel les méthodes seront recherchées dans les classes parentes. Si une méthode existe dans plusieurs parents seule la première sera conservée.
La fonction super
appellera les méthodes parentes dans l'ordre.
La méthode mro
affiche le Method Resolution Order :
ConsoleLight.mro()
# (<class '__main__.ConsoleLight'>, <class '__main__.Console'>, <class '__main__.Machine'>, <class 'object'>)
Mixins
Les mixins sont des classes dédiées à une fonctionnalité particulière. Ils permettent de partager du code entre des objets différents, n'héritant pas de la même classe de base. Un mixin est utilisable en héritant d'une classe de base et du mixin : class ConsoleImage(Image, Console)
.
class Image:
def affiche(self):
pass
class ConsoleImage(Image, Console):
pass
console = ConsoleImage("Sony", "PS4")
console.affiche()
Propriétés et attributs dynamiques
Le décorateur @property
permet de générer un attribut à partir d'une méthode. Exemple avec le mixin Image
:
class Image:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._image = None
@property
def image(self):
if self._image is not None:
return self._image
return f"{self.marque}-{self.modele}.png")
@image.setter
def image(self, value):
self._image = value
@image.deleter
def image(self):
self._image = None
class ConsoleImage(Image, Console):
pass
console = ConsoleImage("Sony", "PS4")
console.image # 'Sony-PS4.png'
console.image = "sony_ps4.png"
console.image # 'sony_ps4.png'
del console.image
console.image # 'Sony-PS4.png'
Les méthodes qui suivent servent à manipuler des attributs dynamiquement :
hasattr(console, "modele") # True
getattr(console, "modele") # PS4
setattr(console, "modele", "PS5")
delattr(console, "modele")
Opérateurs et méthodes spéciales
Les opérateurs peuvent être définis au moyens de méthodes spéciales, par exemple : __eq__
, __lt__
, __add__
, __radd__
, __sub__
...
Pour définir les opérateurs de comparaison (a == b
, a < b
...), on peut utiliser le décorateur @total_ordering
du module functools
:
from functools import total_ordering
@total_ordering
class Inferior:
def __eq__(self, other):
return False
def __lt__(self, other):
return True
Classes abstraites
Le module abc
propose une classe ABC
et un décorateur @abstractmethod
pour définir des classes abstraites et des méthodes abstraites. Une classe abstraite ou une classe héritant d'une classe abstraite sans implémenter ses méthodes abstraites ne peuvent pas être instanciées.
import abc
class MyAbstractClass(abc.ABC):
@abc.abstractmethod
def my_method(self):
pass
class MyClassA(MyAbstractClass):
pass
class MyClassB(MyAbstractClass):
def my_method(self):
return True
MyAbstractClass() # TypeError
MyClassA() # TypeError
MyClassB().my_method() # True
Classes de données
Le décorateur @dataclass
se base sur les annotations de type pour simplifier la création d'une classe. Il génère automatiquement les méthodes __init__
et __repr__
entre autres. Les attributs de classe sont déclarés avec ClassVar
.
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class Console:
nombre: ClassVar[int] = 0
marque: str
modele: str
def __post_init__(self):
self.name = f"{self.marque} {self.modele}"
console = Console(marque="Sony", modele="PS4")
print(console) # "Console(marque='Sony', modele='PS4')"
Décorateurs
Un décorateur permet d'ajouter des fonctionnalités à une fonction ou une classe sans modifier celle-ci et de façon modulaire et réutilisable. D'autres cas d'usage sont par exemple le nettoyage de données, la mise en cache, le contrôle d'accès ou encore l'interaction avec les attributs de classe… Concrètement, un décorateur est un callable qui accepte et retourne un autre callable.
from functools import wraps
def decodecorator(dataType, message1, message2):
def decorator(fun):
print(message1)
@wraps(fun)
def wrapper(*args, **kwargs):
print(message2)
if all([type(arg) == dataType for arg in args]):
return fun(*args, **kwargs)
return "Invalid Input"
return wrapper
return decorator
@decodecorator(str, "Decorator for 'stringJoin'", "stringJoin started...")
def stringJoin(*args):
st = ""
for i in args:
st += i
return st
@decodecorator(int, "Decorator for 'summation'", "summation started...")
def summation(*args):
summ = 0
for arg in args:
summ += arg
return summ
print(stringJoin("I ", "like ", "Geeks", "for", "geeks"))
print(summation(19, 2, 8, 533, 67, 981, 119))
Affiche :
Decorator for 'stringJoin'
Decorator for 'summation'
stringJoin started...
I like Geeksforgeeks
summation started...
1729
Patrons de conception
Les patrons de conception sont des solutions standards à des problèmes récurrents de la conception de logiciels. Par rapport à un algorithme qui définit un ensemble d’actions précises, un patron de conception décrit une solution à un plus haut niveau et doit être adapté au programme existant.
Le site Refactoring.Guru détaille les patrons de conception, la refactorisation et les principes SOLID. Les informations suivantes sont issues de ce site.
Patrons de création
- Fabrique (Factory) : définit une interface pour créer des objets dans une classe mère, mais délègue le choix des types d’objets à créer aux sous-classes.
- Fabrique abstraite (Abstract Factory) : permet de créer des familles d’objets apparentés sans préciser leur classe concrète.
- Monteur (Builder) : permet de construire des objets complexes étape par étape. Il permet de produire différentes variations ou représentations d’un objet en utilisant le même code de construction.
- Prototype (Clone) : crée de nouveaux objets à partir d’objets existants sans rendre le code dépendant de leur classe.
- Singleton : garantit que l’instance d’une classe n’existe qu’en un seul exemplaire, tout en fournissant un point d’accès global à cette instance.
Patrons structurels
- Adaptateur (Adapter) : permet de faire collaborer des objets ayant des interfaces normalement incompatibles.
- Pont (Bridge) : permet de séparer une grosse classe ou un ensemble de classes connexes en deux hiérarchies — abstraction et implémentation — qui peuvent évoluer indépendamment l’une de l’autre.
- Composite : permet d’agencer les objets dans des arborescences afin de pouvoir traiter celles-ci comme des objets individuels.
- Décorateur (Decorator) : permet d’affecter dynamiquement de nouveaux comportements à des objets en les plaçant dans des emballeurs qui implémentent ces comportements.
- Façade (Facade) : procure une interface offrant un accès simplifié à une librairie, un framework ou à n’importe quel ensemble complexe de classes.
- Poids mouche (Flyweight) : permet de stocker plus d’objets dans la RAM en partageant les états similaires entre de multiples objets, plutôt que de stocker les données dans chaque objet.
- Procuration (Proxy) : permet d’utiliser un substitut pour un objet. Elle donne le contrôle sur l’objet original, vous permettant d’effectuer des manipulations avant ou après que la demande ne lui parvienne.
Patrons comportementaux
- Chaîne de responsabilité (Chain of Responsibility) : permet de faire circuler des demandes dans une chaîne de handlers. Lorsqu’un handler reçoit une demande, il décide de la traiter ou de l’envoyer au handler suivant de la chaîne.
- Commande (Action) : prend une action à effectuer et la transforme en un objet autonome qui contient tous les détails de cette action. Cette transformation permet de paramétrer des méthodes avec différentes actions, planifier leur exécution, les mettre dans une file d’attente ou d’annuler des opérations effectuées.
- Itérateur (Iterator) : permet de parcourir les éléments d’une collection sans révéler sa représentation interne.
- Médiateur (Mediator) : diminue les dépendances chaotiques entre les objets. Il restreint les communications directes entre les objets et les force à collaborer uniquement via un objet médiateur.
- Mémento (Memento) : permet de sauvegarder et de rétablir l’état précédent d’un objet sans révéler les détails de son implémentation. La sérialisation permet aussi de prendre des instantanés de l’état d’un objet.
- Observateur (Observer) : permet de mettre en place un mécanisme de souscription pour envoyer des notifications à plusieurs objets, au sujet d’événements concernant les objets qu’ils observent. L’observateur (publisher) permet aux récepteurs (subscribers) de s’inscrire et de se désinscrire dynamiquement à la réception des demandes.
- État (State) : permet de modifier le comportement d’un objet lorsque son état interne change. L’objet donne l’impression qu’il change de classe.
- Stratégie (Strategy) : permet de définir une famille d’algorithmes, de les mettre dans des classes séparées et de rendre leurs objets interchangeables.
- Patron de méthode (Template Method) : permet de mettre le squelette d’un algorithme dans la classe mère, mais laisse les sous-classes redéfinir certaines étapes de l’algorithme sans changer sa structure.
- Visiteur (Visitor) : permet de séparer les algorithmes et les objets sur lesquels ils opèrent.
Principes de conception
Les principes suivants de programmation orientée objet sont issus du livre Clean Code de Robert C. Martin.
Principes de conception SOLID :
- Single Responsibility Principle (SRP) : une classe ou une méthode doit avoir une et une seule responsabilité
- Open/Closed Principle (OCP) : une classe, une méthode ou un module doit être ouvert à l'extension mais fermé à la modification
- Liskov Substitution Principle (LSP) : une instance de type T doit pouvoir être remplacée par une instance de type G, tel que G sous-type de T, sans que cela ne modifie la cohérence du programme
- Interface Segregation Principle (ISP) : préférer plusieurs interfaces spécifiques pour chaque client plutôt qu'une seule interface générale
- Dependency Inversion Principle (DIP) : il faut dépendre des abstractions, pas des implémentations (interface en paramètre)
Autres principes :
- principe de connaissance minimale (loi de Demeter) : ne parler qu'aux amis immédiats, éviter
a.getB().getC()
- les entrées-sorties (et constantes de configuration) doivent être gérées au plus haut niveau (IO on top)
- il faut isoler et tester les limites (bibliothèques externes)
- préférer le polymorphisme aux instructions
if/else
ouswitch/case
: un switch doit créer des objets polymorphes qui prennent la place d’autres switch dans le reste du système - le meilleur commentaire est un bon nom de méthode ou de classe : cependant ils peuvent être utilisés pour documenter une API ou expliquer pourquoi une implémentation particulière est réalisée
- une méthode doit :
- faire une seule chose
- être courte
- avoir un nom clair
- avoir deux arguments maximum
- ne pas modifier les arguments
- avoir des arguments et retours non null