OOP is a style of programming that uses Objects to represent concepts in code
How do we represent the world in Python code?
If we want to represent a salesperson, we could represent them as a list of data
james = ["James", 32, 1000]
We can extract data about our salesperson James
name = james[0]
age = james[1]
sales_budget = james[3]
How does that make you feel?
😭
An object typically has attributes and behaviour
Attributes is something we have
Behaviour is something we do
class SalesPerson:
pass
james = SalesPerson()
james
is an instance of SalesPerson
SalesPerson
We can initialize the class with some data
class SalesPerson:
def __init__(self, name, age):
self.name = name
self.age = age
>>> james = SalesPerson(name="James", age=32)
>>> james.age
32
self
self
is the placeholder we useNow we can make as many salespeople as we want
>>> mike = SalesPerson(name="Mike", age=40)
>>> mike.age
40
We’re all consenting adults
– Guido van Rossum
>>> james.name = "Tony"
>>> james.name
"Tony"
__init__
We have been using instance attributes - let’s try a class attribute
Let’s set Alm Brand be the default company to work for
class SalesPerson:
company = "Alm Brand"
def __init__(self, name)
self.name = name
>>> james = SalesPerson("James")
>>> mike = SalesPerson("Mike")
>>> james.company
"Alm Brand"
>>> mike.company
"Alm Brand"
>>> james.company = "Tryg"
>>> mike.company
"Tryg"
We can add behaviour to our class - these usually act on attributes
class SalesPerson:
company = "Alm Brand"
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hi, I'm {self.name} from {self.company}")
>>> james = SalesPerson("James")
>>> james.greet()
"Hi, I'm James from Alm Brand"
We have added the greet
behaviour to our class - Our SalesPerson
now knows how to greet someone
Create an Organization class.
num_employees
attribute showing how many employees there arework
which prints out that num_employees
did some workclass Organization:
def __init__(self, num_employees):
self.num_employees = num_employees
def work(self):
print(f"{self.num_employees} did some work")
OOP has two powerful concepts that allow objects to interact with each other:
Compose objects together
Inherit from other objects
Objects can inherit from other objects and call its parents methods and attributes
Used to specialize an existing class - we usually call this subclassing
We can inherit from SalesPerson and create a customized version with more specific attributes and behaviour
class Phoner(SalesPerson):
def call(self, telephone_number):
...
class TiedAgent(SalesPerson):
def meet(self, address):
...
Inherited classes still have access to its parents methods
class Phoner(SalesPerson):
def call(self, telephone_number):
...
>>> mike = Phoner("Mike")
>>> mike.greet()
"Hi, I'm Mike"
Note we didn’t define a greet
method on our Phoner
class
We can also overwrite methods from the parent
class Phoner(SalesPerson):
def greet(self):
print(f"Hi, I'm {self.name} and I'm a phoner")
>>> mike = Phoner("Mike")
>>> mike.greet()
"Hi, I'm Mike and I'm a phoner"
Or extend methods
class Phoner(SalesPerson):
def __init__(self, name, phone_number):
super().__init__(name)
self.phone_number = phone_number
>>> jane = Phoner(name="Jane", phone_number=35477777)
>>> jane.phone_number
35477777
super
is used when we want to call some method from the parent - usually when we override a method from the parent class
Create two new classes inheriting from Organization:
CustomerServiceCenter
FranchiseOffice
They should:
accept a contact_method
parameter in their __init__
override the work
method to specify what work they did using the contact_method
parameter
class Organization:
def __init__(self,num_employees):
self.num_employees = num_employees
class CustomerServiceCenter(Organization):
def __init__(self, num_employees, contact_method):
super().__init__(num_employees)
self.contact_method = contact_method
def work(self):
print(f"{self.num_employees} contacted customers "
f"via {self.contact_method}")
class FranchiseOffice(Organization):
def __init__(self, num_employees, contact_method):
super().__init__(num_employees)
self.contact_method = contact_method
def work(self):
print(f"{self.num_employees} contacted customers "
f"via {self.contact_method}")
super()
calls a method from the parent__dunder__
methods__init__
__repr__
__add__
__repr__
What happens if we want to print our SalesPerson
?
class SalesPerson:
def __init__(self, name):
self.name = name
>>> james = SalesPerson("James")
>>> print(james)
<__main__.SalesPerson object at 0x7f479d47db50>
To make our SalesPerson presentable, we need to tell Python what to do when representing a Sales person
class SalesPerson:
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hi, I'm {self.name}")
def __repr__(self):
return f"<SalesPerson {self.name}>"
>>> james = SalesPerson("James")
>>> print(james)
"<SalesPerson James>"
Add a name
attribute to the Organization
class we made previously
Add a __repr__
to the Organization
class
class Organization:
def __init__(self, name, num_employees):
self.name = name
self.num_employees
def work(self):
print(f"{self.num_employees} did some work")
def __repr__(self):
return f"<{self.name}: {self.num_employees} employees>"
Everything in python is an object under the hood.
Ever wonder why we can do this?
>>> "anders bogsnes".title()
"Anders Bogsnes"
We can do silly things
class MyInt(int):
def add_one(self):
return self + 1
>>> my_int = MyInt(2)
>>> my_int.add_one()
3
We can say that an Organization is composed of salespeople
class Organization:
def __init__(self, sales_people):
self.sales_people = sale_people
def work():
print(f"{len(self.sales_people)} sales people "
"did some work")
>>> james = SalesPerson("James")
>>> mike = SalesPerson("Mike")
>>> org = Organization(sales_people=[james, mike])
>>> org.work()
"2 sales people did some work"
We can add a sales_budget
to each SalesPerson
class SalesPerson:
def __init__(self, name, sales_budget):
self.name = name
self.sales_budget = sales_budget
def greet(self):
print("Hello, my name is {self.name}")
Now our organization can easily calculate its salesbudget dynamically
class Organization:
...
def total_budget():
return sum(sales_person.sales_budget
for sales_person
in self.sales_people)
SalesPerson
class knows how to make an appropriate greeting for that instanceOrganization
class doesn’t need to know how that is implementedclass Organization:
...
def greet_all():
for sales_person in sales_people:
sales_person.greet()
Create an Organization
class composed of some number of SalesPerson
SalesPerson
should have a method work
which prints the person’s name and how they’re doing workwork
which delegates to SalesPerson
’s work
methodclass SalesPerson:
def __init__(self, name, contact_method)
self.name = name
self.contact_method
def work(self):
print(f"{self.name} spent the day "
f"{self.contact_method} customers")
>>> james = SalesPerson("James", "meeting")
>>> jane = SalesPerson("Jane", "calling")
class Organization:
def __init__(self, sales_people):
self.sales_people = sales_people
def work(self):
for sales_person in self.sales_people:
sales_person.work()
>>> org = Organization([james, jane])
>>> org.work()
"James spent the day meeting customers"
"Jane spent the day calling customers"
We could compose a Salesperson as well
class SalesPerson:
def __init__(self, name, sales_method):
self.name = name
self.sales_method = sales_method
def sell_to_customer(self, customer):
self.sales_method.sell(customer)
Now we can make a PhoneSalesMethod and a MeetingSalesMethod class and pass to our salesperon
class MeetingSalesMethod:
def sell(self, customer):
self.go_to_meeting(customer)
>>> tied_agent = SalesPerson(name="Mike", sales_method=MeetingSalesMethod())
We could also define a sales_function
class SalesPerson:
def __init__(self, name, sales_method):
self.name = name
self.sales_method = sales_method
def sell_to_customer(self, customer):
self.sales_method(customer)
def phone_sales(customer):
call(customer)
>>> tied_agent = SalesPerson("Mike", sales_method=phone_sales)
We are composing functionality together to define the behaviour of our SalesPerson
We see this pattern in Python all over
If I want to sort a list by a given value
to_sort = [(99, "a"), (98, "b"), (1, "z")]
# I want to sort by the second value of the tuple
>>> sorted(to_sort, key=lambda x: x[0])
[(1, 'z'), (98, 'b'), (99, 'a')]
If I want to support dumping numpy arrays to JSON
import json
import numpy as np
class CustomEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, np.int64):
return int(o)
return super().default(o)
>>> json.dumps(np.int64(20))
...
TypeError: Object of type int64 is not JSON serializable
>>> json.dumps(np.int64(20), cls=CustomEncoder)
'20'
work
method to Organization
Access calculations as if it was an attribute
Let’s define a Person
class with a first name and last name
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
We can now add a property to our person to get their full_name
class Person:
...
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
We now have access to an attribute which does a calculation on the fly
>>> anders = Person(first_name="Anders", last_name="Bogsnes")
>>> print(anders.full_name) # No parentheses
"Anders Bogsnes"
Change the implementation of Organization to use a property for num_employees
class Organization:
def __init__(self, sales_people):
self.sales_people = sales_people
class Organization:
def __init__(self, sales_people):
self.sales_people = sales_people
@property
def num_employees(self):
return len(self.sales_people)
Can also use a setter
if we need to modify or validate data when setting an attribute.
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
@full_name.setter
def full_name(self, value):
self.first_name, self.last_name = value.split(" ")
We can now change full_name
and update first_name
and last_name
are updated automatically
>>> anders = Person("Anders", "Bogsnes")
>>> anders.full_name
"Anders Bogsnes"
>>> anders.full_name = "Thomas Bogsnes"
>>> anders.first_name
"Thomas"
Most often used as an alternate constructor for a class.
What if the data for our sales people was in a JSON file?
{
"name": "John",
"sales_budget": 2000
}
We could read the file first and then construct a SalesPerson
instance
import json
import pathlib
data = json.loads(pathlib.Path("john.json").read_text())
john = SalesPerson(name=data["name"],
sales_budget=data["sales_budget"])
This works, but what if we need to add a new parameter, contact_method
?
We would need to go through all our code and update every location we create a SalesPerson
😰
A better way is to add a classmethod
constructor
class SalesPerson:
...
@classmethod
def from_json(cls, json_file):
data = json.loads(pathlib.Path(json_file).read_text())
return cls(name=data["name"],
age=data["age"])
We can then use it to create a SalesPerson
from our json file
>>> john = SalesPerson.from_json("john.json")
>>> john.name
"John"
Now if we want to add our contact_method
attribute, we just need to add it to the constructor!
😄
Modify the Organization class to have an alternate constructor which accepts a JSON file containing a list of sales people
[
{"name": "John", "sales_budget": 2000},
{"name": "Mike", "sales_budget": 1000}
]
from_json
which will construct a list of SalesPeople and create an instance of Organization with that listimport json
import pathlib
class Organization:
def __init__(self, sales_people):
self.sales_people = sales_people
@classmethod
def from_json(cls, file_path):
data = json.loads(pathlib.Path(file_path).read_text())
return cls(sales_people=[
SalesPerson(name=datum["name"],
sales_budget=["sales_budget"])
for datum in data])
self
class Phoner:
...
@staticmethod
def lookup_phonenumber(phone_number):
# Lookup who the phone number belongs to
print(f"Phone number {phone_number} belongs to John")
>>> Phoner.lookup_phonenumber(35477777)
"Phone number 35477777 belongs to John"
>>> phoner = Phoner("Joe")
>>> phoner.lookup_phonenumber(35477777)
"Phone number 35477777 belongs to John"
It could easily be a function, but it’s grouped together with the Phoner, since it’s often used in that context
We need all SalesPeople
to have a work
method so our Organization
can call it
work
is an interface - a contract of sorts
Any class that inherits from our AbstractSalesPerson will not work unless it implements a work
method
import abc
class AbstractSalesPerson(abc.ABC):
@abc.abstractmethod
def work()
pass
class LazySalesPerson(AbstractSalesPerson):
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hi, I'm {self.name}")
# Oops - I don't know how to work!
>>> mike = LazySalesPerson("Mike")
"TypeError: Can't instantiate abstract class LazySalesPerson with abstract methods work"
Let’s fix our LazySalesPerson
class LazySalesPerson(AbstractSalesPerson):
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hi, I'm {self.name}")
def work(self):
print("I know how to work")
>>> mike = LazySalesPerson("Mike")
>>> mike.work()
"I know how to work"
We want to simulate different strategies for allocating leads to Tied Agents or to Customer Service representatives.
At the end of the month, we want to get a summary for that simulation:
We want to set some parameters for the simulation:
Organization
to contain the sales peopleTiedAgent
class and a CustomerServiceRep
class, both inheriting from a SalesPerson
classLead
class