In Python, instances inherit from classes, and classes inherit from superclasses. Here are the key ideas behind the machinery of attribute inheritance:
- Superclasses are listed in parentheses in a class header: To make a class inherit attributes from another class, just list the other class in parentheses in the new class statement’s header line. The class that inherits is usually called a subclass, and the class that is inherited from is its superclass.
- Classes inherit attributes from their superclasses: Just as instances inherit the attribute names defined in their classes, classes inherit all of the attribute names defined in their superclasses, Python finds them automatically when they’re accessed, if they don’t exist in the subclasses.
- Instances inherit attributes from all accessible classes: Each instance gets names from the class it’s generated from, as well as all of that class’s superclasses. When looking for a name, Python checks the instance, then its class, then all superclasses.
- Each object.attribute reference invokes a new, independent search: Python performs an independent search of the class tree for each attribute fetch expression. This includes references to instances and classes made outside class statements (e.g., X.attr), as well as references to attributes of the self instance argument in a class’s method functions. Each self.attr expression in a method invokes a new search for attr in self and above.
- Logic changes are made by subclassing, not by changing superclasses: By redefining superclass names in subclasses lower in the hierarchy (class tree), subclasses replace and thus customize inherited behavior.
The net effect, and the main purpose of all this searching, is that classes support factoring and customization of code better than any other language tool we’ve seen so far. On the one hand, they allow us to minimize code redundancy (and so reduce maintenance costs) by factoring operations into a single, shared implementation; on the other, they allow us to program by customizing what already exists, rather than changing it in place or starting from scratch.
To illustrate the role of inheritance, this next example builds on the previous one. First, we’ll define a new class, secondclass, that inherits all of firstclass’s(in chapter 1) names and provides one of its own:
class secondclass(firstclass): def display(self): print('Current value = "%s" ' % self.data)
secondclass defines the display method to print with a different format. By defining an attribute with the same name as an attribute in firstclass, secondclass effectively replaces the display attribute in its superclass.
Recall that inheritance searches proceed upward from instances to subclasses to superclasses, stopping at the first appearance of the attribute name that it finds. In this case, since the display name in secondclass will be found before the one in firstclass, we say that secondclass overrides firstclass’s display. Sometimes we call this act of replacing attributes by redefining them lower in the tree overloading.
The net effect here is that secondclass specializes firstclass by changing the behavior of the display method. On the other hand, secondclass still inherits the setdata method in firstclass verbatim. Let’s make an instance to demonstrate:
z = secondclass() z.setdata(50) z.display()
#Output- Current value = “50”
Now, here’s a crucial thing to notice about OOP: the specialization introduced in SecondClass is completely external to FirstClass. That is, it doesn’t affect existing or future FirstClass objects, like the x from the chapter 1 example:
#Output- thecleverprogrammer (same as done in chapter 1)
Rather than changing firstclass, we customized it. Naturally, this is an artificial example, but as a rule, because inheritance allows us to make changes like this in external components (i.e., in subclasses), classes often support extension and reuse better than functions or modules can.
That’it for today we will learn more about classes and Object oriented programming in the next chapter.