Python : l'orienté objet

nora.nckm.eu

Illustration
Table des matières

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 fait console_1.__str__ qui est appelée et donc la méthode __str__ de la classe Console avec l'instance en argument. Ce qui explique pourquoi le paramètre self 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

Patrons structurels

Patrons comportementaux

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 :

Autres principes :

Emojis

Un commentaire sur un de mes articles ? Commencez une discussion sur ma liste de diffusion en envoyant un email à ~nora/public-inbox@lists.sr.ht [règles]