Our first example is an example of composition. We create a Company that consists a list of Persons and an account balance.
Then after we structure our classes with desired behaviour we can use them quite freely.
Create a class called Person for storing the following information about a person:
Create a method say_hi that returns the string "Hi, I'm " + the person's name.
In [ ]:
class Person(object):
def __init__(self, name):
self.name = name
def say_hi(self):
return "Hi, I'm " + self.name
Run the following code to test that you have created the person correctly:
In [ ]:
persons = []
joe = Person("Joe")
jane = Person("Jane")
persons.append(joe)
persons.append(jane)
Now create a class Employee that inherits the class Person. In addition to a name, Employees have a title (string), salary (number) and an account_balance (number).
Override the say_hi method to say "Hi I'm " + name + " and i work as a " + title
In [ ]:
# the reference to Person on the line below means that the object inherits
# Employee
class Employee(Person):
def __init__(self,
name,
salary,
title="Software Specialist",
account_balance=0):
#this calls the constructor of Person class
super().__init__(name)
self.salary = salary
self.title = title
self.account_balance = account_balance
def say_hi(self):
return "Hi I'm " + self.name + " and I work as a " + self.title
Every employee is also a person.
In [ ]:
persons = []
joe = Person("Joe")
jane = Person("Jane")
persons.append(joe)
persons.append(jane)
emp1 = Employee("Jack", 3000)
emp2 = Employee("Jill", 3000)
persons.append(emp1)
persons.append(emp2)
for person in persons:
print(person.say_hi())
Now create a class called Company, which has a name
and a list of Employee objects called employees and an
account balance for the company.
Make a method payday(self) that will go through
the list of employees and deduct their salary from
the corporate account and add it to the employee
account. Before you start deducting money compute the sum of salaries and make sure it is higher than the account balance. If it is not, raise an instance of the NotEnoughMoneyError.
Make a method layoff(self) that will remove
one employe from the list of employees. If there are no more employees raise a NoMoreEmployeesException.
In [ ]:
class NotEnoughMoneyError(Exception):
pass
class NoMoreEmployeesError(Exception):
pass
class Company(object):
def __init__(self, title, employees = [], account_balance=0):
self.title = title
self.employees = employees
self.account_balance = account_balance
def _has_money_to_pay(self):
counter = 0
for employee in self.employees:
counter += employee.salary
return counter < self.account_balance
def payday(self):
if self._has_money_to_pay():
for employee in self.employees:
employee.account_balance += employee.salary
self.account_balance -= employee.salary
else:
raise NotEnoughMoneyError("not enough money to pay")
def layoff(self):
if self.employees:
self.employees.pop() # just pop the last one, logic wasn't specified
else:
raise NoMoreEmployeesException
Okay, you've worked this far just creating the model, let's put it to use.
Make a method smart_payday(company). The method should attempt to call the payday method of the company. If the call raises a NotEnoughMoneyException lay off a worker and then try again. Don't catch the NoMoreEmployeesException as that should be handled at a higher level.
In [ ]:
def smart_payday(company):
payment_succeeded = False
while not payment_succeeded:
try:
company.payday()
payment_succeeded = True
except NotEnoughMoneyError:
company.layoff()
In [ ]:
# a bit of test code
names_and_salaries = [
("Jane", 3000),
("Joe", 2000),
("Jill", 2000),
("Jack", 1500)
]
workers = [Employee(name, salary) \
for name, salary in names_and_salaries]
scs = Company("SCS", employees=workers, account_balance=12000)
smart_payday(scs)
print(scs.account_balance)
print(len(scs.employees))
smart_payday(scs)
print(scs.account_balance)
print(len(scs.employees))
print(scs.employees)
Observe how printing the employees list is not very informative? Adding a magic method called __repr__ will help with that.
Create a class called element for storing the following data
You can use the following as example data.
| Element | symbol | atomic number | molecular weight |
|---|---|---|---|
| Hydrogen | H | 1 | 1.01 |
| Iron | Fe | 26 | 55.85 |
| Silver | Ag | 47 | 107.87 |
Make sure to define a __repr__ so that the textual representation of your elements is human-readable.
In [ ]:
class Element(object):
def __init__(self, name, symbol, atomic_number, molecular_weight):
self.name = name
self.symbol = symbol,
self.atomic_number = atomic_number
self.molecular_weight = molecular_weight
def __repr__(self):
return "<Element " + self.name + ">"
Now create a new class that inherits the Element class, a SortableElement. It should implement the __lt__ and __eq__ and magic functions described here.
In [ ]:
class SortableElement(Element):
def __lt__(self, another):
return self.molecular_weight < another.molecular_weight
def __eq__(self, another):
return self.symbol == other.symbol
Make a list of SortableElements and sort it using list.sort() to try out your list.
In [ ]:
elements = [
SortableElement("Coal", "C", 14, 14.0),
SortableElement("Hydrogen", "H", 1, 1.008),
SortableElement("Helium", "He", 2, 2)
]
elements.sort()
print(elements)
elements.sort(reverse=True)
print(elements)
Create a class compound, that will consist of multiple Element objects. Ignore physical restrictions on forming compounds for now.
Remember that an element can be present multiple times in a compound the way there are two Hydrogens in each water molecule.
Implement a get_molecular_weight for the compound.
If you have time and energy, you can implement addition of new elements, combining of compounds.
In [ ]:
#User beware, the code was written as an example but has not been tested.
class Compound(object):
def __init__(self, name="Compound X"):
self.elements = []
self.element_counts = {}
def add_element(self, element):
if element not in self.elements:
self.elements.append(element)
if element not in self.element_counts:
self.element_counts[element.name] = 1
else:
self.element_counts[element.name] += 1
def get_molecular_weight(self):
weight = 0
for element in self.elements:
weight += self.element_counts[element.name]*element.molecular_weight
Consider the following method, it will raise errors randomly. This type of failure is pretty common for IO-related tasks.
In [ ]:
class RandomException(Exception):
pass
def do_wonky_stuff():
import random
if random.random() > 0.5:
raise RandomException("this exception happened randomly")
return
Wrap a call to do_wonky_stuff with a try-except clause.
In [ ]:
try:
do_wonky_stuff()
except RandomException as ex:
#just ignoring for now
pass
print("yay it worked")
OK, now let's go even deeper.
In [ ]:
class ReallyRandomException(Exception):
pass
def do_really_wonky_stuff():
import random
val = random.random()
if val > 0.75:
raise RandomException("this exception happened randomly")
elif val < 0.15:
raise ReallyRandomException("This exception is actually quite rare")
return
Wrap do_really_wonky_stuff in a try-except -clause with two excepts. In the rarer of the excepts print out something so you'll if it's your lucky day.
In real life you'd probably want to handle different errors in a different way, or at least log or inform the user of what caused the error.
In [ ]:
try:
do_really_wonky_stuff()
except RandomException:
pass #again just ignore
except ReallyRandomException:
print("It's your lucky day")
In [ ]: