Python – Object Oriented Programming – Class Inheritance

Inheritance and Polymorphism

Instance and Class Data

Core Principles of OOP

  • Inheritance
    • Extending functionality of existing code
  • Polymorphism
    • Creating an unified interface
  • Encapsulation
    • Bundling of data and Methods

Instance-level data

class Employee:
  def __init__(self, name, salary):
    self.name = name
    self.salary = salary

emp1 = Employee("Theo Mill", 50000)
emp2 = Employee("Martina Kosova", 65000)
  • name , salary are instance attributes
  • self binds to an instance

Class-level data

class MyClass:
  # Define a class attribute
  CLASS_ATTR_NAME = attr_value
  • data shared among all instances of a class
  • define class attributes in the body of class
  • “Global variable” within the class –> all objects with this class will have this attribute
class Employee:
  # Define a class attribute
  MIN_SALARY = 30000    

  def __init__(self, name, salary):
    self.name = name
    if salary >= Employee.MIN_SALARY:
      self.salary = salary
    else:
      self.salary = Employee.MIN_SALARY

emp1 = Employee('TBD', 40000)
print(emp1.MIN_SALARY)
> 30000
emp2 = Employee('TBD', 60000)
print(emp2.MIN_SALARY)
> 30000
  • MIN_SALARY is shared among all instances
  • No use of the self to define a class attribute
  • use ClassName.ATTR_NAME to access the class attribute value

 

Why use class attributes ?

Global constants related to class

  • minimal or maximal values for attributes
  • commonl used values and constants  ( pi )

Class Methods

  • Methods are already shared for each instance of the same class
  • Class methods cannot use instance level data
class MyClass:
  @classmethod       # <-- use decorator to declare a class method
  def my_method(cls, args, ...):
    # do your thing
    # do not use any instance attributes

MyClass.mymethod(args,...)

Alternative constructors

class Employee:
  MIN_SALARY = 30000
  def __init__(self, name, salary = 30000):
    self.name = name
    if salary >= Employee.MIN_SALARY:
      self.salary = salary
    else:
      self.salary = Employee.MIN_SALARY

  @classmethod
  def from_file(cls, filename):
    with open(filename, "r") as f:
      name = f.readline()
    return cls(name)

# Create an employee without calling Employee()
emp = Employee.from_file("employee_data.txt")
type(emp)
> __main__.Employee
  • can only have one __init__()
  • use class methods to create objects
  • use return to return an object
  • cls(...) will call __init__(...)

Exercise

class Player:
  MAX_POSITION = 10

  def __init__(self):
    self.position = 0

  def movel(self, steps):
    if self.postion + steps < Player.MAX_POSITION:
      self.position = self.position + steps
    else:
      self.position = Player.MAX_POSITION

  def draw(self):
    drawing = "-" * self.position + "|" + "-" * (Player.MAX_POSITION - self.position)
    print(drawing)

p = Player(); p.draw()
> |----------
p.move(4); p.draw()
> ----|------
p.move(5); p.draw()
> ---------|-
p.move(3); p.draw()
> ----------|

 

# Create Players p1 and p2
p1 = Player()
p2 = Player()

print("MAX_SPEED of p1 and p2 before assignment:")
# Print p1.MAX_SPEED and p2.MAX_SPEED
print(p1.MAX_SPEED)
print(p2.MAX_SPEED)

# Assign 7 to p1.MAX_SPEED
p1.MAX_SPEED = 7

print("MAX_SPEED of p1 and p2 after assignment:")
# Print p1.MAX_SPEED and p2.MAX_SPEED
print(p1.MAX_SPEED)
print(p2.MAX_SPEED)

print("MAX_SPEED of Player:")
# Print Player.MAX_SPEED
print(Player.MAX_SPEED)

> Output : 
MAX_SPEED of p1 and p2 before assignment:
3
3
MAX_SPEED of p1 and p2 after assignment:
7
3
MAX_SPEED of Player:
3

 

# Create Players p1 and p2
p1, p2 = Player(), Player()

print("MAX_SPEED of p1 and p2 before assignment:")
# Print p1.MAX_SPEED and p2.MAX_SPEED
print(p1.MAX_SPEED)
print(p2.MAX_SPEED)

# ---MODIFY THIS LINE--- 
Player.MAX_SPEED = 7

print("MAX_SPEED of p1 and p2 after assignment:")
# Print p1.MAX_SPEED and p2.MAX_SPEED
print(p1.MAX_SPEED)
print(p2.MAX_SPEED)

print("MAX_SPEED of Player:")
# Print Player.MAX_SPEED
print(Player.MAX_SPEED)

> OOutput 
MAX_SPEED of p1 and p2 before assignment:
3
3
MAX_SPEED of p1 and p2 after assignment:
7
7
MAX_SPEED of Player:
7

Exercise 2 / Alternative constructors

class BetterDate:    
    # Constructor
    def __init__(self, year, month, day):
      # Recall that Python allows multiple variable assignments in one line
      self.year, self.month, self.day = year, month, day
    
    # Define a class method from_str
    @classmethod
    def from_str(cls, datestr):
        # Split the string at "-" and convert each part to integer
        parts = datestr.split("-")
        year, month, day = int(parts[0]), int(parts[1]), int(parts[2])
        # Return the class instance
        return cls(year, month, day)
        
bd = BetterDate.from_str('2020-04-30')   
print(bd.year)
> 2020
print(bd.month)
> 4
print(bd.day)
> 30
# import datetime from datetime
from datetime import datetime

class BetterDate:
    def __init__(self, year, month, day):
      self.year, self.month, self.day = year, month, day
      
    @classmethod
    def from_str(cls, datestr):
        year, month, day = map(int, datestr.split("-"))
        return cls(year, month, day)
      
    # Define a class method from_datetime accepting a datetime object
    @classmethod
    def from_datetime(cls, datetime):
        year, month, day = datetime.year, datetime.month, datetime.day
        return cls(year, month, day)

# You should be able to run the code below with no errors: 
today = datetime.today()     
bd = BetterDate.from_datetime(today)   
print(bd.year)
print(bd.month)
print(bd.day)

 

Class Inheritance

Code reuse

  1. Someone has already written / done the coding  –>
    1. Modules are great for fixed functionality
    2. OOP is great for customizing functionality
  2. DRY – Don’t Repeat yourself
    1. for example elements of a website –> click and view functionality

Inheritance

New class functionality = Old class functionality + extra

Example

    • Bankaccount class has
      • attribute  : balance
      • method : withdraw()
    • Savingsaccount class
      • attribute : balance, interest_rate
      • method : withdraw(), compute_interest()
    • So both classe have a common attribute and method.
    • You can reuse the code for the attribute and the method that are in common by inheritance
    • If you have another class -> CheckingAccount
      • attribute: balance, limit
      • method : withdraw() and deposit()
      • you here too can reuse the balance and modified withdraw() – with a slight difference –

Implementing Class Inheritance

class MyChield(MyParent):
  # Do stuff here
  • MyParent – Class whose functionality is being extended/inherited
  • MyChild – class that will inherit the functionality and add more
class BankAccount:
  def __init__(self, balance):
    self.balance = balance
  def withdraw(self, amount):
    self.balance -= amount

# empty child class
class SavingsAccount(BankAccount):
  pass

Child class has all of the parent data

# Constructor inherited from Bankaccount
savings_acct = SavingsAccount(1000)
type(savings_acct)
> __main__.SavingsAccount

# Attribute inherited from BankAccount
savings_acct.balance
> 1000

# Method inherited from BankAccount
savings_acct.withdraw(300)

Inheritance : “is-a” relationship

  • A SavingsAccount is a BankAccount        ( with special features )
savings_acct = SavingsAccount(1000)
isinstance(savings_acct, SavingsAccount)
> True
isinstance(savings_acct, BankAccount)
> True

acct = BankAccount(500)
isinstance(acct, SavingsAccount)
> False
isinstance(acct,BankAccount)
> True

Exercise

class Employee:
  MIN_SALARY = 30000    

  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      
        
# Define a new class Manager inheriting from Employee
class Manager(Employee):
    def display(self):
        print("Manager " + self.name)

# Define a Manager object
mng = Manager("Debbie Lashko", 86500)

# Print mng's name
print(mng.name)
> Debbie Lashko
# Call mng.display()
mng.display()
> Manager Debbie Lashko

 

 

Customizing functionality via inheritance

  • for example a SavingsAccount is a special kind of BankAccount that has all the bankaccount functionality, but has additional properties like’interest rate’ and a method to ‘compute interest’
  • What we have so far :
    •  we could create SavingsAccount, but we didn’t have any additional functionality (empty class)
class BankAccount:
  def __init__(self, balance):
    self.balance = balance

  def withdraw(self, amount):
    self.balance -= amount

# empty cmass inherited from BankAccount
class SavingsAccount(BankAccount):
  pass

Customizing constructors

class SavingsAccount(BankAccount):
  # Constructor specific for SavingsAccount with additional parameter
  def __init__(self, balance, interest_rate):
    # Call the parent constructor using ClassName.__init__()
    BankAccount.__init__(self, balance)     # <-- self is SavingsAccount but also a BankAccount
    # Add more functionality
    self.interest_rate = interest_rate
  • Can run constructor of the parent class first by Parent.__init__(self, args ... ) 
  • You don’t have to call the parent constructor

Creating objects with a customized constructor

# Construct the object using the new constructor
acct = SavingsAccount(1000, 0.3)
acct.interest_rate
> 0.03
  • the new constructor when creating an instance of SavingsAccount will be called and a interest_rate will be initialized

Adding functionality

  • Add methods as usual
  • Can use the data from both the parent and the child class
class SavingsAccount(BankAccount):
  def __init__(self, balance, interest_rate):
    BankAccount.init__(self, balance)
    self.interest_rate = interest_rate

  def compute_interest(self, n_periods = 1):
    return self.balance * ( ( 1 + self.interest_rate) ** n_periods - 1 )
  • the functionality / method –> compute interest will only available in the subclass SavingsAccount

Customizing functinality

  • what in case of a customized version of a method in the main class ( eg. witfraw in CheckingsAccount ith an additional parameter )
class CheckingsAccount(BankAccount):
  def __init__(self, balance, limit):
    BankAccount.__init__(self, content)
    self.limit = limit
  def deposit(self, amount):
    self.balance += amount
  def withdraw(self, amount, fee = 0):
    if fee <= self.limit:
      BankAccount.withdraw(self, amount - fee)
    else:
      BankAccount.withdraw(self, amount - self.limit)
  • Can change the signature ( add parameters)
  • Use Parent.method(self, args … ) to call a mthod from parent class
check_acct = CheckingAccount(1000, 25)
check_acct.withdraw(200)
check_acct.withdraw(200, fee=15)

bank_acct = BankAccount(1000)
bank_acct.withdraw(200)
bank_acct.withdraw(200, fee=15)
> typeError : arg 'fee'

 

Exercise

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
  def give_raise(self, amount, bonus=0.05):
    new_amount = amount + amount * bonus
    Employee.give_raise(self, new_amount)

mngr = Manager("Ashta Dunbar", 78500)
mngr.give_raise(1000)
print(mngr.salary)
> 79500.0
mngr.give_raise(2000, bonus=0.03)
print(mngr.salary)
> 81610.0

 

# Original from_str method for reference:
#     @classmethod
#     def from_str(cls, datestr):
#         year, month, day = map(int, datestr.split("-"))
#         return cls(year, month, day)
      
  
# Define an EvenBetterDate class and customize from_str
class EvenBetterDate(BetterDate):
  @classmethod
  def from_str(cls, datestr, format="YYYY-MM-DD"):
    if format=="YYYY-MM-DD":
      year, month, day = map(int, datestr.split("-"))
      return cls(year, month, day)           
    elif format=="DD-MM-YYYY":
      day, month, year = map(int, datestr.split("-"))
      return cls(year, month, day)           

# This code should run with no errors
ebd_str = EvenBetterDate.from_str('02-12-2019', format='DD-MM-YYYY')
print(ebd_str.year)
> 2019
ebd_dt = EvenBetterDate.from_datetime(datetime.today())
print(ebd_dt.year)
> 2020

 

# 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()

ldf = LoggedDF({"col1": [1,2], "col2": [3,4]})
print(ldf.values)
[[1 3]
 [2 4]]
print(ldf.created_at)
> 2020-06-17 21:10:40.562454
# 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 with *args and **kwargs
    pd.DataFrame.to_csv(temp, *args, **kwargs)