Search code examples
pythoninheritancemultiple-inheritance

Why is this inheritance structure built like this?


I've been struggling to understand the details of this code for a couple of days now:

class Rectangle:
    def __init__(self, length, width, **kwargs):
        self.length = length
        self.width = width
        super().__init__(**kwargs)

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * self.length + 2 * self.width

# Square inherits from Rectangle:
class Square(Rectangle):
    def __init__(self, length, **kwargs):
        super().__init__(length=length, width=length, **kwargs)


# Triangle doesn't inherit from any class:
class Triangle:
    def __init__(self, base, height, **kwargs):
        self.base = base
        self.height = height
        super().__init__(**kwargs) 

    def tri_area(self):
        return 0.5 * self.base * self.height

# Pyramid inherits from square and triangle:
class Pyramid(Square, Triangle):
    def __init__(self, base, slant_height, **kwargs):
        self.base = base
        self.slant_height = slant_height
        
        # Adding attributes to kwargs dictionary
        kwargs["height"] = slant_height
        kwargs["length"] = base
        super().__init__( base=base, **kwargs)

    def area(self):
        base_area = super().area()
        perimeter = super().perimeter()
        return 0.5 * perimeter * self.slant_height + base_area

    def area_2(self):
        base_area = super().area()
        triangle_area = super().tri_area()
        return triangle_area * 4 + base_area

The inheritance tree is as such:

Rectangle 
    \
   Square  Triangle
      \     /
      Pyramid

The MRO for the Pyramid class:

(__main__.Pyramid,
 __main__.Square,
 __main__.Rectangle,
 __main__.Triangle,
 object)

I understand the general idea behind the code, and how super().__init__ is used to pass a dictionary of variable keyword arguments to the superclass using **kwargs. The details I'm having issues with are:

  1. Why did we, in the Pyramid class, use base=base when the superclass (Square) takes a length argument?
  2. When I do only put super().__init__(base, **kwargs) instead of base=base, why does it give an error saying that init() got multiple values for argument 'length', is it because of the kwargs["length"] specified above it? If so, how does base=base solve that?
  3. Why do we have super().__init__(**kwargs) in Rectangle when it doesn't have a superclass? My guess is maybe to pass the kwargs to the built-in object base class so Triangle can inherit from it? But I'm not sure.
  4. Why would Triangle also have a call to the base class too? It seems redundant (even removing super().__init__(**kwargs) from Triangle doesn't change anything).

Solution

  • Good questions!

    1. Because Square.__init__ accepts **kwargs we can actually pass whatever we want to it without getting an unexpected keyword argument error1. This will become handy a bit later. Any keyword argument that it (and/or its superclasses' __init__ methods) does not consume will propagate up in the MRO until we get to object.


      1Usually. In this case the (unnecessery) call to

      super().__init__(**kwargs) in Triangle.__init__ breaks this behavior.


    1. Due to the expansion of kwargs, when calling super().__init__(base, **kwargs) in Pyramid.__init__, Square.__init__ is actually called with (2, height=3, length=2). Since the argument of Square.__init__ is called length you recieve the error about multiple values for length.

    2. That's correct. Furthermore, because nothing "consumed" the base keyword argument yet, Triangle.__init__ will receive it.

    3. That's also correct. There is absolutely no need to call super().__init__(**kwargs) in Triangle.__init__, especially that by that point kwargs is an empty dict (all the keyword arguments it contained were consumed already).