Multiple Inheritance in Python
Python supports inheritance from multiple classes. In this lesson, you’ll see:
- How multiple inheritance works
- How to use
super()
to call methods inherited from multiple parents - What complexities derive from multiple inheritance
- How to write a mixin, which is a common use of multiple inheritance
A class can inherit from multiple parents. For example, you could build a class representing a 3D shape by inheriting from two 2D shapes:
class RightPyramid(Triangle, Square):
def __init__(self, base, slant_height):
self.base = base
self.slant_height = slant_height
def what_am_i(self):
return 'RightPyramid'
The Method Resolution Order (MRO) determines where Python looks for a method when there is a hierarchy of classes. Using super()
accesses the next class in the MRO:
class A:
def __init__(self):
print('A')
super().__init__()
class B(A):
def __init__(self):
print('B')
super().__init__()
class X:
def __init__(self):
print('X')
super().__init__()
class Forward(B, X):
def __init__(self):
print('Forward')
super().__init__()
class Backward(X, B):
def __init__(self):
print('Backward')
super().__init__()
If you combine the MRO and the **kwargs
feature for specifying name-value pairs during construction, you can write code that passes parameters to parent classes even if they have different names:
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
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 RightPyramid(Square, Triangle):
def __init__(self, base, slant_height, **kwargs):
self.base = base
self.slant_height = slant_height
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
Multiple inheritance can get tricky quickly. A simple use case that is common in the field is to write a mixin. A mixin is a class that doesn’t care about its position in the hierarchy, but just provides one or more convenience methods:
class SurfaceAreaMixin:
def surface_area(self):
surface_area = 0
for surface in self.surfaces:
surface_area += surface.area(self)
return surface_area
class Cube(Square, SurfaceAreaMixin):
def __init__(self, length):
super().__init__(length)
self.surfaces = [Square, Square, Square, Square, Square, Square]
class RightPyramid(Square, Triangle, SurfaceAreaMixin):
def __init__(self, base, slant_height):
self.base = base
self.slant_height = slant_height
self.height = slant_height
self.length = base
self.width = base
self.surfaces = [Square, Triangle, Triangle, Triangle, Triangle]
Here’s what you get:
>>> cube = Cube(3)
>>> cube.surface_area()
54
00:00
This is the third of three lessons on inheritance in Python and the use of super()
to access methods in parent hierarchy. In this lesson, I’ll be talking about multiple inheritance.
00:12
Multiple inheritance is the process of inheriting from multiple classes into your new base class. In order to do that, I want to add a new base shape called Triangle
. Once again, I’m adding this to shapes.py
.
00:25
There’s nothing in Triangle
you haven’t seen before. The base
and the height
of the triangle are passed in in the constructor. I’ve added an .area()
method, which has the area formula for a triangle—half times the base times the height—and once again, I’ve got a .what_am_i()
method so I can show you how the inheritance mechanisms work.
00:44
So, let’s inherit from Triangle
. The RightPyramid
inherits from Triangle
and Square
—multiple inheritance. In case your geometry is as rusty as mine, a right pyramid is one with a square base and four triangles sloping up to its point. In order to define a RightPyramid
, you need a length
for the base and a slant_height
.
01:05
The slant height is the height of the triangle that is slanted to make the side of the pyramid. Like our other shapes, I’ve also defined a .what_am_i()
so we can look at the relationships.
01:14
Let’s look at the REPL. Import the RightPyramid
,
01:19
create the rightpyramid
, and let’s look at the .what_am_i()
. super()
returns the parent, but in the case of multiple inheritance, it returns the first parent. Because Triangle
was defined before Square
in the list of the RightPyramid
, the super()
method calls the .what_am_i()
of the Triangle
.
01:39
The .__class__
attribute of the RightPyramid
, not surprisingly, says RightPyramid
, and then you may recall the .__class__
attribute also has a .__bases__
attribute that can show you the inheritance. In the case of RightPyramid
, we’re inheriting from Triangle
and Square
.
01:55
One more thing that I’d like to show you is the .__mro__
. This is the method resolution order. This is the order in which Python looks through the inheritance structure.
02:07
In this case, it starts out looking at the RightPyramid
, then goes up the inheritance structure to look at the Triangle
. Triangle
has no parents, so it moves on to the next thing in the multiple inheritance hierarchy, which is the Square
. Square
’s parent is the Rectangle
. And then finally, because Rectangle
has no parents, it reaches the top, the <class 'object'>
.
02:27
All objects in Python inherit from the object
object. You may recall from the first lesson, the results of the dir()
function had a lot of double underscore (__
) methods in it.
02:37
Those double underscore methods are all defined inside of this object
object, which everything in Python inherits from. The method resolution order dictates to Python how to look up a named method when it is called. In single inheritance, this is usually fairly simple.
02:53 It is the path of the inheritance hierarchy. In multiple inheritance, this can get complicated. It’s quite possible that your inheriting classes could have methods of the same name. The MRO dictates which of them gets called. These name clashes can cause problems and can make your code a little confusing.
03:10
There’s different ways of handling it. First off, you could rewrite your code so there are no name clashes. Take our .area()
method on Square
and Triangle
, and rename them to be the .square_area()
and the .triangle_area()
. Now name resolution isn’t necessary, because the name is unique to the class.
03:27
You can use the inheritance declaration itself to specify the order in which things are looked up. This is where MRO shines. You have to be careful though, because the result between RightPyramid
, Triangle
, then Square
versus RightPyramid
, Square
, Triangle
will change which .area()
method gets called. Finally, you can also specify the call itself directly. Using the class object, you can call the .area()
method and pass in the object itself.
03:56
This is the most explicit way. If someone else is reading your code, they won’t have to remember what the MRO is; they’ll know you’re using the Square.area()
method. To illustrate how the MRO works with multiple inheritance, I’ve created a new file called chain.py
. Inside of it, I’m creating five different classes, A
, B
, X
, Forward
, and Backward
.
04:16
Each one of the classes simply has a constructor that prints out that you’re inside of its constructor and then calls the super().__init__()
. At first blush, this might seem weird. If you look at the constructor for A
, there is no parent to A
, but we’re still calling super()
.
04:32
You’ll see why in a second. Let’s pull out our trusty REPL and create a couple of objects. First off, forward
.
04:40
The first thing Forward()
does is call the Forward
constructor. The Forward
constructor prints 'Forward'
and then calls super()
. The super of Forward
is B
.
04:51
So next, we see B
. Going into the B
class, the super()
for this is called, the super of B
is A
, so 'A'
gets printed.
05:00
Then we go into A
, and super()
of A
is called. There is no super for A
. Up until now, I’ve always talked about the parent. In single inheritance, this is true. In multiple inheritance, it’s a little bit of a white lie.
05:14
What is actually happening is the next object in the MRO is what is called. So in this case, the super for A
is X
. If you go back to Forward
, Forward
inherits from B
, then inherits from X
.
05:28
So B
chains to A
, A
chains to X
when super()
is called. To contrast this, let’s look at Backward
. Backward
’s constructor called super()
, that’s X
. X
also has no parent, but super of X
will be the next object in the MRO, which in this case is B
. B()
calls super()
, that inherits from A
.
05:51
A()
calls super()
, and we’re done the chain. As you can see, specifying the order of inheritance of B
and then X
, or X
versus B
changes what methods get called in what order. Let’s take the complexity of this chaining and apply it to our shapes code. First off, I’ve got my RightPyramid
. Notice that I’ve taken the inheritance order and changed it from before.
06:16
Currently, the RightPyramid
is inheriting from Square
first and then Triangle
. In addition to the potential challenge of method names clashing in inheritance, you also have the problem of how to actually construct them.
06:28
In the RightPyramid
you have a base
and a slant_height
, but it’s based on a Square
and a Triangle
, whose constructor attributes aren’t base
and slant_height
—they’re length
and height
. This can cause problems when you start to inherit. One way around this is to use kwargs
(keyword args).
06:45
If you haven’t seen this before, Python supports the ability to pass in a dictionary with the double asterisk (**
) in front of it. This dictionary forms name-value pairs as arguments for a method.
06:57
This allows you to pass in attributes that, in this case, RightPyramid
is going to ignore.
07:03
I’ve taken the base
and the slant_height
and assigned them as before, added a "height"
and "length"
keyword argument, and then I pass it into super()
.
07:12
What that means is when the super()
gets called for the constructor for Square
, you now have all of the arguments there: base
, slant_height
, height
, and length
. Square()
can take the length
that it needs and pass that into its own super()
. base
, slant_height
, and height
are all still there inside of kwargs
, but Square
doesn’t use them. Square()
calls its own super()
. Rectangle
pulls out the length
and width
that it’s interested in inside of kwargs
, calls its own super()
—which of course, there’s no base class for Rectangle
, so now we’re going into RightPyramid
’s inheritance MRO, calling the Triangle
constructor, and the Triangle()
gets the base
and the height
.
07:53 This allows us to have all of the attributes passed in for all of these objects. This works, but it isn’t the easiest code to read. Multiple inheritance solves certain kinds of problems, but it can also make code rather difficult to follow along.
08:08 You need to be careful how you use this tool. There are often ways of constructing the code that are easier to read that achieve the same end result. The complexities of multiple inheritance make programmers wary sometimes about when to use it.
08:22 One of the easiest ways of making sure you don’t have problems is to create classes that don’t have name clashes and are as independent as possible.
08:30
A pattern that’s very common in a lot of frameworks like Django and Flask is something called a mixin. A mixin is an independent class that gets pulled in in the inheritance hierarchy but is not going to impact anything that inherits it. An example of this is the SurfaceAreaMixin
.
08:48
The SurfaceAreaMixin
provides a single method, which is .surface_area()
, and has no expectation about construction. All it requires is that somewhere in the class that is using it, there’s an attribute called .surfaces
. Here’s the RightPyramid
modified to use the SurfaceAreaMixin
.
09:05
SurfaceAreaMixin
is inherited into the RightPyramid
and .surfaces
is defined—a list of the different surfaces for the RightPyramid
.
09:15
The .surface_area()
method can now be called calculating the surface area without the SurfaceAreaMixin
having to understand what the shape is that is being inherited from. Here’s another example with the Cube
. Same idea—SurfaceAreaMixin
is inherited from, and the six surfaces of the Cube
are defined inside of .surfaces
.
09:37
The independence of the SurfaceAreaMixin
means there aren’t any name clash complexities, but you still have the power of inheritance.
09:46
Let’s see this in the REPL. We construct our Cube
, call our .surface_area()
, and get our result.
Alex on Jan. 14, 2020
Thank you,especially for the Mixins example.
Anonymous on Jan. 16, 2020
Yea, I have read the documentation multiple times, but the way this was presented made it extremely digestible and clear - very well put together. Now to watch it 5 more times.
rhuang on Jan. 30, 2020
Great tutorial! Thanks!
ajp4 on Feb. 2, 2020
Best explanation I have seen. Thank you!
Dima on March 27, 2020
I came to this tutorial to understand to concept of Mixins. I really happy I got everything in less than 30 minutes. Really appreciate such explanation simplicity!
Ryan Cook on April 4, 2020
I liked the explanation of the “why” in creating Mix-ins and the potential pitfalls of multiple-inheritance. I love the explanations simplicity and your measured vocal speed. Keep up the Good Work!!
Erikton Konomi on April 12, 2020
Fantastic tutorial! I really enjoyed how concise and to the point all three videos were, with solid examples. The introduction of the mixin pattern however made all the difference in the end. I’ll be decoupling some code at work pretty soon with this :) Thanks!
ricardoaparicio92 on April 18, 2020
I’m lost!
Ricky White RP Team on April 19, 2020
@richardoaparicio92 What are you struggling with?
Liz Schley on April 28, 2020
This is great, so thank you! My purpose was to understand how OOP works in Python, so that I am capable of designing good code when I need to.
Liz Schley on April 28, 2020
My favorite thing was the mro method and using mixins to prevent confusion. Definitely my plan, if possible since confusing code creates bugs that are time-consuming to fix.
graham17 on May 18, 2020
Really useful - a few pennies dropped right here. The only thing I didn’t get was the final cube = SACube(3) I suspect this was an error - I couldn’t find any ref to class SACube anywhere - but maybe I missed it - overall - a great concise tutorial
Christopher Trudeau RP Team on May 19, 2020
Hi graham17,
Good catch. That’s an artifact from an earlier version of the code. We’ll fix it shortly. Thanks for letting us know. …ct
samsku1986 on May 31, 2020
In case of single inheritence why use super() method when we could call the method using class name directly ??. I mean what is the advantage ?
class Rectangle: def init(self,length,width): self.length = length self.width = width
def area(self):
return (self.length * self.width)
def permiter(self):
return (2 * self.length + 2 * self.width)
class Square(Rectangle):
def __init__(self,length):
Rectangle.__init__(self,length,length) >>>>>>>>>>>>>>>>>>>>calling Rectangle,method() directly instead of super
#super().__init__(length,length)
Vijay Alagappan on June 20, 2020
Crystal clear explanation of concepts such as MRO and Mixins! Thanks a lot.
Nathan L on June 27, 2020
You pointed out with Cube
that you could access Rectangle.area()
either by self.area()
or super().area
.
What’s best practice? I would imagine best practice is to use self.parent_method
unless there is a name clash, as with __init__
in subsequent examples.
Christopher Trudeau RP Team on June 30, 2020
Hi Nathan,
Generally, you use self.method() unless there is a reason to do otherwise. If I’m having to get at the parent, then super(), or if there is some situation where clarity is needed that I’m calling a parent method.
nj8456 on July 6, 2020
Encountered this code in “Creating custom Model Managers in Django” in Django by Example Book.
GIVEN the following code
class PublishedManager(models.Manager):
def get_queryset(self):
return (
super(PublishedManager, self)
.get_queryset()
.filter(status='published')
)
class Post(models.Model):
objects = models.Manager()
published_objects = PublishedManager()
publish = models.DateTimeField(default=timezone.now)
AND GIVEN objects
is the default Model manager for Post,
THEN the code super(PublishedManager, self)
is equivalent to calling the default manager Object - objects.get_queryset()
from within an instance of the PublishedManager class right ?
Christopher Trudeau RP Team on July 7, 2020
Hi nj8456,
Yes, you’re right. PublishedManager is overriding models.Manager.get_queryset(), so within that override, if you called self.get_queryset() you would get the overridden instance – a recursive call. The use of super() here gets you at the parent’s method.
This is a common pattern in Django models. You do something similar if you want to override a Model’s save() method – call the parent, to do what it would normally do, then run your specialized code.
In the case of PublishedManager.get_queryset(), you want to do a query just like the parent, but this time also apply a filter. The parent is called, then the filter is applied, then the filtered result is returned.
The brackets notation is a short form for doing:
qs = super(Published Manager, self).get_queryset()
return qs.filter(status='published')
Because get_queryset() returns a QuerySet, the:
fn()
.other_fn()
.more_fn()
pattern, chains the results of each of them together, calling the next function on whatever was returned by the previous.
Omkar on Sept. 4, 2020
Since class A or X here are the ones last in the forward and backward classes, we get done with the chain. But does it mean the class object
init is also called here when last on the chain?
class A:
def __init__(self):
print('A')
super().__init__()
Last on the chain or if even if this was just a single class calling it’s super()
does mean calling object
right as it is the parent?
Christopher Trudeau RP Team on Sept. 4, 2020
Hi Omkar,
Yeah, that one’s a bit tricky. On its own A wouldn’t need the super().init() because it’s super is just the base Object class.
Where it becomes important is through the chaining. In the case of the Backward class, A’s super().init() does nothing. In the case of the Forward class, it is why X’s constructor gets called.
super() calls the next class in the MRO.
The chain works like this:
Backward -> X -> B -> A
So A’s call to super().init() does nothing. By contrast though:
Forward -> B -> A -> X
In this case, without A’s super().init(), X’s init method would not get called.
If you are writing classes that are likely to be mixed in or used with inheritance, it is always good practice to call “super().init()” in case other programmers using your class change the order of inheritance.
Omkar on Sept. 4, 2020
Thanks for the crystal clear explanation :) Enjoying being a part of this community!
James on Sept. 8, 2020
I didn’t quite understand this part of the mixin:
class Cube(Square, SurfaceAreaMixin):
...
self.surfaces = [Square, Square, Square, Square, Square, Square]
class RightPyramid(Square, Triangle, SurfaceAreaMixin):
...
self.surfaces = [Square, Triangle, Triangle, Triangle, Triangle]
Why are they with these values? I’m from Brazil and as this video is still without subtitles it was a little difficult for me to understand. Thanks.
Christopher Trudeau RP Team on Sept. 9, 2020
Hi James,
The idea here was to highlight what a Mixin is used for: code that is common across some classes without necessarily having deep knowledge about the class.
The SurvaceAreaMixin.surface_area() method calculates the surface area of a 3D shape by looking for an attribute named “surfaces” and calling the area() method for each of the surfaces, adding them together.
A Cube has six sides, each of which is a square. By setting Cube.surfaces to be six Squares, the mixin’s surface_area() method will correctly calculate the surface area of a Cube.
The RightPyramid is comprised of a square base and four triangular sides. It represents this by setting its “surfaces” attribute to [Square, Triangle, Triangle, Triangle, Triangle].
The mixin knows on an abstract level how to calculate surface area (by calling the area() method on each shape in the “surfaces” attribute), but doesn’t need to know what the surfaces are for the object inheriting the mixin.
The specifics of the 3D shape are described in the implementation of the class (Cube or RightPyramid), but the algorithm for calculating the surface area can be separated out from this into a common mixin class.
Without the mixin, you would need to write separate surface_area() methods for the Cube and RightPyramid. Having it in the mixin means writing and testing it in only one place.
Ghani on Oct. 7, 2020
Very clear and well-explained!
Saul on Nov. 7, 2020
Excellent material! I have a small quibble and some hopefully useful observations. Please correct me if I go astray. At around the 7:18 mark, it is noted that the Square class constructor receives base, slant_height, height, and length as arguments. But slant_height is not actually included among those arguments. It was captured as a positional parameter in the constructor for RightPyramid, along with base. The reason base itself gets passed up the chain is because we specifically re-submit base in the super().__init__(base=base, **kwargs)
method call. We don’t re-submit slant_height because it isn’t used by any superclasses.
class RightPyramid(Square, Triangle):
def __init__(self, base, slant_height, **kwargs):
self.base = base
self.slant_height = slant_height
kwargs["height"] = slant_height
kwargs["length"] = base
super().__init__(base=base, **kwargs)
In fact, if we had included slant_height, an exception would later result when the super().__init__(**kwargs)
call to object ultimately occurred:
TypeError: object.init() takes no parameters
Using super().__init__(**kwargs)
in this way to chain across multiple inheritance hierarchies (by following the MRO) means that we cannot have any extraneous or unused keyword arguments. They have to be captured by parameters with the same name in a constructor somewhere in the chain. That’s the only way they get consumed and removed from the kwargs dictionary.
Another thing: I was a bit troubled at first by the assignment to the kwargs dictionary in the constructor for RightPyramid. I couldn’t figure out why we would pass along height and length as arguments with these assignment statements but then pass along base as an argument in the super().__init__
call. Well it seems that these approaches are equivalent. We could have also done either of the following just as well:
class RightPyramid(Square, Triangle):
def __init__(self, base, slant_height, **kwargs):
self.base = base
self.slant_height = slant_height
kwargs["height"] = slant_height
kwargs["length"] = base
kwargs["base"] = base
super().__init__(**kwargs)
class RightPyramid(Square, Triangle):
def __init__(self, base, slant_height, **kwargs):
self.base = base
self.slant_height = slant_height
super().__init__(height=slant_height,
length=base,
base=base,
**kwargs)
Again, please let me know if I am missing something. Thanks!
Christopher Trudeau RP Team on Nov. 7, 2020
Hi Saul,
Thanks for your comments.
At around the 7:18 mark, it is noted that the Square class constructor receives base, slant_height, height, and length as arguments. But slant_height is not actually included among those arguments.
I believe what I was attempting convey was that slant_height gets passed in but as the height argument. You are correct that slant_height is not kept as a named argument.
Your are also correct that you have to be careful with these things and what gets consumed. You only have to make sure they all get used if there is a class somewhere with an empty init(), if your constructors always have the *args, **kwargs
arguments you should be ok (he writes nervously without testing :) ). That being said, this is why I tend to prefer to use inheritance for Mixins only. These kinds of subtleties can definitely cause you weird problems both at compile and runtime.
I was a bit troubled at first by the assignment to the kwargs dictionary … Well it seems that these approaches are equivalent.
Yep, you’re right. It has been too long since I wrote that code, I don’t remember why I did it that way. I suspect it had something to do with how it evolved out of earlier versions. Either approach works.
Happy coding!
anindo78 on Nov. 27, 2020
Great tutorial on inheritance!
rikhuygen on Feb. 7, 2021
Great tutorial, and very clearly explained.
There is one thing I’m not completely convinced and that is the Mixin example. The method surface_area()
in the SurfaceAreaMixin class uses the self.surfaces
which you defined in the sub-classes. That sounds a little weird to me. How would I know from the Mixin class which instance variables I must define so the method would not crash with an AttributeError. Is that best practice? Should that be learned from the documentation or by code inspection? Shouldn’t the self.surfaces
be at least declared in an __init__()
method of the Mixin class?
Thanks, Rik
Christopher Trudeau RP Team on Feb. 8, 2021
Hi @rikhuygen,
Yep, this is one of the challenges of trying to teach mixins and object oriented lessons. The really good uses of this kind of tech tend to require more background information.
The whole “using shapes” thing to explain OO inheritance is tried and true but has its limitations. Even if I was going to write a graphic interface that used shapes, I likely wouldn’t go full OO, or if I did, the base class would be much lighter than is used in typical explanations.
The best place for things like Mixins are where you have a lot of independence and are trying to add a feature that isn’t quite specific.
For example, I recently wrote something in Django that required a special kind of identifier. The identifier was a little more complicated than just a field and needed some methods. All of this was put inside of a mixin so that any Django object that needed this kind of identifier would just mix that in. The problem with teaching with this kind of example is then you have to explain what Django is, and how it models objects and how that relates to fields and a whole bunch of other stuff.
To your specific question, the amount of code saved by using this mixin probably doesn’t warrant the hiding of the implementation that happens the way it was done. In the real world I probably wouldn’t have coded it that way, but then I wouldn’t have had a mixin example.
rikhuygen on Nov. 9, 2021
Fair enough, thanks for your answer. (sorry for my late reply)
Alex on Jan. 6, 2023
Hi, Christoph, i’ve enjoyed the tutorial. Could you please answer to my following 2 questions (kind of similar to @Saul, but not entirely i guess):
–1. Is a bad practice to create instance variables(instead of passing it through kwargs) in the init of the Child class, if they are consumed only in the Parent classes, like in the first part of the following example:
class RightPyramid( Triangle,Square):
def __init__(self, base, slant_height):#no kwargs
self.base = base
self.slant_height = slant_height
self.width = self.base #added width in self
self.length = self.width #added length in self
super().__init__(base=self.base,height = self.slant_height) #no kwargs
class RightPyramid( Triangle,Square):
def __init__(self, base, slant_height,**kwargs): #added kwargs
self.base = base
self.slant_height = slant_height
kwargs['length'] = self.base #added k/v for length
super().__init__(base=self.base,height = self.slant_height,**kwargs) #added kwargs
–2. What would be the difference between passing the variable in kwargs or passing it separately through the keyword argument?
class RightPyramid( Triangle,Square):
def __init__(self, base, slant_height,**kwargs):
self.base = base
self.slant_height = slant_height
#kwargs['length'] = self.base # pass by kwargs
super().__init__(base=self.base,height = self.slant_height,length=self.base,**kwargs) # pass by extra keyword
Alex on Jan. 6, 2023
Sorry, just a later note, i guess having mixings is a nice feature, but one disadvantage(please correct me if wrong) is that it is polluting the instance with the unrelated instance variables of the RightPyramid class for example:
class RightPyramid(Square, Triangle, SurfaceAreaMixin):
def __init__(self, base, slant_height):
self.base = base
self.slant_height = slant_height
self.height = slant_height #not needed directly by the class
self.length = base #not needed directly by the class
self.width = base #not needed directly by the class
self.surfaces = [Square, Triangle, Triangle, Triangle, Triangle]
Christopher Trudeau RP Team on Jan. 7, 2023
Hi Alex,
Use of **kwargs
is the more general case, and I might do it if I’m not trying to expose just what the parent is doing. It is kind of a catch-all. If you’re building structures where you’re inheriting directly, there is nothing wrong with using named arguments instead.
One advantage of **kwargs
is pass-through to the next level. The best example I can think of is on database objects in Django: they have a save()
method that takes a number of parameters. You can override a save()
in a child class and only use the arguments that you need, but if some other coder comes along and needs the arguments you didn’t declare, they’re stuck. So best practice is to use **kwargs
for those things you’re not using and pass them all to the parent call.
It really just comes down to a design decision though.
As for your question about polluted classes, yeah, in this case that is an unfortunate side-effect. Shapes are a good analogy for teaching inheritance concepts, but are just an analogy. The best examples of mixins usually require deeper understanding of other systems than is helpful when teaching.
My most commonly used mixin is again in the Django space. I like to have date/time stamps on all database tables that track when a row was created and last updated. It can be useful when debugging production problems. As such, the mixin has these fields in it and just gets added to any data class I create. As these fields and any associated methods are independent of the classes mixing them in, there is no pollution problem. Teaching with this example is problematic though, because you can’t run the code unless you’ve got all the Django stuff setup, and so you’re left with having to use mediocre analogies.
With a lot of this stuff it comes down to practice. Even with practice there are plenty of times where I got bit by a design decision I’ve made and then have to come back in and change it to something else later. Welcome to the fun of coding :)
Andras on March 22, 2024
Hi Chris,
I enjoy your videos very much. At around 8:00, when you show all classes and their __init__
methods, RightPyramid
is calling Square
’s __init__
with a base
keyword, which it doesn’t have. Shouldn’t the last line read:
super().__init__(length=base, **kwargs)
?
Thanks
Christopher Trudeau RP Team on March 23, 2024
Hi Andras,
Multiple inheritance is tricky.
class RightPyramid(Square, Triangle):
def __init__(self, base, slant_height, **kwargs):
self.base = base
self.slant_height = slant_height
kwargs["height"] = slant_height
kwargs["length"] = base
super().__init__(base=base, **kwargs)
The RightPyramid
is inheriting from both Square
and Triangle
. In this code I’m using base
, height
, and length
in super().__init()
. With the base
being specified explicitly, and the height
and length
in kwargs
.
You’re right that the Square
has no base
, but because the arguments are named, and because Square
accepts **kwargs
: base
gets passed in but ignored.
The length
value that Square
accepts is set in the kwargs
dictionary inside RightPyramid
, and in fact is a copy of base
.
If Square
and Triangle
did not have **kwargs
in their constructors, this code would not work. Since they do have **kwargs
, you can pass anything you want in as a named argument, but their respective __init__
code only uses the named arguments they’re interested in. Everything else gets ignored.
Hope this helped.
Andras on March 25, 2024
Hi Chris,
Thanks for the answer. I think most of my confusion came from how **kwargs
are handled in general; it was not directly related to multiple inheritance. My previous proposal would have resulted in TypeError: Square.__init__() got multiple values for argument 'length'
if I ran it, which I did not (shame on me). Now that I understand **kwargs
better, let me ask one more minor thing: when you write the RightPyramid
constructor, you know the direct parent is Square
, so you could pass lenght=base
directly and pass on everything else in kwargs
that other parents (further up the chain) will need, like this:
class RightPyramid(Square, Triangle):
def __init__(self, base, slant_height, **kwargs):
self.base = base
self.slant_height = slant_height
kwargs["height"] = slant_height
kwargs["base"] = base # note "base" key!!!
super().__init__(length=base, **kwargs) # keyword length=base
#super().__init__(base, **kwargs) # positional arg would work too
Was there any specific reason why you “hid” length
in kwargs
and passed base
explicitly? With your approach kwargs
inside Square.__init__
becomes a different dict than the one you passed in. This is not a problem, I understand it now. This is the magic Python gives us automatically with kwargs
functionality (select what’s needed, ignore everything else). Just asking if there was any trick or design pattern you followed that I should keep in mind too when writing similar code.
regards, Andras
Christopher Trudeau RP Team on March 26, 2024
Hi Andras,
Glad you figured it out. Given the course is four years old, I’m not sure I can say exactly what was going through my head when I coded it.
I could argue the more generic use of keyword values is slightly safer in the case where you end up changing the inheritance order, but I’m not sure how important that is. If I was doing a code review, I’m not sure I would be too worried about this case.
The secret reality of all this stuff is that I very seldom use multiple inheritance. It is far more common in other languages (and sometimes far more necessary). Although it is a core computer science concept, and a useful tool in your toolbag, it is only one of a choice of approaches. When I do use multiple inheritance, it tends to be for mixins, which by definition have argument-less constructors, so this particular case we’re discussing doesn’t come up.
If you enjoy the OO aspect of Python and want to understand a bit more, one of the more recent courses does an in depth dive. It has three parts, covering:
- Class concepts, including attributes and methods (realpython.com/courses/python-class-object/)
- Inheritance and class internals (realpython.com/courses/python-class-inheritance/)
- Design guidance (realpython.com/courses/solid-principles-python/)
There is some overlap with this course, but far more depth. The third course talks about some of the rules of object-oriented design and touches on some of the whens/whys of usage.
Happy coding!
Andras on March 26, 2024
Thank you very much, Chris! I just finished that three-part course few days ago. I enjoyed it very much, especially the last part on design. I have many years C++ experience and I feel like I am still looking for the same design principles in Python that I used to work with in C++. It would be nice if you could put together some courses comparing Python vs C++ or Java, and explain what’s the pythonic approach to the most common problems in OOP that are usually tackled with design patterns in C++/Java (singleton, visitor pattern, etc.)
Keep up the good work!
cheers, Andras
Become a Member to join the conversation.
Richard Morris on Jan. 14, 2020
Superbly motivated, organized, and paced. Most appreciated