Additional Object-Oriented Techniques
Introduction
This chapter builds on the Employee class from the previous chapter. First, you'll replace a Java-style to_string() method with Python's __str__() dunder method, which Python calls automatically whenever an object needs to be converted to a string (for example, when passed to print()). Then you'll add operator methods (__eq__, __lt__, and so on) so Employee objects can be compared directly with ==, <, >, etc.
Finally, you'll practise inheritance by defining a Programmer class that extends Employee, and you'll override methods in the subclass to specialise behaviour — a core OO technique for extending functionality without duplicating code.
Concepts
Dunder methods: str
class Employee:
def __init__(self, name, salary):
self._name = name
self._salary = salary
def __str__(self):
return f"{self._name}, salary: {self._salary}"
emp = Employee("Alice", 30000)
print(emp) # Python calls __str__() automatically
Operator overloading
class Employee:
def __init__(self, name, salary):
self._name = name
self._salary = salary
def __eq__(self, other):
return self._salary == other._salary
def __lt__(self, other):
return self._salary < other._salary
def __le__(self, other):
return self._salary <= other._salary
Inheritance
class Programmer(Employee):
def __init__(self, name, salary):
super().__init__(name, salary)
self._skills = {}
def add_skill(self, language, level):
self._skills[language] = level
def improve_skill(self, language, amount=1):
self._skills[language] = self._skills.get(language, 0) + amount
def get_skill(self, language):
return self._skills.get(language, "none")
Overriding superclass methods
class Programmer(Employee):
def pay_bonus(self, percent=1, min_salary=0, max_salary=float("inf")):
super().pay_bonus(percent, min_salary, max_salary)
self._salary += 100 * len(self._skills)
def __str__(self):
return f"{super().__str__()}, skills: {len(self._skills)}"
Examples
class Employee:
def __init__(self, name, salary):
self._name = name
self._salary = salary
def pay_bonus(self, percent=1, min_salary=0, max_salary=float("inf")):
if min_salary <= self._salary <= max_salary:
self._salary += self._salary * (percent / 100)
def __str__(self):
return f"{self._name}, salary: {self._salary:.2f}"
def __eq__(self, other):
return self._salary == other._salary
def __lt__(self, other):
return self._salary < other._salary
class Programmer(Employee):
def __init__(self, name, salary):
super().__init__(name, salary)
self._skills = {}
def add_skill(self, language, level):
self._skills[language] = level
def pay_bonus(self, percent=1, min_salary=0, max_salary=float("inf")):
super().pay_bonus(percent, min_salary, max_salary)
self._salary += 100 * len(self._skills)
def __str__(self):
return f"{super().__str__()}, skills: {len(self._skills)}"
dev = Programmer("Carol", 40000)
dev.add_skill("Python", 4)
dev.add_skill("SQL", 3)
dev.pay_bonus(5)
print(dev)
Challenges
1. Renaming to_string() to str()
- Rename
to_string()in yourEmployeeclass to__str__(). - Update the client code to call
print(emp)directly, instead ofprint(emp.to_string()). - Confirm the output is unchanged.
💡 Show Answer
class Employee:
def __init__(self, name, salary):
self._name = name
self._salary = salary
def __str__(self):
return f"{self._name}, salary: {self._salary}"
emp1 = Employee("Alice", 30000)
emp2 = Employee("Bob", 32000)
print(emp1)
print(emp2)
2. Operator methods
- Add methods to
Employeesupporting==,!=,<,>,<=, and>=, comparing objects by salary. - Write client code to compare two employees using all six operators.
💡 Show Answer
class Employee:
def __init__(self, name, salary):
self._name = name
self._salary = salary
def __eq__(self, other):
return self._salary == other._salary
def __ne__(self, other):
return self._salary != other._salary
def __lt__(self, other):
return self._salary < other._salary
def __gt__(self, other):
return self._salary > other._salary
def __le__(self, other):
return self._salary <= other._salary
def __ge__(self, other):
return self._salary >= other._salary
emp1 = Employee("Alice", 30000)
emp2 = Employee("Bob", 32000)
print(emp1 == emp2)
print(emp1 < emp2)
print(emp1 <= emp2)
3. Defining a Programmer class
- Define a
Programmerclass that inherits fromEmployee. - A programmer has a
skillsdictionary mapping a programming language to a skill level (1 to 5), initially empty. - Add methods to add a new skill, improve an existing skill level, and query the skill level for a given language (returning something sensible if the language isn't in the dictionary).
- Write client code that creates a
Programmerand exercises these methods.
💡 Show Answer
class Programmer(Employee):
def __init__(self, name, salary):
super().__init__(name, salary)
self._skills = {}
def add_skill(self, language, level):
self._skills[language] = level
def improve_skill(self, language, amount=1):
self._skills[language] = self._skills.get(language, 0) + amount
def get_skill(self, language):
return self._skills.get(language, "none")
dev = Programmer("Carol", 40000)
dev.add_skill("Python", 3)
dev.improve_skill("Python")
print(dev.get_skill("Python")) # 4
print(dev.get_skill("Rust")) # none
4. Overriding pay_bonus() and str()
- In the base
Employeeclass, rename__salaryto_salary(single underscore) so subclasses can access it. - Override
pay_bonus()inProgrammerso it applies the regular bonus and then adds an extra £100 for every language in the programmer's skillset. - Override
__str__()inProgrammerso it also shows the number of languages in the skillset.
💡 Show Answer
class Programmer(Employee):
def pay_bonus(self, percent=1, min_salary=0, max_salary=float("inf")):
super().pay_bonus(percent, min_salary, max_salary)
self._salary += 100 * len(self._skills)
def __str__(self):
return f"{super().__str__()}, skills: {len(self._skills)}"
dev = Programmer("Carol", 40000)
dev.add_skill("Python", 4)
dev.add_skill("Go", 2)
dev.pay_bonus(5)
print(dev)
5. Detailed skillset output and multiple inheritance
- Enhance
Programmer.__str__()so it lists every language and skill level in the skillset, rather than just the count. - Explore multiple inheritance in Python by thinking of a scenario where it's appropriate (e.g. a
TeamLeadclass that combinesProgrammerand aManagermixin), and implement it.
💡 Show Answer
class Programmer(Employee):
def __str__(self):
skills_str = ", ".join(f"{lang}: {level}" for lang, level in self._skills.items())
return f"{super().__str__()}, skills: [{skills_str}]"
class ManagerMixin:
def manage_team(self, team):
print(f"Managing team of {len(team)} people")
class TeamLead(Programmer, ManagerMixin):
pass
lead = TeamLead("Dana", 55000)
lead.add_skill("Python", 5)
lead.manage_team(["Alice", "Bob"])
print(lead)