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 attributesself
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
- Someone has already written / done the coding –>
- Modules are great for fixed functionality
- OOP is great for customizing functionality
- DRY – Don’t Repeat yourself
- 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 –
- Bankaccount class has
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 aBankAccount
( 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)