Inheriting Variables and Methods¶
Mechanics of Defining a Subclass¶
We said that inheritance provides us a more elegant way of, for example, creating Dog
and Cat
types, rather than making a very complex Pet
class. In the abstract, this is pretty intuitive: all pets have certain things, but dogs are different from cats, which are different from birds. Going a step further, a Collie dog is different from a Labrador dog, for example. Inheritance provides us with an easy and elegant way to represent these differences.
Basically, it works by defining a new class, and using a special syntax to show what the new sub-class inherits from a super-class. So if you wanted to define a Dog
class as a special kind of Pet
, you would say that the Dog
type inherits from the Pet
type. In the definition of the inherited class, you only need to specify the methods and instance variables that are different from the parent class (the parent class, or the superclass, is what we may call the class that is inherited from. In the example we’re discussing, Pet
would be the superclass of Dog
or Cat
).
Here is an example. Say we want to define a class Cat
that inherits from Pet
. Assume we have the Pet
class that we defined earlier.
We want the Cat
type to be exactly the same as Pet
, except we want the sound cats to start out knowing “meow” instead of “mrrp”, and we want the Cat
class to have its own special method called chasing_rats
, which only Cat
s have.
For reference, here’s the original Tamagotchi code
All we need is the few extra lines at the bottom of the ActiveCode window! The elegance of inheritance allows us to specify just the differences in the new, inherited class. In that extra code, we make sure the Cat
class inherits from the Pet
class. We do that by putting the word Pet in parentheses, class Cat(Pet):
. In the definition of the class Cat
, we only need to define the things that are different from the ones in the Pet
class.
In this case, the only difference is that the class variable sounds
starts out with the string "Meow"
instead of the string "mrrp"
, and there is a new method chasing_rats
.
We can still use all the Pet
methods in the Cat
class, this way. You can call the __str__
method on an instance of Cat
to print
an instance of Cat
, the same way you could call it on an instance of Pet
, and the same is true for the hi
method – it’s the same for instances of Cat
and Pet
. But the chasing_rats
method is special: it’s only usable on Cat
instances, because Cat
is a subclass of Pet
which has that additional method.
In the original Tamagotchi game in the last chapter, you saw code that created instances of the Pet
class. Now let’s write a little bit of code that uses instances of the Pet
class AND instances of the Cat
class.
And you can continue the inheritance tree. We inherited Cat
from Pet
. Now say we want a subclass of Cat
called Cheshire
. A Cheshire cat should inherit everything from Cat
, which means it inherits everything that Cat
inherits from Pet
, too. But the Cheshire
class has its own special method, smile
.
How the interpreter looks up attributes¶
So what is happening in the Python interpreter when you write programs with classes, subclasses, and instances of both parent classes and subclasses?
This is how the interpreter looks up attributes:
- First, it checks for an instance variable or an instance method by the name it’s looking for.
- If an instance variable or method by that name is not found, it checks for a class variable. (See the previous chapter for an explanation of the difference between instance variables and class variables.)
- If no class variable is found, it looks for a class variable in the parent class.
- If no class variable is found _there_, the interpreter looks for a class variable in THAT class’s parent, if it exists – the “grandparent” class.
- This process goes on until the last ancestor is reached, at which point Python will signal an error.
Let’s look at this with respect to some code.
Say you write the lines:
new_cat = Cheshire("Pumpkin")
print(new_cat.name)
In the second line, after the instance is created, Python looks for the instance variable name
in the new_cat
instance. In this case, it exists. The name on this instance of Cheshire
is Pumpkin
. There you go!
When the following lines of code are written and executed:
cat1 = Cat("Sepia")
cat1.hi()
The Python interpreter looks for hi
in the instance of Cat
. It does not find it, because there’s no statement of the form cat1.hi = ...
. (Be careful here – if you had set an instance variable on Cat called hi
it would be a bad idea, because you would not be able to use the method that it inherited anymore. We’ll see more about this later.)
Then it looks for hi as a class variable (or method) in the class Cat, and still doesn’t find it.
Next, it looks for a class variable hi
on the parent class of Cat
, Pet
. It finds that – there’s a method called hi
on the class Pet
. Because of the ()
after hi
, the method is invoked. All is well.
However, for the following, it won’t go so well
p1 = Pet("Teddy")
p1.chasing_rats()
The Python interpreter looks for an instance variable or method called chasing_rats
on the Pet
class. It doesn’t exist. Pet
has no parent classes, so Python signals an error.
Check your understanding
-
exceptions-1: After you run the code,
- 1
- Neither Cheshire nor Cat defines an __init__ constructor method, so the grandaprent class, Pet, will have it's __init__ method called. Check how many instance variables it sets.
- 2
- Neither Cheshire nor Cat defines an __init__ constructor method, so the grandaprent class, Pet, will have it's __init__ method called. Check how many instance variables it sets.
- 3
- Neither Cheshire nor Cat defines an __init__ constructor method, so the grandaprent class, Pet, will have it's __init__ method called. Check how many instance variables it sets.
- 4
- Neither Cheshire nor Cat defines an __init__ constructor method, so the grandaprent class, Pet, will have it's __init__ method called. That constructor method sets the instance variables name, hunger, boredom, and sounds.
new_cat = Cheshire("Pumpkin")
, how many instance variables exist for the new_cat instance of Cheshire?
- We are Siamese if you please. We are Siamese if you don’t please.
- another_cat is an instance of Siamese, so its song() method is invoked.
- Error
- another_cat is an instance of Siamese, so its song() method is invoked.
- Pumpkin
- This would print if the statement was print new_cat.name.
- Nothing. There’s no print statement.
- There is a print statement in the method definition.
exceptions-2: What would print after running the following code:
new_cat = Cheshire("Pumpkin”)
class Siamese(Cat):
def song(self):
print("We are Siamese if you please. We are Siamese if you don’t please.")
another_cat = Siamese("Lady")
another_cat.song()
- We are Siamese if you please. We are Siamese if you don’t please.
- You cannot invoke methods defined in the Siamese class on an instance of the Cheshire class. Both are subclasses of Cat, but Cheshire is not a subclass of Siamese, so it doesn't inherit its methods.
- Error
- You cannot invoke methods defined in the Siamese class on an instance of the Cheshire class. Both are subclasses of Cat, but Cheshire is not a subclass of Siamese, so it doesn't inherit its methods.
- Pumpkin
- This would print if the statement was print new_cat.name.
- Nothing. There’s no print statement.
- There is a print statement in the method definition for Siamese.
exceptions-3: What would print after running the following code:
new_cat = Cheshire("Pumpkin”)
class Siamese(Cat):
def song(self):
print("We are Siamese if you please. We are Siamese if you don’t please.")
another_cat = Siamese("Lady")
new_cat.song()