Sign in to your Python Morsels account to save your screencast settings.
Don't have an account yet? Sign up here.
How does class inheritance work in Python?
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.
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 (a.k.a. mappings).
All of that functionality was inherited from the Counter
class.
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!
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
Want some more practice with classes in Python?
The Diving Into Classes exercise path includes 6 class exercises for advanced beginners. Python Morsels also includes dozens more exercises on classes and object-oriented Python as well.
Need to fill-in gaps in your Python skills?
Sign up for my Python newsletter where I share one of my favorite Python tips every week.
Classes are a way to bundle functionality and state together.
The terms "type" and "class" are interchangeable: list
, dict
, tuple
, int
, str
, set
, and bool
are all classes.
You'll certainly use quite a few classes in Python (remember types are classes) but you may not need to create your own often.
To track your progress on this Python Morsels topic trail, sign in or sign up.
Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.