web-dev-qa-db-fra.com

Meilleure pratique d'héritage: * args, ** kwargs ou spécifiant explicitement des paramètres

Je me trouve souvent moi-même en train d'écraser les méthodes d'une classe parente et je ne peux jamais décider si je devrais explicitement lister des paramètres donnés ou simplement utiliser une construction globale *args, **kwargs. Une version est-elle meilleure que l'autre? Y a-t-il une meilleure pratique? Quels sont les (dés) avantages qui me manquent?

class Parent(object):

    def save(self, commit=True):
        # ...

class Explicit(Parent):

    def save(self, commit=True):
        super(Explicit, self).save(commit=commit)
        # more logic

class Blanket(Parent):

    def save(self, *args, **kwargs):
        super(Blanket, self).save(*args, **kwargs)
        # more logic

Avantages perçus de la variante explicite

  • Plus explicite (Zen of Python)
  • plus facile à saisir
  • paramètres de fonction facilement accessibles

Avantages perçus de la variante de couverture

  • plus sec
  • la classe parente est facilement interchangeable
  • le changement des valeurs par défaut dans la méthode parent est propagé sans toucher à un autre code
37
Maik Hoepfel

Principe de substitution de Liskov

Généralement, vous ne voulez pas que la signature de votre méthode varie en types dérivés. Cela peut poser problème si vous souhaitez échanger l’utilisation des types dérivés. Ceci est souvent désigné sous le nom de principe de substitution de Liskov .

Avantages des signatures explicites

En même temps, je ne pense pas qu'il soit correct pour toutes vos méthodes d'avoir une signature de *args, **kwargs. Signatures explicites:

  • aide à documenter la méthode à travers de bons noms d'arguments
  • aide à documenter la méthode en spécifiant quels arguments sont requis et lesquels ont des valeurs par défaut
  • fournir une validation implicite (les arguments obligatoires manquants jettent des exceptions évidentes)

Arguments de longueur variable et couplage

Ne confondez pas les arguments de longueur variable avec les bonnes pratiques de couplage. Il devrait exister une certaine cohésion entre une classe parente et des classes dérivées, sans quoi elles ne seraient pas liées les unes aux autres. Il est normal que le code associé entraîne un couplage qui reflète le niveau de cohésion. 

Emplacements où utiliser des arguments de longueur variable

L'utilisation d'arguments de longueur variable ne devrait pas être votre première option. Il devrait être utilisé quand vous avez une bonne raison comme:

  • Définir un wrapper de fonction (c'est-à-dire un décorateur).
  • Définir une fonction polymorphe paramétrique.
  • Lorsque les arguments que vous pouvez prendre sont réellement variables, par exemple (fonction de connexion généralisée à la base de données). Les fonctions de connexion à la base de données prennent généralement un chaîne de connexion sous différentes formes, à la fois sous forme d'argument unique et sous forme d'argument multiple. Il existe également différents ensembles d'options pour différentes bases de données.
  • ...

Faites-vous quelque chose de mal?

Si vous constatez que vous créez souvent des méthodes qui prennent beaucoup d'arguments ou des méthodes dérivées avec différentes signatures, vous pouvez avoir un problème plus important dans la façon dont vous organisez votre code.

29
dietbuddha

Mon choix serait:

class Child(Parent):

    def save(self, commit=True, **kwargs):
        super(Child, self).save(commit, **kwargs)
        # more logic

Cela évite d’avoir accès aux arguments de commit depuis *args et **kwargs et il garde les choses en sécurité si la signature de Parent:save change (par exemple en ajoutant un nouvel argument par défaut).

Update : Dans ce cas, le fait d'avoir * args peut causer des problèmes si un nouvel argument de position est ajouté au parent. Je ne garderais que **kwargs et ne gérerais que de nouveaux arguments avec des valeurs par défaut. Cela éviterait les erreurs de se propager.

13
luc

Si vous êtes certain que Child gardera la signature, l'approche explicite est certainement préférable, mais lorsque Child modifiera la signature, je préfère personnellement utiliser les deux approches:

class Parent(object):
    def do_stuff(self, a, b):
        # some logic

class Child(Parent):
    def do_stuff(self, c, *args, **kwargs):
        super(Child, self).do_stuff(*args, **kwargs)
        # some logic with c

De cette façon, les modifications de la signature sont assez lisibles dans Child, tandis que la signature originale est tout à fait lisible dans Parent.

À mon avis, c’est également le meilleur moyen d’avoir un héritage multiple, car appeler super plusieurs fois est assez dégoûtant quand on n’a pas d’arguments ni de kwargs.

Pour ce qui en vaut la peine, c’est aussi la méthode préférée dans de nombreuses librairies et frameworks Python (Django, Tornado, Requests, Markdown, pour n’en nommer que quelques-uns). Bien que l'on ne devrait pas baser ses choix sur de telles choses, j'insinue simplement que cette approche est assez répandue.

4
dmg

Pas vraiment une réponse, mais plutôt une remarque: si vous voulez vraiment vous assurer que les valeurs par défaut de la classe parent sont propagées aux classes enfants, vous pouvez faire quelque chose comme:

class Parent(object):

    default_save_commit=True
    def save(self, commit=default_save_commit):
        # ...

class Derived(Parent):

    def save(self, commit=Parent.default_save_commit):
        super(Derived, self).save(commit=commit)

Cependant, je dois admettre que cela a l'air assez moche et que je ne l'utiliserais que si je sens que j'en ai vraiment besoin.

3

Je préfère les arguments explicites, car l'auto-complétion vous permet de voir la signature de la méthode lors de l'appel de la fonction.

2
GeneralBecos

En plus des autres réponses:

Avoir des arguments variables peut "découpler" le parent de l'enfant, mais crée un couplage entre l'objet créé et le parent, ce qui, selon moi, est pire, car vous avez créé un couple "à longue distance" (plus difficile à repérer, plus difficile à identifier). maintenir, car vous pouvez créer plusieurs objets dans votre application)

Si vous recherchez le découplage, jetez un coup d'œil à la composition avant l'héritage

0
JACH