3.7 Inheritance: Tracing Initialization#

Let’s trace what happens when we initialize an object in the context of inheritance. We’ll use a fresh example about monsters:

from typing import Any


class Monster:
    def __init__(self, name: str) -> None:
        self.name = name
        self.hunger = 100
        self.energy = 0

    def eat(self, item: Any) -> None:
        raise NotImplementedError

    def sleep(self, minutes: int) -> None:
        self.energy += minutes

    def __str__(self) -> str:
        return self.name + 'Boo!'


class Gremlin(Monster):
    def __init__(self, name: str) -> None:
        Monster.__init__(self, name)
        self.stuffy = 'Bunny'

    def eat(self, item: Any) -> None:
        # Eating makes Gremlins more hungry!
        self.hunger = self.hunger + 10

    def __str__(self) -> str:
        return self.name + 'Mwahaha!'


if __name__ == '__main__':
    m = Gremlin('Julius')

In the main block, when we call Gremlin('Julius') we invoke the three-step process for creating an instance of a class:

  1. Create a new Gremlin object behind the scenes.

  2. Call __init__ with the new object passed to the parameter self, along with the other argument, 'Julius'.

  3. Return the new object (which we then assign to variable `m``, which must first be created).

Let’s go through these steps. In step 1, a new Gremlin object is created for us, and is currently empty. This is the state of memory afterwards:

A stack frame for the main block and a (currently empty) Gremlin object with id106.

In step 2, the __init__ method for Gremlin is called, with the new object passed as the parameter self, and the other argument, 'Julius', passed as name. The Gremlin object is still empty. At the moment after the Gremlin initializer has been called, this is the state of memory:

The stack now include a frame for the call to Gremlin's initializer. The object is still empty.

If we look at the code for the Gremlin initializer, we see that the first thing this method does is call the initializer for Monster, passing the values of self and name. The Gremlin object is still empty.

The stack now include a frame for the call to Monster's initializer. The object is still empty.

The Monster initializer has three lines of code. It use self to access the new Gremlin object and defines three new attributes within it, giving each a value:

The object now has three attributes inside it, which are name, hunger, and energy.

When the Monster initializer returns, we resume the Gremlin initializer where we left off. It has one line of code left, which uses self to access the Gremlin object and put a new attribute called stuffy inside it:

The stack frame for Monster's initializer has been popped, and the object now also has an attribute called stuffy.

Finally, step 2 is done and we pop the Gremlin initializer call from the stack. We are back in the __main__ block where we were in the middle of completing an assignment statement: m = Gremlin('Julius'). We have evaluated the right-hand side, yielding the id of a Gremlin object, id106. We will assign it to m, but since there is no m in the frame where we are working, we make one first:

The stack frame for Gremlin's initializer has been popped also, and we are back to the stack frame for the main block. It contains variable m, which holds id106, the id of the new Gremlin object we just initialized.

A lot had to happen to execute that one-line main block!