Thursday, March 4, 2021

Python Morsels: Inheriting one class from another

Watch first

Need a bit more background? Or want to dive deeper?

Watch other class-related screencasts.

Transcript:

How does class inheritance work in Python?

Creating a class that inherits from another class

We have a class called FancyCounter, that inherits from another class, Counter (which is in the collections module in the Python standard library):

from collections import Counter


class FancyCounter(Counter):
    def commonest(self):
        (value1, count1), (value2, count2) = self.most_common(2)
        if count1 == count2:
            raise ValueError("No unique most common value")
        return value1

The way we know we're inheriting from the Counter class because when we defined FancyCounter, just after the class name we put parentheses and wrote Counter inside them.

To create a class that inherits from another class, after the class name you'll put parentheses and then list any classes that your class inherits from.

In a function definition, parentheses after the function name represent arguments that the function accepts. In a class definition the parentheses after the class name instead represent the classes being inherited from.

Usually when practicing class inheritance in Python, we inherit from just one class. You can inherit from multiple classes (that's called multiple inheritance), but it's a little bit rare. We'll only discuss single-class inheritance right now.

Methods are inherited from parent classes

To use our FancyCounter class, we can call it (just like any other class):

>>> from fancy_counter import FancyCounter
>>> letters = FancyCounter("Hello there!")

Our class will accept a string when we call it because the Counter class has implemented a __init__ method (an initializer method).

Our class also has a __repr__ method for a nice string representation:

>>> letters
FancyCounter({'e': 3, 'l': 2, 'H': 1, 'o': 1, ' ': 1, 't': 1, 'h': 1, 'r': 1, '!': 1})

It even has a bunch of other functionality too. For example, it has overridden what happens when you use square brackets to assign key-value pairs on class instances:

>>> letters['l'] = -2
>>> letters
FancyCounter({'e': 3, 'H': 1, 'o': 1, ' ': 1, 't': 1, 'h': 1, 'r': 1, '!': 1, 'l': -2})

We can assign key-value pairs because our parent class, Counter creates dictionary-like objects.

All of that functionality was inherited from the Counter class.

Adding new functionality while inheriting

So our FancyCounter class inherited all of the functionality that our Counter class has but we've also extended it by adding an additional method, commonest, which will give us the most common item in our class.

When we call the commonest method, we'll get the letter e (which occurs three times in the string we originally gave to our FancyCounter object):

>>> letters.commonest()
'e'

Our commonest method relies on the most_common method, which we didn't define but which our parent class, Counter, did define:

    def commonest(self):
        (value1, count1), (value2, count2) = self.most_common(2)
        if count1 == count2:
            raise ValueError("No unique most common value")
        return value1

Our FancyCounter class has a most_commonest method because our parent class, Counter defined it for us!

Overriding inherited methods

If we wanted to customize what happens when we assigned to a key-value pair in this class, we could do that by overriding the __setitem__ method. For example, let's make it so that if we assign a key to a negative value, it instead assigns it to 0.

Before when we assigned letters['l'] to -2, we'd like it to be set to 0 instead of -2 (it's -2 here because we haven't customized this yet):

>>> letters['l'] = -2
>>> letters['l']
-2

To customize this behavior we'll make a __setitem__ method that accepts self, key, and value because that's what __setitem__ is given by Python when it's called:

    def __setitem__(self, key, value):
        value = max(0, value)

The above __setitem__ method basically says: if value is negative, set it to 0.

If we stop writing our __setitem__ at this point, it wouldn't be very useful. In fact that __setitem__ method would do nothing at all: it wouldn't give an error, but it wouldn't actually do anything either!

In order to do something useful we need to call our parent class's __setitem__ method. We can call our parent class' __setitem__ method by using super.

    def __setitem__(self, key, value):
        value = max(0, value)
        return super().__setitem__(key, value)

We're calling super().__setitem__(key, value), which will call the __setitem__ method on our parent class (Counter) with key and our new non-negative value.

Here's a full implementation of this new version of our FancyCounter class:

from collections import Counter


class FancyCounter(Counter):
    def commonest(self):
        (value1, count1), (value2, count2) = self.most_common(2)
        if count1 == count2:
            raise ValueError("No unique most common value")
        return value1
    def __setitem__(self, key, value):
        value = max(0, value)
        return super().__setitem__(key, value)

To use this class we'll call it and pass in a string again:

>>> from fancy_counter import FancyCounter
>>> letters = FancyCounter("Hello there!")

But this time, if we assign a key to a negative value, we'll see that it will be assigned to0 instead:

>>> letters['l'] = -2
>>> letters['l']
0

Summary

If you want to extend another class in Python, taking all of its functionality and adding more functionality to it, you can put some parentheses after your class name and then write the name of the class that you're inheriting from.

If you want to override any of the existing functionality in that class, you'll make a method with the same name as an existing method in your parent class. Usually (though not always) when overriding an existing method, you'll want to call super in order to extend the functionality of your parent class rather than completely overriding it.

Using super allows you to delegate back up to your parent class, so you can essentially wrap around the functionality that it has and tweak it a little bit for your own class's use.

That's the basics of class inheritance in Python.



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...