Inheritance best practice : *args, **kwargs or explicitly specifying parameters [closed]
Asked Answered
P

6

73

I often find myself overwriting methods of a parent class, and can never decide if I should explicitly list given parameters or just use a blanket *args, **kwargs construct. Is one version better than the other? Is there a best practice? What (dis-)advantages am I missing?

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

Perceived benefits of explicit variant

  • More explicit (Zen of Python)
  • easier to grasp
  • function parameters easily accessed

Perceived benefits of blanket variant

  • more DRY
  • parent class is easily interchangeable
  • change of default values in parent method is propagated without touching other code
Pauperism answered 31/1, 2013 at 12:52 Comment(4)
A lot of it depends on exactly what you're subclassing... If there's a high chance that you're going to add (or someone else will) additional kwargs to methods of the base class, it makes a lot of sense to stick with **kwargs. If this isn't likely, then explicitly specifying kwargs is definitely much better from a readability standpoint.Touchline
Well, you're certainly throwing away information. Explicit is better than implicit and all that.Tryst
You have two choices: 1) explicitly name the parameters and prohibit signatures changes in derived classes(otherwise goodluck with super and multiple inheritance), or 2) Use **kwargs and allow changing the signature of the methods. Which one should be used probably depends on the situation.Respective
Using *args and **kwargs makes it hard to create proper docstrings, intellisense in vsc, pycharm editors do not pick them upSterne
A
50

Liskov Substitution Principle

Generally you don't want you method signature to vary in derived types. This can cause problems if you want to swap the use of derived types. This is often referred to as the Liskov Substitution Principle.

Benefits of Explicit Signatures

At the same time I don't think it's correct for all your methods to have a signature of *args, **kwargs. Explicit signatures:

  • help to document the method through good argument names
  • help to document the method by specifying which args are required and which have default values
  • provide implicit validation (missing required args throw obvious exceptions)

Variable Length Arguments and Coupling

Do not mistake variable length arguments for good coupling practice. There should be a certain amount of cohesion between a parent class and derived classes otherwise they wouldn't be related to each other. It is normal for related code to result in coupling that reflects the level of cohesion.

Places To Use Variable Length Arguments

Use of variable length arguments shouldn't be your first option. It should be used when you have a good reason like:

  • Defining a function wrapper (i.e. a decorator).
  • Defining a parametric polymorphic function.
  • When the arguments you can take really are completely variable (e.g. a generalized DB connection function). DB connection functions usually take a connection string in many different forms, both in single arg form, and in multi-arg form. There are also different sets of options for different databases.
  • ...

Are You Doing Something Wrong?

If you find you are often creating methods which take many arguments or derived methods with different signatures you may have a bigger issue in how you're organizing your code.

Aden answered 8/2, 2013 at 10:54 Comment(0)
D
30

My choice would be:

class Child(Parent):

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

It avoids accessing commit argument from *args and **kwargs and it keeps things safe if the signature of Parent:save changes (for example adding a new default argument).

Update : In this case, having the *args can cause troubles if a new positional argument is added to the parent. I would keep only **kwargs and manage only new arguments with default values. It would avoid errors to propagate.

Damnable answered 31/1, 2013 at 13:10 Comment(4)
I believe that this the best answer, because it addresses the needs exposed by the question. Namely: keep arguments explicit versus possibility of adding arguments to the parent in future. Despite the answer provided by @dietbuddha is "academically" correct, it does not offer an alternative which could be employed in practice, when you don't know for sure what will be the fate of your code in future. This answer addresses that.Columbia
The answer provided by Alex Martelli in this SO thread has more details about this topic.Columbia
The way you write it you imply that parent signature has **kwargs too which is not the case in the example of the question.Extrorse
I don't know if it is because I'm using Python 3 but for me the above code fragment did not work. I needed to use *args, **kwargs, not just **kwargs for this to work. Curiously, it also worked for me using just *argsElegit
M
9

If you are certain that Child will keep the signature, surely the explicit approach is preferable, but when Child will change the signature I personally prefer to use both approaches:

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

This way, changes in the signature are quite readable in Child, while the original signature is quite readable in Parent.

In my opinion this is also the better way when you have multiple inheritance, because calling super a few times is quite disgusting when you don't have args and kwargs.

For what it's worth, this is also the preferred way in quite a few Python libs and frameworks (Django, Tornado, Requests, Markdown, to name a few). Although one should not base his choices on such things, I'm merely implying that this approach is quite widespread.

Marler answered 6/2, 2013 at 13:40 Comment(0)
S
6

Not really an answer but more a side note: If you really, really want to make sure the default values for the parent class are propagated to the child classes you can do something like:

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)

However I have to admit this looks quite ugly and I would only use it if I feel I really need it.

Siu answered 10/2, 2013 at 13:11 Comment(0)
T
5

I prefer explicit arguments because auto complete allows you to see the method signature of the function while making the function call.

Theater answered 7/2, 2013 at 19:16 Comment(0)
H
0

In addition to the other answers:

Having variable arguments may "decouple" the parent from the child, but creates a coupling between the object created and the parent, which I think is worse, because now you created a "long distance" couple (more difficult to spot, more difficult to maintain, because you may create several objects in your application)

If you're looking for decoupling, take a look at composition over inheritance

Hardnosed answered 26/10, 2018 at 15:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.