class JazzRecord:
def __init__(self, player, album, year):
self.player = player
self.album = album
self.year = year
def as_string(self):
return "{} (by {}, {})".format(self.album, self.player, self.year)
record = JazzRecord("John Coltrane", "Giant Steps", 1957)
print(record.as_string())
import math
# we have to repeat some class declarations here
# so that our solution works in this notebook:
class Point2D:
def __init__(self, x0, y0):
self.x = x0 # set attribute x
self.y = y0 # set attribute y
def distance_to_origin(self):
"""method which computes length of Vector2D"""
return math.hypot(self.x, self.y)
def as_string(self):
return "<Point2D x={}, y={}>".format(self.x, self.y)
class Vector2D(Point2D):
def add(self, other):
""" is called when you execute self + other """
return Vector2D(self.x + other.x, self.y + other.y)
def length(self):
return self.distance_to_origin()
def as_string(self):
return "<Vector2D x={}, y={}>".format(self.x, self.y)
class FancyVector2D(Vector2D):
def __init__(self, x, y, name):
# call method of same name in base class
super().__init__(x, y)
self.name = name
def __len__(self):
# implements len(..) for a Point2D Object
return 2
def __str__(self):
# to string conversion, when str(.) is called
# or implicitly within print:
return "<FancyVector2D x={}, y={} name='{}'>".format(self.x, self.y, self.name)
def __add__(self, other):
# implements "self + other"
assert isinstance(other, FancyVector2D)
new_name = "{} + {}".format(self.name, other.name)
return FancyVector2D(self.x + other.x, self.y + other.y, new_name)
def __getitem__(self, index):
# square bracket access
if 0 <= index < 2:
return (self.x, self.y)[index]
raise IndexError()
def __eq__(self, other):
# implements "self == other"
if type(self) != type(other):
return False
return self.x == other.x and self.y == other.y
# SOLUTIONS FROM THE EXERCISE:
def scale(self, factor):
self.x *= factor
self.y *= factor
def __mul__(self, other):
assert isinstance(other, Vector2D) # can also be vector2d here !
return self.x * other.x + self.y * other.y
f = FancyVector2D(2, 1, "v1")
f.scale(2)
print(f)
v = Vector2D(1, 2)
print(f * v)
This solution implements a bit more than the exercise asked for:
class ComplexNumber(Vector2D):
def __str__(self):
if self.y == 0:
return str(self.x)
elif self.x == 0:
return "{} i".format(self.y)
elif self.y > 0:
return "{} + {} i".format(self.x, self.y)
else:
return "{} - {} i".format(self.x, -self.y)
def __mul__(self, other):
re = self.x * other.x - self.y * other.y
im = self.x * other.y + self.y * other.x
return ComplexNumber(re, im)
# EVERYTHING WHAT FOLLOWS NOW WAS NOT PART OF THE
# EXERCISE !
# used to print internals using repr() function
# we use the same implementation as for __str__:
__repr__ = __str__
def __neg__(self):
"implements unary minus"
return ComplexNumber(-self.x, -self.y)
def __abs__(self):
"called by abs(..) function"
return self.length()
def __iadd__(self, other):
"implementx self += other"
self.x += other.x
self.y += other.y
return self
def __float__(self):
"implements type conversion to float."
assert self.y == 0
return float(self.x) # could be int
def __hash__(self):
"""computes hash value, required if you want to use complex
numbers as keys of a dictionary, you als must implent
__eq__ to complete this"""
return hash((self.x, self.y)) # use builtin hash function
def __eq__(self, other):
if type(self) != type(other):
return False
return self.x == other.x and self.y == other.y
def __getattr__(self, name):
"is called when the user uses unknown attributes"
if name == "real":
return self.x
if name == "imag":
return self.y
raise AttributeError("instances of ComplexNumber have no attribute '{}'".format(name))
@property
def conjugate(self):
"""properties are computed attributes, so the client writes
c.conjugate and not c.conjugate()
we also could have used properites for implementin additional
real and imag attributes as we did it in __getattr__.
"""
return ComplexNumber(self.x, -self.y)
c1 = ComplexNumber(1, 1)
c2 = ComplexNumber(1, -1)
c3 = ComplexNumber(2, 0)
print("check to string conversion")
print("c1 is", c1)
print("c2 is", c2)
print("c3 is", c3)
print()
print("check multiplication")
print("c1 * c1 is", c1 * c1)
print("c1 * c2 is", c1 * c2)
print("c1 * c3 is", c1 * c3)
print()
print("-c1 is", -c1)
print("abs(c1) is", abs(c1))
print()
c1 += c2
print("c1 += c2 updated c1 to", c1)
print("c3 has no imag value, so conversion to float is", float(c3))
print()
print("dictionary with complex numbers as keys:")
dd = {c3: 1, c2: 2}
print(dd)
# this will also call __repr__ for the keys:
print("dd[c3] is", dd[c3])
print()
print("check if attribute lookup for real and imag works:")
print("c3.real is", c3.real)
print("c3.imag is", c3.imag)
print()
print("c1.conjugate is", c1.conjugate)
The main issue is that we have two not fully compatible inheritance hierarchies for the employees, the suggested position-based:
- Employee
- Manager
- Boss
- LowerManager
- Subordinate
- Staff
- Student
and the alternative indirectly implied supervision-based hierarchy:
- Employee
- UnsupervisedEmployee
- Boss
- SupervisedEmployee
- LowerManager
- Staff
- Student
This can be solved in at least two reasonable ways: by applying the mixin pattern, or a variant of so called Null object pattern.
Further technical issues to consider:
department.employees.append(employee)
shouldn't be
possible. What about the read access - do we need to copy the list for the same
reasons?Let's start w/ the Department, which is mostly independent of the Employees hierarchy:
class Department:
def __init__(self, name, employees=None):
self.name = name
# list is private; write via `.add_employee()`, read via `.get_employees()`
self._employees = [] if employees is None else employees
def add_employee(self, employee):
# Note: because Python is dynamically typed, lack of the Employee declaration
# won't trigger any errors until this bit of code actually runs
assert isinstance(employee, Employee)
if employee.is_supervised() and employee.supervisor not in self._employees:
raise ValueError(
"Can't add {employee}! His supervisor-{supervisor}-is not employed "
"in {department}.".format(
employee=employee.full_name(),
supervisor=employee.supervisor.full_name(),
department=self,
)
)
self._employees.append(employee)
# Employees are supposed to know to which department they belong
employee.department = self
def get_employees(self):
# Using generator pushed responsibility for list, if needed, on the caller
for employee in self._employees:
yield employee
def __str__(self):
return "{} department".format(self.name)
Let's test the department class w/o employees:
department = Department("Silly Walks")
print(department, "employees are:")
for employee in department.get_employees():
print("*", employee)
print()
Let's implement first Employees using a simplified variant of the Null object pattern, where as a NoSupervisor
instance we simply take None
value.
from abc import ABC, abstractmethod
class Employee(ABC):
def __init__(self, name, surname, supervisor):
self.name = name
self.surname = surname
self._validate_supervisor(supervisor)
self.supervisor = supervisor
self.department = None
def _validate_supervisor(self, supervisor):
if self.is_supervised() and not supervisor.is_manager():
raise ValueError(
"Supervisor of {employee} has to be a Manager instance, whereas "
"{supervisor} is not.".format(
employee=self.full_name(),
supervisor=supervisor,
)
)
def is_supervised(self):
# By default everyone is supervised, but this behaviour can be overridden
return True
def has_department(self):
return self.department is not None
@abstractmethod
def is_manager(self):
pass
@abstractmethod
def position(self):
pass
def full_name(self):
return "{surname}, {name}".format(name=self.name, surname=self.surname)
def __str__(self):
ret = self.full_name()
if self.has_department():
ret += ", {position} in {department}".format(
position=self.position(),
department=self.department,
)
if self.is_supervised():
ret += ", supervised by {supervisor}".format(
supervisor=self.supervisor.full_name()
)
return ret
class Manager(Employee):
def is_manager(self):
return True
class Boss(Manager):
def __init__(self, name, surname):
super().__init__(name, surname, None)
def is_supervised(self):
return False
def position(self):
return "boss"
class LowerManager(Manager):
def position(self):
return "lower management member"
class Subordinate(Employee):
def is_manager(self):
return False
class Staff(Subordinate):
def position(self):
return "staff member"
class Student(Subordinate):
def position(self):
return "student"
Let's run some tests w/ employees:
department = Department("Silly Walks")
boss = Boss("Brian", "Cohen")
print(boss)
department.add_employee(boss)
print(boss)
try:
LowerManager("Prophet", "Blood & Thunder")
except TypeError as e:
print("Expected ERROR:", e)
try:
LowerManager("Prophet", "Blood & Thunder", Student("Joe", "Doe", boss))
except ValueError as e:
print("Expected ERROR:", e)
manager_1 = LowerManager("Dirk", "Deadly", boss)
manager_2 = LowerManager("Prophet", "Blood & Thunder", manager_1)
subordinates = [
Staff("Big", "Nose", manager_1),
Staff("Guard", "Giggling", manager_1),
Staff("Helper", "Stoners", manager_1),
Student("Follower", "Shoe", manager_2),
Student("Prophet", "False", manager_2),
]
try:
department.add_employee(manager_2)
except ValueError as e:
print("Expected ERROR:", e)
department.add_employee(manager_1)
department.add_employee(manager_2)
for subordinate in subordinates:
department.add_employee(subordinate)
print(department, "employees are:")
for employee in department.get_employees():
print("*", employee)
print()
Let's compare w/ the Mixin pattern approach. It's arguably more secure but also technically more involved (but so would be the Null object pattern solution if we would like to implement it properly, using NoSupervisor
class, subclassing e.g. from the Manager
class).
from abc import ABC, abstractmethod
class Employee(ABC):
def __init__(self, name, surname):
self.name = name
self.surname = surname
self.department = None
@abstractmethod
def is_supervised(self):
pass
def has_department(self):
return self.department is not None
@abstractmethod
def is_manager(self):
pass
@abstractmethod
def position(self):
pass
def full_name(self):
return "{surname}, {name}".format(name=self.name, surname=self.surname)
def __str__(self):
ret = self.full_name()
if self.has_department():
ret += ", {position} in {department}".format(
position=self.position(),
department=self.department,
)
return ret
class Manager(Employee):
def is_manager(self):
return True
class SupervisedEmployeeMixin:
# This mixin does not inherit from Employee, but since it's only intended use,
# in in Employee hierarchy, we can "in advance" call here super().__init__
def __init__(self, name, surname, supervisor):
# unless you have a good reason, it's best to always initialise base class
# first - can use already its methods here, like `full_name`
super().__init__(name, surname)
self._validate_supervisor(supervisor)
self.supervisor = supervisor
def _validate_supervisor(self, supervisor):
if self.is_supervised() and not supervisor.is_manager():
raise ValueError(
"Supervisor of {employee} has to be a Manager instance, whereas "
"{supervisor} is not.".format(
employee=self.full_name(),
supervisor=supervisor,
)
)
def is_supervised(self):
return True
def __str__(self):
return "{self_}, supervised by {supervisor}".format(
self_=super().__str__(),
supervisor=self.supervisor.full_name(),
)
class UnsupervisedEmployeeMixin:
def is_supervised(self):
return False
class Boss(UnsupervisedEmployeeMixin, Manager):
# Note: a side-effect of keeping Mixin clean (no inheritance) is that it has to be
# declared here first, because otherwise it will be last in MRO, so on
# `__init__` call the ABC implementation will complain about lack of
# implementation of an abstract `is_supervised` method, before it's found
def position(self):
return "boss"
class LowerManager(SupervisedEmployeeMixin, Manager):
def position(self):
return "lower management member"
class Subordinate(SupervisedEmployeeMixin, Employee):
def is_manager(self):
return False
class Staff(Subordinate):
def position(self):
return "staff member"
class Student(Subordinate):
def position(self):
return "student"
We've only changed internal implementation, not the API of our object. We expect identical results for the same tests we run previously:
department = Department("Silly Walks")
boss = Boss("Brian", "Cohen")
print(boss)
department.add_employee(boss)
print(boss)
try:
LowerManager("Prophet", "Blood & Thunder")
except TypeError as e:
print("Expected ERROR:", e)
try:
LowerManager("Prophet", "Blood & Thunder", Student("Joe", "Doe", boss))
except ValueError as e:
print("Expected ERROR:", e)
manager_1 = LowerManager("Dirk", "Deadly", boss)
manager_2 = LowerManager("Prophet", "Blood & Thunder", manager_1)
subordinates = [
Staff("Big", "Nose", manager_1),
Staff("Guard", "Giggling", manager_1),
Staff("Helper", "Stoners", manager_1),
Student("Follower", "Shoe", manager_2),
Student("Prophet", "False", manager_2),
]
try:
department.add_employee(manager_2)
except ValueError as e:
print("Expected ERROR:", e)
department.add_employee(manager_1)
department.add_employee(manager_2)
for subordinate in subordinates:
department.add_employee(subordinate)
print(department, "employees are:")
for employee in department.get_employees():
print("*", employee)
print()