Categories
Code

Step 10: Implement Inheritance with data models

If you’re relatively new to Object Oriented Programming (OOP), you’ve probably learned about a feature known as “inheritance“. It’s a really neat feature, but in practice and in real life… well… it isn’t used as much as you might think. As you may have noticed throughout this tutorial, we haven’t created any inheritance of our own.

A common saying in the software development world is:

Favor Composition over Inheritance

But why do we say that? Well, it usually comes down to flexibility.

Inheritance makes code more rigid… less flexible. But it does provide a kind of “contract” on how things can or will be done. And in certain instances, it can make your code DRY-er. (Remember WET vs DRY? If not, feel free to review Chapter 3…)

Wikipedia has an entry on Composition over Inheritance if you wish to read more about this: https://en.wikipedia.org/wiki/Composition_over_inheritance

Implementing Inheritance with the Image data models

TheCatAPI has 2 data model types for Images:

Image – Full version: https://docs.thecatapi.com/api-reference/models/image-full

Image – Short version: https://docs.thecatapi.com/api-reference/models/image-short

If you click back and forth between the two documents, you can quickly figure out that they’re almost exactly the same. The full version has 3 extra fields (sub_id, created_at, and original_filename) that the short version does not.

And that’s it! That’s the only difference.


Let’s implement these data models… Let’s start with Category since we haven’t implemented that yet:

class Category:
    def __init__(self, id: int, name: str):
        self.id = id
        self.name = name

That was easy. Let’s implement the “Image – Short version”

class ImageShort:
    def __init__(self, id: int, url: str, categories: List[Category] = None, breeds: List[Breed] = None, **kwargs):
        self.id = id
        self.url = url
        self.categories = [Category(**c) for c in categories] if categories else []
        self.breeds = [Breed(**b) for b in breeds] if breeds else []
        self.__dict__.update(kwargs)

Only 7 lines, but a lot going on here. Let’s review what we did here.

class ImageShort:
    def __init__(self, id: int, url: str, categories: List[Category] = None, breeds: List[Breed] = None, **kwargs):

Lines 1 and 2 are easy to follow. We create an ImageShort class with a constructor and 2 mandatory parameters (id and url), and 2 optional parameters (categories and breeds) as well as a keyword-arguments variable (**kwargs) — in case anything else is passed in via key-value pair.

        self.id = id
        self.url = url

Lines 3 and 4 we assign id and url, to their respective instance versions (self.id and self.url) — easy peasy.

The next 2 lines are dense and packed, so let’s unpack what we did here:

self.categories = [Category(**c) for c in categories] if categories else []
self.breeds     = [Breed(**b)    for b in breeds]     if breed      else []

Extra whitespace has been added on the 2nd line so that it is easy to see that it is like the 1st line.

Let’s just focus on the first line and maybe we can then understand the other:

self.categories = [Category(**c) for c in categories] if categories else []

We assign to the instance variable (self.categories)… well… we assign it one of 2 things:

Here we leverage a feature in Python known as a ternary operator. We check if categories parameter DOES have anything in it.

If categories does have something in it, well, it’s probably a List of Dictionaries containing Category JSON data.

[Category(**c) for c in categories]

So we use a list comprehension to iterate over each dictionary item in the list and use the ** operator to unpack the dictionary data into the data model. And the list produced by the list comprehension is assigned to self.categories.

Though, if categories is nothing or empty, we assign self.categories with an empty list [].


Now if you’re saying to yourself right now, “Wow, this ternary operator and list comprehension stuff are too difficult to understand.” (Like this example here:)

self.categories = [Category(**c) for c in categories] if categories else []

No problem. An equivalent version would look like:

    self.categories = []
    if categories:
        for c in categories:
            category = Category(**c)
            self.categories.append(category)

If this verbose version makes more sense to you than that single line solution, hey, that’s okay, too!

Combining an instance variable assignment with a ternary operator and a list comprehension may seem a little sophisticated at first. But as you do more and more fancy tricks like this, in time, you’ll realize that they’re quite trivial. Perhaps elegant, even!


Now when we look at:

self.breeds     = [Breed(**b)    for b in breeds]     if breed      else []

Oh, we see that it’s not so hard to understand after all!

Either we unpack the list of dictionary data into a list of Breed objects
-or-
breeds parameter is empty and we assign an empty list []

All in 1 line. Pretty slick!

And the last line of this ImageShort constructor:

self.__dict__.update(kwargs)

Well, this is just that same last line in the previous chapter which takes any unhandled key-value pairs in the parameter list and stuffs them into the internal __dict__ python object dictionary. (See the previous chapter, if you need a review on this…)


This is great. Now we have an ImageShort class:

class ImageShort:
    def __init__(self, id: int, url: str, categories: List[Category] = None, breeds: List[Breed] = None, **kwargs):
        self.id = id
        self.url = url
        self.categories = [Category(**c) for c in categories] if categories else []
        self.breeds = [Breed(**b) for b in breeds] if breeds else []
        self.__dict__.update(kwargs)

How do we implement Inheritance when we make the ImageFull class?

class ImageFull(ImageShort):
    def __init__(self, id: int, url: str, sub_id: int = 0, created_at: str = '', original_filename: str = '',
                 categories: List[Category] = None, breeds: List[Breed] = None, **kwargs):
        super().__init__(id, url, categories, breeds)
        self.sub_id = sub_id
        self.created_at = created_at
        self.original_filename = original_filename
        self.__dict__.update(kwargs)

The first line shows that our class ImageFull inherits ImageShort by containing it in parentheses.

The constructor line is very similar. In fact, when I wrote this code, I just copy and pasted the same __init__ constructor and then added 3 more parameters. I decided to make them optional and gave them “False-y” default values (either empty string or 0).

The next line is the secret sauce of using inheritance:

super().__init__(id, url, categories, breeds)

super() is a generic name of the inherited class. Here we’re calling the parent (aka. super) class’s constructor with all the parameters we passed in. (Well, all the parameters that it understands.)

Notice that we did NOT pass along the **kwargs parameter in the super().__init__()

That one line partially initializes most of the class variables (the ones associated with ImageShort). Then we continue like normal:

        self.sub_id = sub_id
        self.created_at = created_at
        self.original_filename = original_filename
        self.__dict__.update(kwargs)

And that’s it!

The ImageFull class has all the same instance variables as ImageShort as well as 3 additional variables. We used inheritance to save ourselves some work.

Testing the code with breakpoints

Let’s try out this new code and watch it work in action. To do this, we are going to quickly cover a new topic: Breakpoints.

It’s likely that many of you reading this already know how to use breakpoints, but in case you do not know about breakpoints, this section will try to walk you through using them.


To add a breakpoint on the line, left-click with your mouse in the gutter (empty area) between the line number and the line below.

super().__init__(id, url, categories, breeds, **kwargs)

A red dot (🔴) should appear between the line number and your code. This indicates an unconditional breakpoint for when running in debugging mode. That means the Python interpreter will pause the running code here and wait for you to instruct how to proceed.

Restart your Python console/REPL by clicking the Rerun button (Ctrl + F5) kind of like this icon: [ ↪ ]

Then type in the following commands in the Python Console/REPL:

from the_cat_api.models import *

Then click the “bug” icon 🐞 to attach debugger

Debugger connected.
full_image = ImageFull(id=1, url="http://somewebsite.com/cat.gif", sub_id=5, created_at="1/1/1999", original_filename="prettykitty.gif")

At this point, the debugger should be halted on the line that you placed a breakpoint. Do you want to see your code in action? Press the F7 key (aka. step-into).

Notice how it skips from the ImageFull class to the ImageShort class?

You can continue on by pressing the F8 key (aka. step-over) until the debugger exits both constructors.

Look in your variable watch screen on the right side and you’ll see a variable called full_image. Click on the triangle icon (>) to open up the variable and you’ll see all the instance variables.

And with that, you just used Inheritance to keep your code tight and DRY.

Now that we have enough data models implemented we can move on to the next chapter:

Step 11: Creating high-level endpoint abstractions


Source code: https://github.com/PretzelLogix/py-cat-api/tree/10_inheritance_models

4 replies on “Step 10: Implement Inheritance with data models”

Heh. I think I had cats-on-the-brain when I was writing this… 😄 (Good catch; corrected.)

Leave a Reply

Your email address will not be published. Required fields are marked *