6 Inheritance

Methods

Often two classes may share attributes and other things in common. Writing two classes would likely be redundant and inefficient if you fix a bug in one for example. So instead we define a base class that includes the common functionality and then a subclass. Here we call classes above subclasses a superclass of that subclass. The ultimate superclass is Object.

Example:

class Employee:
	def get_monthly_payment(self) -> float:
	
		""" 
		Return the amount that this Employee should be 
		paid in one month.
		Round the amount to the nearest cent.
	    """
		raise NotImplementedError

def pay(self, pay_date: date) -> None:
    """
    Pay this Employee on the given date and record the 
	payment.
    (Assume this is called once per month.)
    """
    payment = self.get_monthly_payment()
    print(f'An employee was paid {payment} on {pay_date}.')

class SalariedEmployee(Employee):
	pass

class HourlyEmployee(Employee):
	pass

Here since we have not yet completed the get_monthly_payment function, it is called an abstract method also any class with an abstract method is an abstract class.

To finish we have:

class SalariedEmployee(Employee):
    """
    An employee whose pay is computed based on an 
    annual salary.
    """
    def get_monthly_payment(self) -> float:
        """
        Return the amount that this Employee should be 
        paid in one month.

        Round the amount to the nearest cent.
        """
        return round(60000 / 12, 2)


class HourlyEmployee(Employee):
    """
    An employee whose pay is computed based on an
    hourly rate.
    """
    def get_monthly_payment(self) -> float:
        """
        Return the amount that this Employee should be 
        paid in one month.

        Round the amount to the nearest cent.
        """
        return round(160 * 20, 2)

Now when calling a method, python will look at the lowest subclass first for that function, even if that function is from a superclass,
example:

>>> type(fred)
<class 'employee.SalariedEmployee'>

so fred.get_monthly_payment() will check methods in the SalariedEmployee class first. If it doesn't find anything it raises and AttributeError.

Attributes and initializers

Since methods at the subclass level are prioritized, rather than override the superclass __init__ method, we would do the following:

class Employee:
    """
    An employee of a company.

    This is an abstract class. Only subclasses should be
    instantiated.
    
    === Attributes ===
    id_: This employee's ID number.
    name: This employee's name.
    """
    id_: int
    name: str

    def __init__(self, id_: int, name: str) -> None:
        """Initialize this employee.

        Note: This initializer is meant for internal use 
        only;
        Employee is an abstract class and should not be 
        instantiated directly.
        """
        self.id_ = id_
        self.name = name

class SalariedEmployee(Employee):
    """
    An employee whose pay is computed based on an
    annual salary.

    === Attributes ===
    salary: This employee's annual salary

    === Representation invariants ===
    - salary >= 0
    """
    id_: int
    name: str
    salary: float

    def __init__(self, id_: int, name: str, salary: float) -> None:
        """
        Initialize this salaried Employee.

        >>> e = SalariedEmployee(14, 'Fred Flintstone', 5200.0)
        >>> e.salary
        5200
        """
        # Note that to call the superclass initializer, we need to use the
        # full method name '__init__'. This is the only time you should write
        # '__init__' explicitly.
        Employee.__init__(self, id_, name)
        self.salary = salary # extra attribute

If we use:

super().__int__(id_, name)

We don't have to pass self

So, subclasses only inherit methods; we have to directly call the method from the superclass to pass the attributes. In fact, abstract classes aren't meant to be initialized directly in the first place and are considered private. Therefore, they can be freely overridden so each subclass has a different initializer signature but will all call the superclass' __init__ method. We call the parent class the ancestor. The same thing can occur with other special methods. We can override, adopt, or extend ancestor methods by using them as helpers.

The Employee class represents the shared public interface of SalariedEmployee and HourlyEmployee. The public interface is how client code interacts with the class's methods and attributes. The Employee class is a polymorphism; it takes different forms than other classes. When an instance attribute is associated with another class, it is called composition.

Options for Inherited Method:

  1. Don't override
  2. Override abstract methods (mandatory)
  3. Complete override
  4. Use and extend

The object Class and Python Special Methods

An object is not just theoretical but a class in Python. It is a superclass/ancestor of every other class.

Inherited special methods come from this class.