web-dev-qa-db-fra.com

À quoi sert l'héritage en Python?

Supposons que vous ayez la situation suivante

#include <iostream>

class Animal {
public:
    virtual void speak() = 0;
};

class Dog : public Animal {
    void speak() { std::cout << "woff!" <<std::endl; }
};

class Cat : public Animal {
    void speak() { std::cout << "meow!" <<std::endl; }
};

void makeSpeak(Animal &a) {
    a.speak();
}

int main() {
    Dog d;
    Cat c;
    makeSpeak(d);
    makeSpeak(c);
}

Comme vous pouvez le voir, makeSpeak est une routine qui accepte un objet Animal générique. Dans ce cas, Animal est assez similaire à une interface Java, car il ne contient qu'une méthode virtuelle pure. MakeSpeak ne connaît pas la nature de l'animal qu'il passe. Il lui envoie simplement le signal "Speak" et laisse la liaison tardive pour prendre soin de la méthode à appeler: soit Cat :: speak () ou Dog :: speak (). Cela signifie que, pour makeSpeak, la connaissance de la sous-classe est réellement passé n'est pas pertinent.

Mais qu'en est-il de Python? Voyons le code du même cas en Python. Veuillez noter que j'essaie d'être aussi similaire que possible au cas C++ pendant un moment:

class Animal(object):
    def speak(self):
        raise NotImplementedError()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

Maintenant, dans cet exemple, vous voyez la même stratégie. Vous utilisez l'héritage pour tirer parti du concept hiérarchique des chiens et des chats étant des animaux. Mais en Python, il n'y a pas besoin de cette hiérarchie. Cela fonctionne aussi bien

class Dog:
    def speak(self):
        print "woff!"

class Cat:
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

Dans Python vous pouvez envoyer le signal "parler" à n'importe quel objet que vous voulez. Si l'objet est capable de le gérer, il sera exécuté, sinon il lèvera une exception. Supposons que vous ajoutez un avion de classe aux deux codes et soumettez un objet Airplane à makeSpeak. Dans le cas C++, il ne sera pas compilé, car Airplane n'est pas une classe dérivée d'Animal. Dans le cas Python, il lèvera une exception lors de l'exécution, ce qui pourrait même être un comportement attendu.

De l'autre côté, supposons que vous ajoutiez une classe MouthOfTruth avec une méthode speak (). Dans le cas C++, soit vous devrez refactoriser votre hiérarchie, soit vous devrez définir une méthode makeSpeak différente pour accepter les objets MouthOfTruth, ou dans Java vous pouvez extraire le comportement dans un CanSpeakIface et implémenter l'interface pour chacun. Il existe de nombreuses solutions ...

Ce que je voudrais souligner, c'est que je n'ai pas encore trouvé une seule raison d'utiliser l'héritage dans Python (en dehors des cadres et des arbres d'exceptions, mais je suppose que des stratégies alternatives existent)) . vous n'avez pas besoin d'implémenter une hiérarchie dérivée de la base pour fonctionner de manière polymorphe. Si vous souhaitez utiliser l'héritage pour réutiliser l'implémentation, vous pouvez accomplir la même chose via le confinement et la délégation, avec l'avantage supplémentaire que vous pouvez la modifier au moment de l'exécution, et vous définissez clairement l'interface du contenu, sans risquer d'effets secondaires inattendus.

Donc, à la fin, la question se pose: quel est l'intérêt de l'héritage en Python?

Edit : merci pour les réponses très intéressantes. En effet, vous pouvez l'utiliser pour la réutilisation de code, mais je suis toujours prudent lors de la réutilisation de l'implémentation. En général, j'ai tendance à faire des arbres d'héritage très peu profonds ou aucun arbre du tout, et si une fonctionnalité est commune, je la refactorise comme une routine de module commune, puis je l'appelle à partir de chaque objet. Je vois l'avantage d'avoir un seul point de changement (par exemple, au lieu d'ajouter à Dog, Cat, Moose et ainsi de suite, j'ajoute simplement à Animal, qui est l'avantage de base de l'héritage), mais vous pouvez obtenir la même chose avec une chaîne de délégation (par exemple à la JavaScript). Je ne prétends pas que c'est mieux, juste une autre façon.

J'ai également trouvé n article similaire à cet égard.

81
Stefano Borini

Vous faites référence au type de canard au moment de l'exécution comme héritage "prioritaire", mais je pense que l'héritage a ses propres avantages en tant qu'approche de conception et d'implémentation, faisant partie intégrante de la conception orientée objet. À mon humble avis, la question de savoir si vous pouvez réaliser quelque chose autrement n'est pas très pertinente, car en fait, vous pourriez coder Python sans classes, fonctions et plus, mais la question est de savoir comment bien conçu, robuste et lisible votre code sera.

Je peux donner deux exemples pour lesquels l'héritage est la bonne approche à mon avis, je suis sûr qu'il y en a plus.

Premièrement, si vous codez judicieusement, votre fonction makeSpeak peut vouloir valider que son entrée est en effet un Animal, et pas seulement que "il peut parler", auquel cas la méthode la plus élégante serait d'utiliser l'héritage. Encore une fois, vous pouvez le faire d'autres façons, mais c'est la beauté de la conception orientée objet avec héritage - votre code vérifiera "vraiment" si l'entrée est un "animal".

Deuxièmement, et clairement plus simple, l'encapsulation est une autre partie intégrante de la conception orientée objet. Cela devient pertinent lorsque l'ancêtre a des membres de données et/ou des méthodes non abstraites. Prenons l'exemple idiot suivant, dans lequel l'ancêtre a une fonction (speak_twice) qui invoque une fonction alors abstraite:

class Animal(object):
    def speak(self):
        raise NotImplementedError()

    def speak_twice(self):
        self.speak()
        self.speak()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

En supposant "speak_twice" est une fonctionnalité importante, vous ne voulez pas la coder à la fois dans Dog et Cat, et je suis sûr que vous pouvez extrapoler cet exemple. Bien sûr, vous pouvez implémenter une fonction Python stand-alone qui acceptera un objet de type canard, vérifier s'il a une fonction speak et l'invoquer deux fois, mais c'est à la fois non élégant et manque le point numéro 1 (valider c'est un Animal). Pire encore, et pour renforcer l'exemple d'encapsulation, que se passe-t-il si une fonction membre de la classe descendante souhaite utiliser "speak_twice"?

Cela devient encore plus clair si la classe ancêtre a un membre de données, par exemple "number_of_legs" qui est utilisé par des méthodes non abstraites dans l'ancêtre comme "print_number_of_legs", mais est initié dans le constructeur de la classe descendante (par exemple Dog l'initialiserait avec 4 tandis que Snake l'initierait avec 0).

Encore une fois, je suis sûr qu'il existe d'innombrables autres exemples, mais fondamentalement, tous les logiciels (assez grands) basés sur une conception orientée objet solide nécessiteront l'héritage.

79
Roee Adler

L'héritage dans Python concerne la réutilisation de code. Factorisez les fonctionnalités communes dans une classe de base et implémentez différentes fonctionnalités dans les classes dérivées.

12
Roberto Bonvallet

L'héritage en Python est plus pratique que toute autre chose. Je trouve qu'il est préférable de fournir une classe avec un "comportement par défaut".

En effet, il existe une importante communauté de développeurs Python qui s'opposent à l'utilisation de l'héritage du tout. Quoi que vous fassiez, ne vous contentez pas d'en faire trop. Avoir une hiérarchie de classes trop compliquée est une certitude moyen d'être étiqueté "programmeur Java", et vous ne pouvez pas avoir ça. :-)

10
Jason Baker

Je pense que le point d'héritage dans Python n'est pas de faire compiler le code, c'est pour la vraie raison de l'héritage qui étend la classe dans une autre classe enfant, et pour remplacer la logique dans le Cependant, le canard tapant Python rend le concept "interface" inutile, car vous pouvez simplement vérifier si la méthode existe avant l'invocation sans avoir besoin d'utiliser une interface pour limiter la structure de la classe.

8
bashmohandes

Je pense qu'il est très difficile de donner une réponse significative et concrète avec de tels exemples abstraits ...

Pour simplifier, il existe deux types d'héritage: l'interface et l'implémentation. Si vous avez besoin d'hériter de l'implémentation, alors python n'est pas si différent que les langages typés OO langages comme C++).

L'héritage de l'interface est là où il y a une grande différence, avec des conséquences fondamentales pour la conception de votre logiciel selon mon expérience. Des langages comme Python ne vous oblige pas à utiliser l'héritage dans ce cas, et éviter l'héritage est un bon point dans la plupart des cas, car il est très difficile de corriger un mauvais choix de conception par la suite. C'est un point bien connu soulevé dans tout bon OOP livre.

Il y a des cas où l'utilisation de l'héritage pour les interfaces est recommandée en Python, par exemple pour les plug-ins, etc ... Pour ces cas, Python 2.5 et ci-dessous manque d'une approche élégante "intégrée") , et plusieurs grands frameworks ont conçu leurs propres solutions (zope, trac, twister). Python 2.6 et supérieur a classes ABC pour résoudre ce problème .

7
David Cournapeau

Ce n'est pas l'héritage que le typage du canard rend inutile, ce sont des interfaces - comme celle que vous avez choisie pour créer une classe animale tout abstraite.

Si vous aviez utilisé une classe d'animaux qui introduisait un comportement réel pour ses descendants, alors les classes de chiens et de chats qui introduisaient un comportement supplémentaire, il y aurait une raison pour les deux classes. Ce n'est que dans le cas de la classe ancêtre qui ne fournit aucun code réel aux classes descendantes que votre argument est correct.

Parce que Python peut connaître directement les capacités de n'importe quel objet, et parce que ces capacités sont mutables au-delà de la définition de classe, l'idée d'utiliser une interface abstraite pure pour "dire" au programme quelles méthodes peuvent être appelées est quelque peu inutile. Mais ce n'est pas le seul, ni même le principal point d'héritage.

6
Larry Lustig

En C++/Java/etc, le polymorphisme est provoqué par l'héritage. Abandonnez cette croyance erronée et les langages dynamiques s'ouvrent à vous.

Essentiellement, en Python il n'y a pas d'interface autant que "la compréhension que certaines méthodes sont appelables". Assez ondulé à la main et à consonance académique, non? Cela signifie que parce que vous appelez "parler" vous vous attendez clairement à ce que l'objet ait une méthode de "parler". Simple, hein? C'est très Liskov-ian dans la mesure où les utilisateurs d'une classe définissent son interface, un bon concept de conception qui vous mène à un TDD plus sain.

Donc, ce qui reste est, comme une autre affiche a poliment réussi à éviter de le dire, une astuce de partage de code. Vous pouvez écrire le même comportement dans chaque classe "enfant", mais ce serait redondant. Plus facile à hériter ou à mélanger des fonctionnalités invariantes dans la hiérarchie d'héritage. Un code plus petit et plus sec est généralement meilleur.

5
agileotter

Vous pouvez contourner l'héritage dans Python et à peu près n'importe quel autre langage. Il s'agit cependant de réutilisation et de simplification de code.

Juste une astuce sémantique, mais après avoir construit vos classes et classes de base, vous n'avez même pas besoin de savoir ce qui est possible avec votre objet pour voir si vous pouvez le faire.

Disons que vous avez un chien qui est un animal de la sous-classe.

command = raw_input("What do you want the dog to do?")
if command in dir(d): getattr(d,command)()

Si tout ce que l'utilisateur a tapé est disponible, le code exécutera la méthode appropriée.

En utilisant cela, vous pouvez créer la combinaison de monstruosité hybride Mammifère/Reptile/Oiseau que vous voulez, et maintenant vous pouvez lui faire dire `` Bark! '' en volant et en tirant sa langue fourchue et il le manipulera correctement! Aie du plaisir avec ça!

1
mandroid

Un autre petit point est que le 3ème exemple de l'op, vous ne pouvez pas appeler isinstance (). Par exemple en passant votre 3ème exemple à un autre objet qui prend et de type "Animal" un appel parle dessus. Si vous le faites, vous ne devriez pas vérifier le type de chien, le type de chat, etc. Je ne sais pas si la vérification d'instance est vraiment "Pythonic", en raison d'une liaison tardive. Mais alors, vous devrez implémenter d'une manière que l'AnimalControl n'essaye pas de jeter des types de Cheeseburger dans le camion, car les Cheeseburgers ne parlent pas.

class AnimalControl(object):
    def __init__(self):
        self._animalsInTruck=[]

    def catachAnimal(self,animal):
        if isinstance(animal,Animal):
            animal.speak()  #It's upset so it speak's/maybe it should be makesNoise
            if not self._animalsInTruck.count <=10:
                self._animalsInTruck.append(animal) #It's then put in the truck.
            else:
                #make note of location, catch you later...
        else:
            return animal #It's not an Animal() type / maybe return False/0/"message"
1
yedevtxt

Je ne vois pas grand chose à l'héritage.

Chaque fois que j'ai utilisé l'héritage dans des systèmes réels, je me suis brûlé parce que cela conduisait à un réseau de dépendances enchevêtré, ou j'ai simplement réalisé à temps que je serais beaucoup mieux sans lui. Maintenant, je l'évite autant que possible. Je n'en ai tout simplement jamais l'usage.

class Repeat:
    "Send a message more than once"
    def __init__(repeat, times, do):
        repeat.times = times
        repeat.do = do

    def __call__(repeat):
        for i in xrange(repeat.times):
             repeat.do()

class Speak:
    def __init__(speak, animal):
        """
        Check that the animal can speak.

        If not we can do something about it (e.g. ignore it).
        """
        speak.__call__ = animal.speak

    def twice(speak):
        Repeat(2, speak)()

class Dog:
     def speak(dog):
         print "Woof"

class Cat:
     def speak(cat):
         print "Meow"

>>> felix = Cat()
>>> Speak(felix)()
Meow

>>> fido = Dog()
>>> speak = Speak(fido)
>>> speak()
Woof

>>> speak.twice()
Woof

>>> speak_twice = Repeat(2, Speak(felix))
>>> speak_twice()
Meow
Meow

Lors d'une conférence de presse, une question a été posée à James Gosling: "Si vous pouviez revenir en arrière et faire Java différemment, que laisseriez-vous de côté?". Sa réponse était "Classes", mais il était sérieux et a expliqué que ce n'était pas vraiment les classes qui étaient le problème mais l'héritage.

Je le vois comme une dépendance à la drogue - cela vous donne une solution rapide qui fait du bien, mais au final, cela vous gâche. J'entends par là que c'est un moyen pratique de réutiliser le code, mais cela force un couplage malsain entre la classe enfant et la classe parent. Les modifications apportées au parent peuvent briser l'enfant. L'enfant dépend du parent pour certaines fonctionnalités et ne peut pas modifier cette fonctionnalité. Par conséquent, la fonctionnalité fournie par l'enfant est également liée au parent - vous ne pouvez avoir que les deux.

Il est préférable de fournir une seule classe orientée client pour une interface qui implémente l'interface, en utilisant la fonctionnalité d'autres objets qui sont composés au moment de la construction. En faisant cela via des interfaces correctement conçues, tout couplage peut être éliminé et nous fournissons une API hautement composable (ce n'est pas nouveau - la plupart des programmeurs le font déjà, mais pas assez). Notez que la classe d'implémentation ne doit pas simplement exposer des fonctionnalités, sinon le client doit simplement utiliser directement les classes composées - il doit faire quelque chose de nouveau en combinant cette fonctionnalité.

Il y a l'argument du camp de l'héritage selon lequel les implémentations de délégation pure souffrent parce qu'elles nécessitent beaucoup de méthodes de "collage" qui transmettent simplement des valeurs à travers une "chaîne" de délégation. Cependant, cela réinvente simplement une conception de type héritage utilisant la délégation. Les programmeurs ayant trop d'années d'exposition aux conceptions basées sur l'héritage sont particulièrement vulnérables à tomber dans ce piège, car, sans le savoir, ils réfléchiront à la façon dont ils implémenteraient quelque chose en utilisant l'héritage, puis le convertiraient en délégation.

Une bonne séparation des problèmes comme le code ci-dessus ne nécessite pas de méthodes de collage, car chaque étape est en fait ajout de valeur, donc ce ne sont pas vraiment des méthodes de `` collage '' (si elles n'ajoutent pas de valeur, le design est défectueux).

Cela se résume à ceci:

  • Pour le code réutilisable, chaque classe ne devrait faire qu'une seule chose (et bien le faire).

  • L'héritage crée des classes qui font plus d'une chose, car elles sont mélangées aux classes parentes.

  • Par conséquent, l'utilisation de l'héritage rend les classes difficiles à réutiliser.

1
Mike A

Les classes dans Python sont simplement des façons de regrouper un tas de fonctions et de données .. Elles sont différentes des classes en C++ et autres ..

J'ai surtout vu l'héritage utilisé pour remplacer les méthodes de la super-classe. Par exemple, peut-être une utilisation plus Python de l'héritage serait ..

from world.animals import Dog

class Cat(Dog):
    def speak(self):
        print "meow"

Bien sûr, les chats ne sont pas un type de chien, mais j'ai cette classe (tierce) Dog qui fonctionne parfaitement, sauf la speak méthode que je veux remplacer - cela évite de réimplémenter la classe entière, juste pour miauler. Encore une fois, alors que Cat n'est pas un type de Dog, mais un chat hérite de beaucoup d'attributs ..

Un bien meilleur exemple (pratique) de substitution d'une méthode ou d'un attribut est la façon dont vous modifiez l'agent utilisateur pour urllib. Vous sous-classe essentiellement urllib.FancyURLopener et changez l'attribut de version ( de la documentation ):

import urllib

class AppURLopener(urllib.FancyURLopener):
    version = "App/1.7"

urllib._urlopener = AppURLopener()

Une autre manière d'utiliser les exceptions est pour les exceptions, lorsque l'héritage est utilisé d'une manière plus "correcte":

class AnimalError(Exception):
    pass

class AnimalBrokenLegError(AnimalError):
    pass

class AnimalSickError(AnimalError):
    pass

..vous pouvez alors intercepter AnimalError pour intercepter toutes les exceptions qui en héritent, ou une spécifique comme AnimalBrokenLegError

0
dbr