Welcome to part 17 of the intermediate Python programming tutorial series. In this part of the series, we're going to discuss inheritance.
Inheritance is a major form of modularity, but actually also plays roles in scaling and maintainability. When creating classes and using Object Oriented Programming, we're usually doing it under the idea that we're making something that either other people will use or our future selves will use. One way to think about it is that you're creating a product that people will use. In my cases, you're creating a tool that people will use. Consider a hammer class. Your hammer will have attributes like a handle, and then a metal head of some kind for pounding nails. This is a minimum viable product (MVP), and should be shipped in this state. You might have various ideas, like maybe having a hammer with two sides to head for various sizes of nails, or maybe some sort of blended head where one side is metal and the other is rubber!
It can be tempting to implement your ideas, because they sound reasonable to you, but you don't want to do that. You will want to let clients modify the hammer class if they want a rubber hammer (the equivalent of buying a rubber hammer instead). Instead, you want to just ship the simplest version of your product, and see what clients think. You may find that almost all of your clients are requesting a feature for the hammer to have one side for pounding nails, and the other side for prying out nails. You hadn't even considered it, but now you're adding something that makes a whole lot more sense in practice.
Inheritance allows you to create super simple classes, and for your users to modify the classes. If you get enough requests for something new, then you can consider adding it, but first let the clients play with it. Back to our Blob example, let's consider inheriting from the Blob
class:
class BlueBlob(Blob): pass
We're doing nothing in the class, besides inheriting from the Blob
class. The Blob
class is our "Base" class, or Parent class. It's also our "super" class. I wouldn't necessarily suggest you leave Python and start applying these terms to all other languages, but, in terms of Python, you can call it any of these things, and you will hear it called all of these. This is mostly because these terms are actually coming from people from other languages, who are applying their own OOP terms to Python's.
The new BlueBlob
class, inheriting from Blob
is our "child" class, or "subclass." When inheriting from another class, you are inheriting everything, all of the methods, including the special methods. Thus, in this case, we're inheriting everything and changing nothing. We can, however, now add our own methods, or even overwrite methods. If, say, we write a new __init__
method, the BlueBlob
class will honor ours OVER the original one. We'll see later an issue with this, and how to overcome it. For now, let's change:
def main(): blue_blobs = dict(enumerate([BlueBlob(BLUE,WIDTH,HEIGHT) for i in range(STARTING_BLUE_BLOBS)])) red_blobs = dict(enumerate([BlueBlob(RED,WIDTH,HEIGHT) for i in range(STARTING_RED_BLOBS)]))
We're just simply using the new class. The result should be the same as before:
Blobs doing blob things!...blobjects...if you will.
Now, a common task might be to add our own __init__
method, but what happens if we try this?
class BlueBlob(Blob): def __init__(self): pass
We get an error:
TypeError: __init__() takes 1 positional argument but 4 were given
We've overwritten the original dunder init method! So do we need to basically write all of the code from the original init method? Nope! We have super()
at our disposal!
class BlueBlob(Blob): def __init__(self, color, x_boundary, y_boundary): super().__init__(color, x_boundary, y_boundary) self.color = BLUE
Now, we should have all blobs as blue.
The super()
call allows us to dynamically refer to the base class. We could also do:
class BlueBlob(Blob): def __init__(self, color, x_boundary, y_boundary): Blob.__init__(self, color, x_boundary, y_boundary) self.color = BLUE
The problem here, however, is we'll quickly run into a lot of trouble when doing multiple inheritance. For a while, Python's super()
has been somewhat controversial, but there's a great talk by Raymond Hettinger that you should check out called Super considered super!. It's fairly long, but can help you to understand Python's super, and Raymond is a great speaker.
As a side note, you might see him and other people making classes like: SomeClass(object)
(note the "object"). This was how one might use the New-Style classes that came about a long time ago. People keep putting it in there, but new-style classes are the default in Python 3, and this is not necessary anymore.
We can also add new methods, like:
class BlueBlob(Blob): def __init__(self, color, x_boundary, y_boundary): Blob.__init__(self, color, x_boundary, y_boundary) self.color = BLUE def move_fast(self): self.x += random.randrange(-5,5) self.y += random.randrange(-5,5)
Then within the draw_environment
method, we can change blob.move() to
:blob.move_fast()
def draw_environment(blob_list): game_display.fill(WHITE) for blob_dict in blob_list: for blob_id in blob_dict: blob = blob_dict[blob_id] pygame.draw.circle(game_display, blob.color, [blob.x, blob.y], blob.size) blob.move_fast() blob.check_bounds() pygame.display.update()