L'héritage de Python est-il un style d'héritage «est-un» ou un style de composition?

10

Étant donné que Python permet l'héritage multiple, à quoi ressemble l'héritage idiomatique en Python?

Dans les langages à héritage unique, comme Java, l'héritage serait utilisé lorsque vous pourriez dire qu'un objet "est-a" d'un autre objet et que vous souhaitez partager du code entre les objets (de l'objet parent à l'objet enfant). Par exemple, vous pourriez dire que Dogc'est Animal:

public class Animal {...}
public class Dog extends Animal {...}

Mais comme Python prend en charge l'héritage multiple, nous pouvons créer un objet en composant de nombreux autres objets ensemble. Considérez l'exemple ci-dessous:

class UserService(object):
    def validate_credentials(self, username, password):
        # validate the user credentials are correct
        pass


class LoggingService(object):
    def log_error(self, error):
        # log an error
        pass


class User(UserService, LoggingService):
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def authenticate(self):
        if not super().validate_credentials(self.username, self.password):
            super().log_error('Invalid credentials supplied')
            return False
         return True

Est-ce une utilisation acceptable ou bonne de l'héritage multiple en Python? Au lieu de dire que l'héritage est lorsqu'un objet "est-a" d'un autre objet, nous créons un Usermodèle composé de UserServiceet LoggingService.

Toute la logique des opérations de base de données ou de réseau peut être séparée du Usermodèle en les plaçant dans l' UserServiceobjet et en conservant toute la logique de connexion dans le LoggingService.

Je vois certains problèmes avec cette approche:

  • Est-ce que cela crée un objet divin? Puisqu'il Userhérite de, ou est composé de, UserServiceet LoggingServicesuit-il vraiment le principe de la responsabilité unique?
  • Afin d'accéder aux méthodes sur un objet parent / suivant (par exemple, UserService.validate_credentialsnous devons utiliser super. Cela rend un peu plus difficile de voir quel objet va gérer cette méthode et n'est pas aussi clair que, par exemple , instancier UserServiceet faire quelque chose commeself.user_service.validate_credentials

Quelle serait la façon Pythonique d'implémenter le code ci-dessus?

Iain
la source

Réponses:

9

L'héritage de Python est-il un style d'héritage «est-un» ou un style de composition?

Python prend en charge les deux styles. Vous démontrez la relation has-a de la composition, où un utilisateur dispose d'une fonctionnalité de journalisation d'une source et d'une validation des informations d'identification d'une autre source. Les bases LoggingServiceet UserServicesont des mixins: elles fournissent des fonctionnalités et ne sont pas destinées à être instanciées par elles-mêmes.

En composant des mixins dans le type, vous avez un utilisateur qui peut se connecter, mais qui doit ajouter sa propre fonctionnalité d'instanciation.

Cela ne signifie pas que l'on ne peut pas s'en tenir à l'héritage unique. Python prend également cela en charge. Si votre capacité de développement est entravée par la complexité de l'héritage multiple, vous pouvez l'éviter jusqu'à ce que vous vous sentiez plus à l'aise ou que vous arriviez à un stade de la conception où vous croyez que cela vaut le coup.

Est-ce que cela crée un objet divin?

La journalisation semble quelque peu tangentielle - Python a son propre module de journalisation avec un objet logger, et la convention est qu'il y a un logger par module.

Mais mettez le module de journalisation de côté. Peut-être que cela viole la responsabilité unique, mais peut-être, dans votre contexte spécifique, il est crucial de définir un utilisateur. La discrétisation de la responsabilité peut être controversée. Mais le principe plus large est que Python permet à l'utilisateur de prendre la décision.

Est-ce super moins clair?

supern'est nécessaire que lorsque vous devez déléguer à un parent dans l'ordre de résolution de méthode (MRO) de l'intérieur d'une fonction du même nom. L'utiliser au lieu de coder en dur un appel à la méthode du parent est une meilleure pratique. Mais si vous n'alliez pas coder en dur le parent, vous n'en avez pas besoin super.

Dans votre exemple ici, vous n'avez qu'à faire self.validate_credentials. selfn'est pas plus clair, de votre point de vue. Ils suivent tous les deux le MRO. J'utiliserais simplement chacun le cas échéant.

Si vous aviez appelé authenticate, à la validate_credentialsplace, vous auriez dû utiliser super(ou coder en dur le parent) pour éviter une erreur de récursivité.

Suggestion de code alternatif

Donc, en supposant que la sémantique est OK (comme la journalisation), ce que je ferais, en classe User:

    def validate_credentials(self): # changed from "authenticate" to 
                                    # demonstrate need for super
        if not super().validate_credentials(self.username, self.password):
            # just use self on next call, no need for super:
            self.log_error('Invalid credentials supplied') 
            return False
        return True
Aaron Hall
la source
1
Je ne suis pas d'accord. L'héritage crée toujours une copie de l'interface publique d'une classe dans ses sous-classes. Ce n'est pas une relation "a-un". Il s'agit d'un sous-typage, clair et simple, qui ne convient donc pas à l'application décrite.
Jules
@Jules Avec quoi êtes-vous en désaccord? J'ai dit beaucoup de choses qui peuvent être démontrées et j'ai tiré des conclusions qui suivent logiquement. Vous êtes incorrect lorsque vous dites: « L' héritage crée toujours une copie de l'interface publique d'une classe dans ses sous - classes. » En Python, il n'y a pas de copie - les méthodes sont recherchées dynamiquement selon l'ordre de résolution des méthodes de l'algorithme C3 (MRO).
Aaron Hall
1
Il ne s'agit pas de détails spécifiques sur le fonctionnement de l'implémentation, mais de l'apparence de l'interface publique de la classe. Dans le cas de l'exemple, les Userobjets ont dans leurs interfaces non seulement les membres définis dans la Userclasse, mais aussi ceux définis dans UserServiceet LoggingService. Ce n'est pas une relation "has-a", car l'interface publique est copiée (bien que ce ne soit pas via une copie directe, mais plutôt par une recherche indirecte des interfaces des superclasses).
Jules
Has-a signifie Composition. Les mixins sont une forme de composition. La classe User n'est pas un UserService ou LoggingService, mais elle a cette fonctionnalité. Je pense que l'héritage de Python est plus différent de Java que vous ne le pensez.
Aaron Hall
@AaronHall Vous simplifiez trop (cela a tendance à vous contredire une autre réponse , que j'ai trouvée par hasard). Du point de vue de la relation de sous-type, un utilisateur est à la fois un UserService et un LoggingService. Maintenant, l'esprit ici est de composer des fonctionnalités pour qu'un Utilisateur ait telle ou telle fonctionnalité. Les mixins en général n'ont pas besoin d'être implémentés avec l'héritage multiple. Cependant, c'est la façon habituelle de le faire en Python.
coredump
-1

Outre le fait qu'il autorise plusieurs superclasses, l'héritage de Python n'est pas substantiellement différent de Java, c'est-à-dire que les membres d'une sous-classe sont également membres de chacun de leurs supertypes [1]. Le fait que Python utilise le type de canard ne fait aucune différence non plus: votre sous-classe a tous les membres de ses superclasses, elle peut donc être utilisée par n'importe quel code qui pourrait utiliser ces sous-classes. Le fait que l'héritage multiple soit effectivement implémenté en utilisant la composition est un redingue: cette copie automatisée des propriétés d'une classe à une autre est le problème, et peu importe qu'il utilise la composition ou devine comme par magie comment les membres sont supposés au travail: les avoir c'est mal.

Oui, cela viole une seule responsabilité, car vous donnez à vos objets la possibilité d'effectuer des actions qui ne font pas logiquement partie de ce pour quoi ils sont conçus. Oui, cela crée des objets "dieu", ce qui est essentiellement une autre façon de dire la même chose.

Lors de la conception de systèmes orientés objet en Python, la même maxime prêchée par les livres de conception Java s'applique également: préférez la composition à l'héritage. Il en va de même pour (la plupart [2]) des autres systèmes à héritage multiple.

[1]: on pourrait appeler cela une relation "est-une", bien que je n'aime pas personnellement le terme car il suggère l'idée de modéliser le monde réel, et la modélisation orientée objet n'est pas la même chose que le monde réel.

[2]: Je ne suis pas sûr de C ++. C ++ prend en charge "l'héritage privé" qui est essentiellement une composition sans avoir besoin de spécifier un nom de champ lorsque vous souhaitez utiliser les membres publics de la classe héritée. Cela n'affecte pas du tout l'interface publique de la classe. Je ne comme l' utiliser, mais je ne vois aucune bonne raison de ne pas.

Jules
la source