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

Je me retrouve souvent à écraser les méthodes d'une classe parent, et ne peux jamais décider si je devrais explicitement énumérer des paramètres donnés ou simplement utiliser une construction blanket *args, **kwargs. Une version est-elle meilleure que l'autre? Est-il une meilleure pratique? Ce qui (dés)avantages qui me manque?

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 de Python)
  • plus facile à saisir
  • Paramètres de fonction facilement accessibles

Perçu avantages de la variante de couverture

  • plus sec
  • la classe parent est facilement interchangeable
  • la modification des valeurs par défaut dans la méthode parent est propagée sans toucher à un autre code
35
demandé sur Paolo 2013-01-31 16:52:45

5 réponses

Principe De Substitution Liskov

Généralement, vous ne voulez pas que votre signature de méthode varie dans les types dérivés. Cela peut causer des problèmes si vous souhaitez échanger l'utilisation de types dérivés. Ceci est souvent appelé le principe de Substitution 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 requis manquants lancent des exceptions évidentes)

Arguments de longueur Variable et couplage

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

Emplacements Pour 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é lorsque vous avez une bonne raison comme:

  • définir un wrapper de fonction (c'est-à-dire un décorateur).
  • définition d'une fonction polymorphe paramétrique.
  • Quand les arguments que vous pouvez prendre sont vraiment complètement variable (par exemple une fonction de connexion DB généralisée). Les fonctions de connexion DB prennent généralement une chaîne de connexion sous de nombreuses formes différentes, à la fois sous forme d'arg unique et sous forme multi-arg. Il existe également différents ensembles d'options pour différentes bases de données.
  • ...

Tu Fais Quelque Chose De Mal?

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

25
répondu dietbuddha 2015-04-13 03:21:40

Mon choix serait:

class Child(Parent):

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

Il évite d'accéder à l'argument commit de *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, avoir les *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 les nouveaux arguments avec des valeurs par défaut. Cela éviterait les erreurs à propager.

13
répondu luc 2013-07-09 09:53:25

Si vous êtes certain que Child gardera la signature, l'approche explicite est certainement préférable, mais quand Child changera 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 changements dans la signature sont assez lisibles dans Child, tandis que la signature originale est assez lisible dans Parent.

À mon avis, c'est aussi le meilleur moyen lorsque vous avez plusieurs héritages, car appeler super plusieurs fois est assez dégoûtant quand vous n'avez pas d'args et kwargs.

Pour ce que ça vaut, c'est aussi le moyen préféré dans un bon nombre de bibliothèques et de 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, je laisse simplement entendre que cette approche est assez répandue.

4
répondu dmg 2013-02-06 13:48:17

Pas vraiment une réponse mais plus une note de côté: si vous voulez vraiment, 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 semble assez laid et je ne l'utiliserais que si je sens que j'en ai vraiment besoin.

3
répondu Clemens Klein-Robbenhaar 2013-02-10 13:11:04

Je préfère les arguments explicites Car Auto complete vous permet de voir la signature de la méthode de la fonction lors de l'appel de la fonction.

2
répondu GeneralBecos 2013-02-07 19:16:33