W3docs

Le Polymorphisme en Python

Apprenez le polymorphisme Python : surcharge de méthodes, duck typing, surcharge d'opérateurs et interfaces abstraites — avec des exemples clairs.

Le polymorphisme (du grec « plusieurs formes ») permet à un seul morceau de code de fonctionner avec des objets de types différents, à condition que ces objets supportent l'interface attendue. On appelle le même nom de méthode sur chaque objet, et chaque objet répond selon sa propre implémentation.

Le polymorphisme est l'un des quatre piliers de la programmation orientée objet, aux côtés de l'encapsulation, l'héritage et l'abstraction. C'est ce qui fait que les fonctions acceptant un type de classe de base fonctionnent automatiquement avec n'importe quelle sous-classe.

Ce chapitre couvre :

  • Ce qu'est le polymorphisme et pourquoi il est important
  • Le polymorphisme par surcharge de méthodes
  • Le polymorphisme par duck typing
  • La surcharge d'opérateurs — une forme de polymorphisme intégrée dans Python
  • Les classes de base abstraites comme moyen formel de définir des interfaces polymorphiques
  • Les schémas pratiques et les pièges courants

Avant de lire ce chapitre, assurez-vous d'être à l'aise avec les classes et objets Python et l'héritage Python.

Pourquoi le polymorphisme est-il important ?

Sans polymorphisme, une fonction qui travaille avec des animaux aurait besoin d'une chaîne explicite de if/elif pour chaque type d'animal :

def make_sound(animal):
    if type(animal).__name__ == "Dog":
        print("Woof!")
    elif type(animal).__name__ == "Cat":
        print("Meow!")
    elif type(animal).__name__ == "Bird":
        print("Tweet!")
    # ... add a new branch every time you add a new animal type

Cette approche est fragile. Chaque nouveau type d'animal nécessite de modifier cette fonction. Avec le polymorphisme, on écrit :

def make_sound(animal):
    animal.speak()   # works for any object that has a speak() method

Ajouter un nouveau type d'animal ne requiert que de définir sa méthode speak() — la fonction elle-même ne change jamais. C'est le principe ouvert/fermé : ouvert à l'extension, fermé à la modification.

Le polymorphisme par surcharge de méthodes

La forme de polymorphisme la plus courante en Python est la surcharge de méthodes : une sous-classe fournit sa propre version d'une méthode définie par la classe parente.

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} makes a sound."


class Dog(Animal):
    def speak(self):
        return f"{self.name} says woof!"


class Cat(Animal):
    def speak(self):
        return f"{self.name} says meow!"


class Bird(Animal):
    def speak(self):
        return f"{self.name} says tweet!"

Une seule boucle fonctionne maintenant pour les trois types :

animals = [Dog("Rex"), Cat("Whiskers"), Bird("Tweety")]

for animal in animals:
    print(animal.speak())

# Rex says woof!
# Whiskers says meow!
# Tweety says tweet!

animal.speak() est distribué vers la version correcte à l'exécution selon le type réel de l'objet. Cette distribution à l'exécution s'appelle dispatch dynamique ou liaison tardive.

Étendre ou remplacer la méthode parente

Lorsque vous effectuez une surcharge, vous pouvez soit remplacer complètement le comportement du parent, soit l'étendre en utilisant super() :

class Animal:
    def speak(self):
        print("[Animal vocalization]")


class Dog(Animal):
    def speak(self):
        super().speak()           # keep the parent's output
        print("Woof! (Dog override adds this)")


Dog().speak()
# [Animal vocalization]
# Woof! (Dog override adds this)

Utilisez super() lorsque la version parente effectue une configuration ou une journalisation utile qui doit encore s'exécuter. Omettez-le lorsque vous souhaitez remplacer entièrement le comportement.

Le polymorphisme par duck typing

Le système de types de Python est structurel plutôt que nominal. Un objet n'a pas besoin d'appartenir à une hiérarchie de classes particulière ; il suffit qu'il possède les bonnes méthodes. C'est ce qu'on appelle le duck typing — nommé d'après le dicton « si ça marche comme un canard et que ça cancane comme un canard, c'est un canard ».

class Dog:
    def speak(self):
        return "Woof!"


class Robot:
    def speak(self):
        return "Beep boop."


class Human:
    def speak(self):
        return "Hello!"


def introduce(entity):
    print(entity.speak())


introduce(Dog())    # Woof!
introduce(Robot())  # Beep boop.
introduce(Human())  # Hello!

Dog, Robot et Human ne partagent aucune classe parente commune (en dehors de l'object natif). Pourtant, introduce() fonctionne avec les trois parce que chacun possède une méthode speak(). La fonction ne vérifie pas le type d'entity — elle appelle simplement la méthode et fait confiance à ce que l'objet réponde correctement.

Quand utiliser le duck typing plutôt que l'héritage ?

SituationApproche préférée
Les objets sont logiquement liés (tous des animaux)Hiérarchie d'héritage
Les objets ne sont pas liés mais partagent un comportementDuck typing
Vous souhaitez imposer l'interface au moment de la définitionClasses de base abstraites
Travail avec des types natifs ou des classes tierces que vous ne pouvez pas modifierDuck typing

Le duck typing est idiomatique en Python et est largement utilisé dans la bibliothèque standard — par exemple, len() fonctionne sur tout objet qui définit __len__, quelle que soit sa classe.

Le polymorphisme avec des fonctions et des boucles

Vous pouvez écrire une seule fonction qui traite différents types de manière uniforme grâce au polymorphisme. Considérons une application de dessin :

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        import math
        return math.pi * self.radius ** 2

    def describe(self):
        return f"Circle with radius {self.radius}"


class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def describe(self):
        return f"Rectangle {self.width}x{self.height}"


class Triangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

    def describe(self):
        return f"Triangle base={self.base} height={self.height}"


shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]

for shape in shapes:
    print(f"{shape.describe()}: area = {shape.area():.2f}")

# Circle with radius 5: area = 78.54
# Rectangle 4x6: area = 24.00
# Triangle base=3 height=8: area = 12.00

La boucle appelle area() et describe() sur chaque forme sans se soucier de la classe à laquelle appartient l'objet. L'ajout ultérieur d'une classe Pentagon ne nécessite que d'écrire la nouvelle classe — la boucle ne change pas.

La surcharge d'opérateurs

Les opérateurs arithmétiques et de comparaison de Python sont également polymorphiques. + sur des entiers additionne des nombres ; + sur des chaînes les concatène ; + sur des listes les fusionne. Python réalise cela grâce aux méthodes spéciales (dunder).

Vous pouvez faire en sorte que vos propres classes répondent aux opérateurs en définissant ces méthodes :

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"


v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)    # Vector(4, 6)
print(v1 * 3)     # Vector(3, 6)

Le même opérateur + se comporte maintenant différemment selon que les opérandes sont des entiers, des chaînes ou des objets Vector. C'est le polymorphisme au niveau des opérateurs.

Pour une plongée approfondie dans les méthodes spéciales de Python, voir Les méthodes magiques Python.

Le polymorphisme avec les classes de base abstraites

Les classes de base abstraites (ABC) poussent le duck typing un cran plus loin en imposant l'interface au moment de la définition de la classe. Si une sous-classe ne parvient pas à implémenter une méthode requise, Python lève une TypeError au moment où vous tentez de l'instancier.

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        """Return the area of the shape."""

    @abstractmethod
    def perimeter(self) -> float:
        """Return the perimeter of the shape."""


class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        import math
        return math.pi * self.radius ** 2

    def perimeter(self) -> float:
        import math
        return 2 * math.pi * self.radius


class Square(Shape):
    def __init__(self, side: float):
        self.side = side

    def area(self) -> float:
        return self.side ** 2

    def perimeter(self) -> float:
        return 4 * self.side


def print_info(shape: Shape) -> None:
    print(f"Area:      {shape.area():.2f}")
    print(f"Perimeter: {shape.perimeter():.2f}")


print_info(Circle(5))
# Area:      78.54
# Perimeter: 31.42

print_info(Square(4))
# Area:      16.00
# Perimeter: 16.00

Si vous tentez d'instancier une sous-classe qui n'implémente pas area() :

class Blob(Shape):
    pass   # forgot to implement area() and perimeter()

b = Blob()
# TypeError: Can't instantiate abstract class Blob with abstract methods area, perimeter

Les ABC vous offrent le filet de sécurité d'un contrat formel tout en permettant un polymorphisme à l'exécution.

ABC vs. Duck Typing — Lequel choisir ?

  • Le duck typing est plus simple et plus flexible. Préférez-le pour les petites bases de code internes et les scripts.
  • Les ABC rendent l'interface attendue explicite, détectent rapidement les bugs de méthodes manquantes et apparaissent dans l'auto-complétion des IDE et les vérificateurs de types. Préférez-les dans les grandes bases de code, les bibliothèques publiques et partout où vous souhaitez imposer un contrat.

Un exemple concret : le traitement des paiements

Le polymorphisme brille dans les architectures de type plugin. Considérons un système de paiement qui doit prendre en charge plusieurs fournisseurs :

from abc import ABC, abstractmethod


class PaymentProvider(ABC):
    @abstractmethod
    def charge(self, amount: float, currency: str) -> bool:
        """Attempt to charge the given amount. Return True on success."""

    @abstractmethod
    def refund(self, transaction_id: str) -> bool:
        """Refund a previous transaction. Return True on success."""


class StripeProvider(PaymentProvider):
    def charge(self, amount: float, currency: str) -> bool:
        print(f"[Stripe] Charged {amount} {currency}")
        return True

    def refund(self, transaction_id: str) -> bool:
        print(f"[Stripe] Refunded transaction {transaction_id}")
        return True


class PayPalProvider(PaymentProvider):
    def charge(self, amount: float, currency: str) -> bool:
        print(f"[PayPal] Charged {amount} {currency}")
        return True

    def refund(self, transaction_id: str) -> bool:
        print(f"[PayPal] Refunded transaction {transaction_id}")
        return True


def process_order(provider: PaymentProvider, amount: float) -> None:
    success = provider.charge(amount, "USD")
    if success:
        print("Order complete.")


process_order(StripeProvider(), 99.99)
# [Stripe] Charged 99.99 USD
# Order complete.

process_order(PayPalProvider(), 49.5)
# [PayPal] Charged 49.5 USD
# Order complete.

process_order ne sait pas et ne se soucie pas de savoir si elle reçoit un StripeProvider ou un PayPalProvider. Ajouter un nouveau CryptoProvider ne nécessite que d'écrire la nouvelle classe — rien d'autre ne change. C'est le polymorphisme offrant une extensibilité concrète.

Pièges courants

Piège 1 : Vérifier les types avec type() plutôt qu'isinstance()

Comparer type(obj) == Dog défait le polymorphisme car cela renvoie False pour les sous-classes. Préférez isinstance(obj, Animal), qui renvoie True pour Dog et toute future sous-classe :

class Animal:
    pass

class Dog(Animal):
    pass

d = Dog()

# Fragile — breaks for subclasses:
print(type(d) == Animal)   # False

# Correct — subclass-aware:
print(isinstance(d, Animal))  # True

Piège 2 : Signatures de méthodes incohérentes

Le polymorphisme suppose que toutes les implémentations d'une méthode acceptent les mêmes arguments. Si Dog.speak() requiert un argument que Cat.speak() n'accepte pas, les appelants qui les traitent de manière uniforme rencontreront des erreurs :

# Inconsistent — will cause errors in a loop
class Dog:
    def speak(self, volume):   # extra argument!
        return f"Woof at volume {volume}"

class Cat:
    def speak(self):
        return "Meow!"

Gardez des signatures de méthodes cohérentes entre les classes polymorphiques.

Piège 3 : Arguments par défaut mutables dans les méthodes surchargées

Il s'agit d'un piège plus général en Python, mais qui fait trébucher dans les hiérarchies de classes : n'utilisez jamais un objet mutable (liste, dict) comme valeur d'argument par défaut — il est créé une seule fois et partagé entre tous les appels.

# Bug: the list is shared across all instances
class Item:
    def __init__(self, tags=[]):   # BAD
        self.tags = tags

# Fix:
class Item:
    def __init__(self, tags=None):
        self.tags = tags if tags is not None else []

Résumé

ConceptCe que cela signifie
Surcharge de méthodesUne sous-classe fournit sa propre version d'une méthode parente
Dispatch dynamiquePython choisit la bonne version de méthode à l'exécution
Duck typingTout objet possédant les bonnes méthodes fonctionne, quelle que soit sa classe
Surcharge d'opérateursLes méthodes dunder (__add__, __len__, …) rendent les opérateurs polymorphiques
Classes de base abstraitesImposent formellement l'interface que les sous-classes doivent implémenter

Le polymorphisme est ce qui rend le code extensible sans modification. Écrivez des fonctions qui dépendent du comportement (noms de méthodes), et non de types concrets, et votre code s'adapte naturellement aux nouvelles classes sans changements.

Pratique

Pratique
Which of the following statements about Python polymorphism are correct?
Which of the following statements about Python polymorphism are correct?
Was this page helpful?