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 abonus
parameter with the default value of1.05
(bonus of 5%), - multiplies
amount
bybonus
, - 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 **kwargs
to catch all of them.
- Import
pandas
aspd
. - Define
LoggedDF
class inherited frompd.DataFrame
. - Define a constructor with arguments
*args
and**kwargs
that:- calls the
pd.DataFrame
constructor with the same arguments, - assigns
datetime.today()
toself.created_at
.
- calls the
# 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 toLoggedDF
that: - copies
self
to a temporary DataFrame using.copy()
, - creates a new column
created_at
in the temporary DataFrame and fills it withself.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!