Skip to main content

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 your Employee class to __str__().
  • Update the client code to call print(emp) directly, instead of print(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 Employee supporting ==, !=, <, >, <=, 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 Programmer class that inherits from Employee.
  • A programmer has a skills dictionary 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 Programmer and 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 Employee class, rename __salary to _salary (single underscore) so subclasses can access it.
  • Override pay_bonus() in Programmer so it applies the regular bonus and then adds an extra £100 for every language in the programmer's skillset.
  • Override __str__() in Programmer so 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 TeamLead class that combines Programmer and a Manager mixin), 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)