Inheritance At present we know about defining classes and constructing object that are instances of classes. We have explored various relationships between an object and the class from which it was constructed. The is commonly called object-based programming. To get to object-oriented programming, we must also see how to define subclass from superclasses (sometimes called derived classes from base classes) and see how objects constructed from these sub/derived classes behave. Specifically, we will learn how the attibutes of instances constructed from objects in an inheritance hierarchy are found: this is fundamentally what inheritnace is about. We we specify that a sub/derived class has one or more super/base classes when we define the subclass: we include the name of all its superclases in parentheses following the class statment. If a subclass has not superclasses, the parenthese (and empty content) are optional. class subclass(superclass1,superclass2, ...): Many languages support just single inheritance (a subclass may have only one direct superclass: Java is one example) but Python (like C++) supports multiple inheritance: a subclass can have many superclasses. To start the discussion, we will discuss subclasses with a single superclass, and generalize this restriction later. At present we know that instances refer to the classes they are constructed from, and we can use the type function on any instance to return a reference to the class it was constructed from. When we specify an attibute of an instance, Python first looks for the attribute in the namespace of the instance itself, but it if doesn't find the attribute there, it looks for the attribute in the class the object was constructed from. We captured this rule in the Fundamental Equation of Object-Oriented Programming (shown below in its current form, but soon expanded for inheritance). If m is a method we have seen that o.m(...) -> type(o).m(o,...) When looking up a method attribute for instance o, if it is not found in o (it typically isn't) Python checks for the attribute in type(o): the class o was constructed from, calling that method with o as the first argument (which is why methods defined in classes all have the first parameter self). This mechanism works for non-method attributes too: if any attribute is not found in the object's namespace, we search the namespace of the class it was constructed from. By this mechanism, an object doesn't need to store all the methods that operate on it. The methods are stored in the objects class, and looked up there as needed. Again, this works for class intance variables, but it less likely. For classes defined with superclasses, we generalize this translation as follows: if Python cannot find an attribute in the instance, and cannot find the attributet in the class the instance was constructed from, Python look in the superclass (again, we are no assuming only one) of the class....and this process of looking in superlclasses continues, if necessary, until there are no more suplerclasses. Ultimately if no attribute is found, Python raises a NameError exception. .... Subclasses (1) define new attributes (2) inherit attributes (and don't override them) (3) override inherited attributes We will look at the counters.py module to see concrete examples of all these behaviors. This module solves real problem (defines a modular counter class), but does so with a mechanism much more complicated than needed. In the next lecture we will see real uses of inheritance: we show to to implement a defaultdict; how to use mix-inheritance to implement privacy, and see a function that searches an instance-class-superclass hierarch just a Python does (including multiple inheritance).