133

Lets say I have

class Super():
  def method1():
    pass

class Sub(Super):
  def method1(param1, param2, param3):
      stuff

Is this correct? Will calls to method1 always go to the sub class? My plan is to have 2 sub classes each override method1 with different params

asdasasdsa
  • 1,331
  • 2
  • 9
  • 3

5 Answers5

145

In Python, methods are just key-value pairs in the dictionary attached to the class. When you are deriving a class from a base class, you are essentially saying that method name will be looked into first derived class dictionary and then in the base class dictionary. In order to "override" a method, you simply re-declare the method in the derived class.

So, what if you change the signature of the overridden method in the derived class? Everything works correctly if the call is on the derived instance but if you make the call on the base instance, you will get an error because the base class uses a different signature for that same method name.

There are however frequent scenarios where you want derived class method have additional parameters and you want method call work without error on base as well. This is called "Liskov substitution principle" (or LSP) which guarantees that if person switches from base to derived instance or vice versa, they don't have to revamp their code. To do this in Python, you need to design your base class with the following technique:

class Base:
    # simply allow additional args in base class
    def hello(self, name, *args, **kwargs):
        print("Hello", name)

class Derived(Base):
      # derived class also has unused optional args so people can
      # derive new class from this class as well while maintaining LSP
      def hello(self, name, age=None, *args, **kwargs):
          super(Derived, self).hello(name, age, *args, **kwargs) 
          print('Your age is ', age)

b = Base()
d = Derived()

b.hello('Alice')        # works on base, without additional params
b.hello('Bob', age=24)  # works on base, with additional params
d.hello('Rick')         # works on derived, without additional params
d.hello('John', age=30) # works on derived, with additional params

Above will print:

    Hello Alice
    Hello Bob
    Hello Rick
    Your age is  None
    Hello John
    Your age is  30
. Play with this code
Shital Shah
  • 63,284
  • 17
  • 238
  • 185
  • 6
    Thanks for this update several years later with a much clearer and more actionable discussion, and a working example plus playpen! – nealmcb Feb 26 '20 at 03:17
  • 2
    should we place age in the signature of hello after `*args`? If not, code like, `d.hello("John", "blue", age=30)` will not work. I mean, in general positional args should always be defined prior to kwargs – Quickbeam2k1 Mar 05 '20 at 09:46
  • 1
    I think the answer to your question depends whether we want to allow param with default option (`age` here) to be set also by position or only as kwarg. Note, that your suggestion will work only in Python 3, where you can specify keyword only arguments with `*`. See e.g. [this](https://stackoverflow.com/a/9873280/7964098). – Nerxis Jul 03 '20 at 08:15
  • 7
    Good answer but in "LSP which guarantees that if person switches from base to derived instance or vice versa", the "vice versa" part is incorrect. – Michał Jabłoński Sep 29 '20 at 12:43
  • Yep, LSP does not enforce instances of a subclass to be exchangable to the parent class. – ruohola Dec 14 '20 at 14:28
  • 1
    If you omit `*args, **kwargs` from the derived class method's signature, then does it still work? – dumbledad May 07 '21 at 14:13
  • _"if you make the call on the base instance, you will get an error"_ Would you please provide an example of such code which results in an error? It would be very helpful. – Jeyekomon Jan 14 '22 at 13:01
  • @Jeyekomon Using OP's code example, this would mean calling `Super.method1` while using the signature of `Sub.method1`, like this: `Super.method1(1, 2, 3)`. You'll get an error like this: `"TypeError: method1() takes 0 positional arguments but 3 were given"`. – insectean Mar 21 '22 at 18:09
  • @insectean Yes, this. Why would you call the method `Super.method1` with wrong arguments? Is it a common requirement / good practice to be able to use the `method1` without knowing its origin? Without the need for distinguishing between the super class and its subclasses? Or is it some other reason? I am not really experienced so I fail to imagine a good scenario to fit such practice. – Jeyekomon Mar 23 '22 at 08:10
  • 1
    @Jeyekomon You're on the right track with these questions. Indeed it is common to use superclasses this way. An example I recently came across was publishing metadata for different types of products. The base class defined the `publish` method, and each subclass overrode it with special behavior for its product type. This way, you could store subclass instances in a list of the base class type, then call `publish` on each of them without specifying their subclass, and still get the special behavior. This makes use of something called dynamic polymorphism, runtime polymorphism, or late binding. – insectean Mar 24 '22 at 18:17
51

Python will allow this, but if method1() is intended to be executed from external code then you may want to reconsider this, as it violates LSP and so won't always work properly.

Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
  • 3
    Is the violation of LSP on the fact that Sub.method1 takes 3 arguments while Super.method1 takes none making them in fact different interfaces? – unode May 18 '11 at 16:59
  • 2
    @Unode: Correct. This could be solved by having the arguments of the subclass's method all have default values, but then you get into which defaults would be appropriate. – Ignacio Vazquez-Abrams May 18 '11 at 17:02
  • 4
    I see. But then just to clarify. If the parent method1 was defined as `Super.method1(param1=None, param2=None, param3=None)` it would still violate LSP if on subclasses it's defined as `Sub.method1(param1, param2, param3)` right? As attributes are mandatory in one case but not the other. Hence to my understanding without changing the subclass interface, the only way to not violate LSP would be having the parameters without default values on the parent. Am I correct on this or over-interpreting the LSP? – unode May 18 '11 at 18:44
  • 3
    @Unode: Also correct. Having the contract become less restrictive in the subclass violates LSP. – Ignacio Vazquez-Abrams May 18 '11 at 18:47
  • except when `method1` is `__init__`, where LSP doesn't apply – joel Feb 13 '20 at 13:29
4

You could do something like this if it's ok to use default arguments:

>>> class Super():
...   def method1(self):
...     print("Super")
...
>>> class Sub(Super):
...   def method1(self, param1="X"):
...     super(Sub, self).method1()
...     print("Sub" + param1)
...
>>> sup = Super()
>>> sub = Sub()
>>> sup.method1()
Super
>>> sub.method1()
Super
SubX
Zitrax
  • 19,036
  • 20
  • 88
  • 110
2

In python, all class methods are "virtual" (in terms of C++). So, in the case of your code, if you'd like to call method1() in super class, it has to be:

class Super():
    def method1(self):
        pass

class Sub(Super):
    def method1(self, param1, param2, param3):
       super(Sub, self).method1() # a proxy object, see http://docs.python.org/library/functions.html#super
       pass

And the method signature does matter. You can't call a method like this:

sub = Sub()
sub.method1() 
Zaur Nasibov
  • 22,280
  • 12
  • 56
  • 83
2

It will work:

>>> class Foo(object):
...   def Bar(self):
...     print 'Foo'
...   def Baz(self):
...     self.Bar()
... 
>>> class Foo2(Foo):
...   def Bar(self):
...     print 'Foo2'
... 
>>> foo = Foo()
>>> foo.Baz()
Foo
>>> 
>>> foo2 = Foo2()
>>> foo2.Baz()
Foo2

However, this isn't generally recommended. Take a look at S.Lott's answer: Methods with the same name and different arguments are a code smell.

Community
  • 1
  • 1
quasistoic
  • 4,657
  • 1
  • 17
  • 10