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
Avantages perçus de la variante de couverture
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:
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:
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.
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.
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.
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.
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.
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