2.22. Héritage

L’héritage permet à une classe d”étendre une autre classe – de partir de l’ensemble de ses méthodes et attributs, puis d’ajouter ou de redéfinir ce qui diffère. L’originale est appelée la base (ou le parent) ; l’extension est appelée la sous-classe.

Une classe de base Shape pointant vers le bas vers une sous-classe Square ; Square redéfinit une méthode (area) et hérite du reste.

Square réutilise chaque méthode de Shape et redéfinit celles qu’elle a besoin de spécialiser.

2.22.1. Définir une sous-classe

La classe de base se place entre les parenthèses sur la ligne class :

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

    def describe(self):
        return self.name + " with area " + str(self.area())

    def area(self):
        return 0

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")
        self.side = side

    def area(self):
        return self.side * self.side

s = Square(4)
print(s.describe())

Sortie

square with area 16

Square hérite de describe de Shape sans modification et redéfinit area pour renvoyer une valeur réelle. Le describe de la base fonctionne toujours car il appelle self.area() – qui se résout vers la redéfinition à l’exécution.

2.22.2. super()

super() renvoie un proxy qui permet à une méthode d’appeler la version de la classe de base d’une méthode. L’usage le plus courant est d’appeler le __init__ de la base depuis le __init__ d’une sous-classe afin que la base puisse initialiser ses propres attributs :

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")    # run Shape.__init__
        self.side = side

Oublier super().__init__ est une source fréquente de bogues : les attributs que la base aurait définis ne sont jamais définis, et les méthodes héritées qui en dépendent plantent plus tard.

2.22.2.1. La classe de base implicite

Chaque classe hérite implicitement de object, la racine de la hiérarchie de types de Python. Des méthodes comme __repr__, __eq__ et toute la machinerie derrière l’accès aux attributs proviennent de là, même lorsqu’une classe ne déclare aucune base. Écrire class Foo(object): explicitement et écrire class Foo: sont équivalents en Python moderne ; la seconde est la forme conventionnelle.

2.22.3. Quand l’héritage aide – et quand il n’aide pas

Utilisez l’héritage lorsqu’une classe est véritablement une sorte plus spécifique d’une autre, partageant la plupart de son comportement. Le test classique est la vérification « est-un » : un Square est un Shape.

Lorsque deux classes se trouvent simplement à partager quelques fonctions auxiliaires, l’héritage est excessif. Ayez plutôt recours à la composition : faites qu’une classe contienne une instance de l’autre comme attribut et l’utilise à travers cet attribut. La forme est « a-un », et non « est-un » :

class Logger:
    def log(self, msg):
        print("[log]", msg)

# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
    def run(self):
        self.log("starting")

# composition: Worker HAS-A Logger
class WorkerComposes:
    def __init__(self):
        self.logger = Logger()

    def run(self):
        self.logger.log("starting")

Les deux fonctionnent. La version par composition est généralement préférable car elle :

  • Garde les interfaces réduites. WorkerComposes n’expose que run et logger. WorkerInherits expose aussi log – les appelants peuvent écrire worker.log(...) directement, que ce soit voulu ou non.

  • Découple les durées de vie. Le logger peut être remplacé, partagé entre workers ou construit différemment pour chaque worker, tout cela sans toucher à la classe Worker. Une sous-classe est soudée à sa base.

  • Évite les collisions de noms de méthodes. Deux bases qui définissent toutes deux run entrent en conflit lorsqu’une classe tente d’hériter des deux ; deux attributs n’entrent jamais en conflit.

Une règle pratique :

  • Utilisez l’héritage lorsque la sous-classe est réellement une sorte de la base et que chaque méthode de la base a aussi du sens sur elle – Square est un Shape ; MemoryError est une Exception.

  • Utilisez la composition pour tout le reste – lorsque vous voulez simplement le comportement d’une autre classe, et non son identité. La plupart du code réel utilise la composition bien plus que l’héritage.