Search code examples
pythonsuperkeyword-argument

Using **kwargs on super() gives me Attribute Error


Just trying to create a simple toy example to calculate the surface area of a pyramid:

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 perim(self):
        return 2 * (self.length + self.width)

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

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

class Pyramid(Square, Triangle):
    def __init__(self, base, slant, **kwargs):
        kwargs["height"] = slant
        kwargs["length"] = base
        super().__init__(base = base, **kwargs)

    def surf_area(self):
        return super().area() + 4 * super().tri_area()

p = Pyramid(2,4)
p.surf_area()

But this gives me an error of

AttributeError                            Traceback (most recent call last)
<ipython-input-154-d97bd6ab2312> in <module>
      1 p = Pyramid(2,4)
----> 2 p.surf_area()

<ipython-input-153-457095747484> in surf_area(self)
      6 
      7     def surf_area(self):
----> 8         return super().area() + 4 * super().tri_area()

<ipython-input-151-8b1d4ef9dca9> in tri_area(self)
      5         super().__init__(**kwargs)
      6     def tri_area(self):
----> 7         return 0.5 * self.base * self.height

AttributeError: 'Pyramid' object has no attribute 'base'

The online resources don't seem to give much of a conceptual understanding of **kwargs too well (or they're written in too much a labrintyine manner for a beginner). Does this somehow have to do with the fact that **kwargs as an iterable need to be exhausted completely before going to the next call?


Solution

  • I can sort of understand why you'd be confused. The main trick to realise is this: absolute bottom line, kwargs is not something magical. It is just a dictionary holding key value pairs. If a call requires more positional arguments than provided, it can look into keyword arguments and accept some values. However, kwargs does not invoke some magic that associates all names provided as a self.<some_name_here> .

    So, first to just get a visual understanding of what's going on, what you should do when you don't understand a piece of code is making sure it runs how you think it does. Let's add a couple print statements and see what's happening.

    Version 1:

    class Rectangle:
        def __init__(self, length, width, **kwargs):
            self.length = length
            self.width = width
            print(f"in rectangle. length = {self.length}, width = {self.width}")
        def area(self):
            return self.length * self.width
    
    
    class Square(Rectangle):
        def __init__(self, length, **kwargs):
            print("in square")
            super().__init__(length = length, width = length, **kwargs)
    
    class Triangle:
        def __init__(self, base, height, **kwargs):
            print("in triangle")
            self.base = base
            self.height = height
            super().__init__(**kwargs)
        def tri_area(self):
            return 0.5 * self.base * self.height
    
    class Pyramid(Square, Triangle):
        def __init__(self, base, slant, **kwargs):
            print("in pyramid")
            kwargs["height"] = slant
            kwargs["length"] = base
            super().__init__(base = base, **kwargs)
    
        def surf_area(self):
            print(f"area : {super().area()}")
            print(f"tri_area : {super().tri_area()}")
            return super().area() + 4 * super().tri_area()
    
    p = Pyramid(2,4)
    p.surf_area()
    

    Output:

    in pyramid
    in square
    in rectangle. length = 2, width = 2
    area : 4
    #and then an error, note that it occurs when calling super().tri_area()
    #traceback removed for brevity.
    AttributeError: 'Pyramid' object has no attribute 'base'
    

    I suspect this already breaks some assumptions you had about how the code runs. Notice that the triangle's init was never called. But let's get rid of the parts that work fine, and add an additional print statement. I will also take the liberty of calling it with a different value for first argument, something that pops out easier.

    Version 2:

    class Rectangle:
        def __init__(self, length, width, **kwargs):
            self.length = length
            self.width = width
            print(f"in rectangle. length = {self.length}, width = {self.width}")
            print(f"kwargs are: {kwargs}")
    
    
    class Square(Rectangle):
        def __init__(self, length, **kwargs):
            print("in square")
            super().__init__(length = length, width = length, **kwargs)
    
    class Triangle:
        def __init__(self, base, height, **kwargs):
            print("in triangle")
            self.base = base
            self.height = height
            super().__init__(**kwargs)
        def tri_area(self):
            return 0.5 * self.base * self.height
    
    class Pyramid(Square, Triangle):
        def __init__(self, base, slant, **kwargs):
            print("in pyramid")
            kwargs["height"] = slant
            kwargs["length"] = base
            super().__init__(base = base, **kwargs)
    
        def surf_area(self):
            print(f"tri_area : {super().tri_area()}")
            return super().tri_area()
    
    p = Pyramid(10000,4)
    p.surf_area()
    

    Output:

    in pyramid
    in square
    in rectangle. length = 10000, width = 10000
    kwargs are: {'base': 10000, 'height': 4}
    #error with traceback
    AttributeError: 'Pyramid' object has no attribute 'base'
    

    Bottom line: the kwargs holds a key with the name base, but this has no relation to self.base. However, my recommendation is to get rid of the whole class structure, and spend some time playing around with any basic function, get rid of the extra stuff.

    Say, a demonstration:

    def some_func(a, b, **kwargs):
        print(a, b)
        print(kwargs)
    
    some_func(1, 2)
    some_func(1, 2, c=42)
    some_func(a=1, c=42, b=2)
    
    def other_func(a, b, **look_at_me):
        print(a, b)
        print(look_at_me)
    
    other_func(1, 2)
    other_func(1, 2, c=42)
    other_func(a=1, c=42, b=2)
    

    These two chunks produce the same outputs. No magic here. Output:

    1 2
    {}
    1 2
    {'c': 42}
    1 2
    {'c': 42}
    

    When you added the classes into the mix, and inheritance, there's too many things happening at once. It is easier to miss what happens, so it's a good idea to use smaller code samples.