Creating a subclass

The purpose of child classes — or sub-classes, as they are usually called – is to customize and extend functionality of the parent class. 

Let’s call the Employee class from what we have done earlier. In most organizations, managers enjoy more privileges and more responsibilities than a regular employee. So it would make sense to introduce a Manager class that has more functionality than Employee.

But a Manager is still an employee, so the Manager class should be inherited from the Employee class.

We’re going to add the display() method in the  Manager class.

# Small example of how to use inheritance from BankAccount into SavingsAccount. 
# In this case a SavingsAccount is a BankAccount.
# Empty class inherited from BankAccount
class SavingsAccount(BankAccount):
    pass

Adding functionality

  • Add methods as usual
  • Can use the data from both the parent and the childclass
class SavingsAccount(BankAccount)
    def __init__(self, balance, interest_rate):
        BankAccount.__init__(self, balance)
        self.interest_rate = interest_rate
       
    # New functionality
    def compute_interest(self, n_periods = 1):
        return self.balance * ( (1 + self.interest_rate) ** n_periods - 1)

Enough examples, let’s make it work:

class Employee:
  MIN_SALARY = 35000    

  def __init__(self, name, salary=MIN_SALARY):
      self.name = name
      if salary >= Employee.MIN_SALARY:
        self.salary = salary
      else:
        self.salary = Employee.MIN_SALARY
  def give_raise(self, amount):
    self.salary += amount      
        
# MODIFY Manager class and add a display method
class Manager(Employee):
  def display(self):
    print("Manager " + self.name)

mng = Manager("Jo Banjo", 92500)
print(mng.name)

# Call mng.display()
mng.display()

The Manager class now includes functionality that wasn’t present in the original class (the display() function) in addition to all the functionality of the Employee class. Notice that there wasn’t anything special about adding this new method.

Polymorphism

With classes and subclasses we’re starting to touch the subject of polymorphism. Polymorphism is the ability of an object to take on many forms. The most common use of polymorphism in OOP occurs when a parent class reference is used to refer to a child class object. 

Sometimes an object comes in many types or forms. If we have a button, there are many different draw outputs (round button, check button, square button, button with image) but they do share the same logic: onClick().  We access them using the same method. https://en.wikipedia.org/wiki/Polymorphism_(computer_science)

Method inheritance

Inheritance is powerful because it allows us to reuse and customize code without rewriting existing code. By calling methods of the parent class within the child class, we reuse all the code in those methods, making our code concise and manageable. 

In this exercise, you’ll continue working with the Manager class that is inherited from the Employee class. You’ll add new data to the class, and customize the give_raise() method from Chapter 1 to increase the manager’s raise amount by a bonus percentage whenever they are given a raise.

A simplified version of the Employee class, as well as the beginning of the Manager class from the previous lesson is provided for you in the script pane.

Add a give_raise() method to Manager that:

  • accepts the same parameters as Employee.give_raise(), plus a bonusparameter with the default value of 1.05 (bonus of 5%),
  • multiplies amount by bonus,
  • uses the Employee‘s method to raise salary by that product.
class Employee:
    def __init__(self, name, salary=30000):
        self.name = name
        self.salary = salary

    def give_raise(self, amount):
        self.salary += amount

        
class Manager(Employee):
    def display(self):
        print("Manager ", self.name)

    def __init__(self, name, salary=50000, project=None):
        Employee.__init__(self, name, salary)
        self.project = project

    # Add a give_raise method
    def give_raise(self, amount, bonus=1.05):
        Employee.give_raise(self, amount * bonus)
    
    
mngr = Manager("Ashta Dunbar", 78500)
mngr.give_raise(1000)
print(mngr.salary)
mngr.give_raise(2000, bonus=1.03)
print(mngr.salary)
    79550.0
    81610.0

In the new class, the use of the default values ensured that the signature of the customized method was compatible with its signature in the parent class. But what if we defined Manager‘s’give_raise() to have 2 non-optional parameters? What would be the result of mngr.give_raise(1000)? Experiment in console and see if you can understand what’s happening. Adding print statements to both give_raise() could help!

When typing print(mngr.give_raise(1000)) the console prints: None
The same for print(mngr.give_raise(1000, 0.05)), this also prints None

Why is this?

Customizing a DataFrame

In your company, any data has to come with a timestamp recording when the dataset was created, to make sure that outdated information is not being used. You would like to use pandas DataFrames for processing data, but you would need to customize the class to allow for the use of timestamps.

In this exercise, you will implement a small LoggedDF class that inherits from a regular pandas DataFrame but has a created_at attribute storing the timestamp. You will then augment the standard to_csv() method to always include a column storing the creation date.

Tip: all DataFrame methods have many parameters, and it is not sustainable to copy all of them for each method you’re customizing. The trick is to use variable-length arguments *args and **kwargsto catch all of them.

  • Import pandas as pd.
  • Define LoggedDF class inherited from pd.DataFrame.
  • Define a constructor with arguments *args and **kwargs that:
    • calls the pd.DataFrame constructor with the same arguments,
    • assigns datetime.today() to self.created_at.
# Import pandas as pd
import pandas as pd

# Define LoggedDF inherited from pd.DataFrame and add the constructor
class LoggedDF(pd.DataFrame):
    def __init__(self, *args, **kwargs):
        # Define a constructor with arguments *args and **kwargs that:
        # calls the pd.DataFrame constructor with the same arguments,
        pd.DataFrame.__init__(self, *args, **kwargs)
        self.created_at = datetime.today()
    
    
ldf = LoggedDF({"col1": [1,2], "col2": [3,4]})
print(ldf.values)
print(ldf.created_at)
  • Add a to_csv() method to LoggedDF that:
  • copies self to a temporary DataFrame using .copy(),
  • creates a new column created_at in the temporary DataFrame and fills it with self.created_at
  • calls pd.DataFrame.to_csv() on the temporary variable.
# Import pandas as pd
import pandas as pd

# Define LoggedDF inherited from pd.DataFrame and add the constructor
class LoggedDF(pd.DataFrame):
  
  def __init__(self, *args, **kwargs):
    pd.DataFrame.__init__(self, *args, **kwargs)
    self.created_at = datetime.today()
    
  def to_csv(self, *args, **kwargs):
    # Copy self to a temporary DataFrame
    temp = self.copy()
    
    # Create a new column filled with self.created_at
    temp["created_at"] = self.created_at
    
    # Call pd.DataFrame.to_csv on temp, passing in *args and **kwargs
    pd.DataFrame.to_csv(temp, *args, **kwargs)

Using *args and **kwargs allows you to not worry about keeping the signature of your customized method compatible. Notice how in the very last line, you called the parent method and passed an object to it that isn’t self. When you call parent methods in the class, they should accept some object as the first argument, and that object is usually self, but it doesn’t have to be!

Add a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.