Object Oriented Programming
Contents
Object Oriented Programming#
Object oriented programming is a way to organize your code around objects, or types, instead of functions or dictionaries.
It allows us to define our own types and give those types properties and behaviors.
Table of Contents
Introduction#
Lets think back to our old pypet program. We had something like this:
1cat = {
2 'name': "Flufosourus",
3 'weight': 7,
4 'is_hungry': True,
5 'pic': "(=^o.o^=)__",
6}
7
8fish = {
9 'name': "Scaley",
10 'weight': 0.5,
11 'is_hungry': False,
12 'pic': "<`)))><",
13}
14
15def feed(pet):
16 if pet["is_hungry"]:
17 print("Feeding: " + pet["name"])
18 pet["is_hungry"] = False
19 pet["weight"] = pet["weight"] + 1
20 else:
21 print(pet["name"] + "is not hungry, thanks anyway.")
22
23def main():
24 feed(cat)
25 feed(fish)
26
27if __name__ == "__main__":
28 main()
There are a lot of similarities between each of our pets, aren’t there? Each one
has a name
, a weight
, a pic
and an is_hungry
value.
In this lesson we’ll rewrite that using object oriented programming, staring by
creating an Animal
class.
Part 1: Simple Classes#
Here is the simplest class.
class Animal:
...
Now we can create a new animal instance like so:
cat = Animal()
type(cat)
__main__.Animal
We could assign properties to cat
after it has been instantiated.
cat.name = "Flufosourus"
cat.weight = 7
cat.is_hungry = False
cat.pic = "(=^o.o^=)__"
print(cat.name)
print(cat.pic)
Flufosourus
(=^o.o^=)__
Part 1.1 Exercise#
(Car class)
Create a
Car
class.Make a new
Car
object and assign it to the variablecar
.Add properties for
make
,model
,color
andyear
.Print those properties
make
,model
,color
andyear
.
Solution to Exercise 94 (Car class)
1class Car:
2 ...
3
4car = Car()
5car.make = "Honda"
6car.model = "Accord"
7car.year = 2010
8car.color = "Black"
9
10print(car.color, car.make, car.model, car.year)
Part 2: Constructors#
Usually when we create a class, we have an idea of what properties it is going to have. Instead of relying on the programmer to assign arbitrary properties, we often want the programmer to provide those values when the object is being created.
This is where a constructor comes in. A constructor is a dunder method
that is used to create new instances of that class like when you call
Animal()
.
To make a constructor we’ll add the __init__
method to the Animal
class.
Class methods always take at least one argument self
, which is a special
variable that refers to the object itself.
class Animal:
def __init__(self):
print("Here is a new animal.")
Now lets see what happens when we create a new Animal
object.
cat = Animal()
Here is a new animal.
To make this class more useful, lets have __init__
take the arguments name
,
pic
, weight
and is_hungry
. Then we’ll use self
to assign each of these
values to their respective properties on the object.
class Animal:
def __init__(self, name, pic, weight, is_hungry):
self.name = name
self.pic = pic
self.weight = weight
self.is_hungry = is_hungry
print(f"Here is your new animal: {self.name}.")
Now we can pass in arguments when we instantiate the object.
cat = Animal(
"Flufosourus",
"(=^o.o^=)__",
7,
True
)
Here is your new animal: Flufosourus.
And each of the properties that we assigned on self
will now be available
from cat
.
print(cat.name)
print(cat.pic)
Flufosourus
(=^o.o^=)__
Part 2.1 Exercise#
(Car Constructor)
Add an
__init__
method to your car class that takes the argumentsmake
,model
,year
, andcolor
.Remove the lines where you set the properties on
car
.Modify your code that creates the
car
object to send those values as arguments toCar
.
Solution to Exercise 95 (Car Constructor)
1class Car:
2 def __init__(self, make, model, year, color):
3 self.make = make
4 self.model = model
5 self.year = year
6 self.color = color
7
8car = Car(
9 "Honda",
10 "Accord",
11 2010,
12 "Black",
13)
14
15print(car.color, car.make, car.model, car.year)
Part 3: Default and Keyword Arguments#
Often you want to give an property a default value if the user does not
specify the value. You can do this in the method definition with
PARAM=DEFAULT_VALUE
.
Lets make is_hungry
False
by default.
class Animal:
def __init__(self, name, pic, weight, is_hungry=False):
self.name = name
self.pic = pic
self.weight = weight
self.is_hungry = is_hungry
Now when we create the cat
object, we can choose to leave off the is_hungry
argument, and it will get set to False
.
cat = Animal(
"Flufosourus",
"(=^o.o^=)__",
7,
)
print(cat.is_hungry)
False
If you have more than two arguments or if it isn’t obvious what the arguments are just by looking at it, it can make your code clearer to use keyword arguments.
To do this, just put NAME=VALUE
when calling the constructor.
cat = Animal(
name="Flufosourus",
pic="(=^o.o^=)__",
weight=7,
)
Part 3.1 Exercises#
(Car Keyword Arguments)
Modify where you create the car object to use keyword arguments.
Solution to Exercise 96 (Car Keyword Arguments)
1car = Car(
2 make="Honda",
3 model="Accord",
4 year=2010,
5 color="Black",
6)
(Car Default)
Add an
is_clean
argument to theCar
constructor and set it toFalse
by default.In the constructor, set the
is_clean
property onself
to the value of theis_clean
argument.After creating the
car
object, print the value ofcar.is_clean
.Make a second
Car
object namedtruck
with different values for themake
,model
and so on. PassTrue
foris_clean
.Print the properties for
truck
, includingis_clean
.
Solution to Exercise 97 (Car Default)
1class Car:
2 def __init__(self, make, model, year, color, is_clean=False):
3 self.make = make
4 self.model = model
5 self.year = year
6 self.color = color
7 self.is_clean = is_clean
8
9car = Car(
10 make="Honda",
11 model="Accord",
12 year=2010,
13 color="Black",
14)
15
16truck = Car(
17 make="Ford",
18 model="F-150",
19 year=2021,
20 color="Red",
21 is_clean=True,
22)
23
24print(car.is_clean)
25print(truck.color, truck.year, truck.make, truck.model, truck.is_clean)
Part 4: Methods#
A benefit to writing things in an object oriented way is that data and the behavior associated with it are all packaged together. That means that an objects methods already have access to its properties.
To demonstrate this, lets add a feed()
method to the Animal
class.
class Animal:
def __init__(self, name, pic, weight, is_hungry=False):
self.name = name
self.pic = pic
self.weight = weight
self.is_hungry = is_hungry
print(f"Here is your new animal: {self.name}.")
def feed(self):
if self.is_hungry:
print("Feeding: " + self.name)
self.is_hungry = False
self.weight = self.weight + 1
else:
print(self.name + "is not hungry, thanks anyway.")
cat = Animal(
"Flufosourus",
"(=^o.o^=)__",
7,
True
)
print(cat.name)
print(cat.pic)
Here is your new animal: Flufosourus.
Flufosourus
(=^o.o^=)__
Now we can call cat.feed()
.
cat.feed()
Feeding: Flufosourus
And we can see that cat.weight
and cat.is_hungry
have both been changed.
print(cat.weight)
print(cat.is_hungry)
8
False
Part 4.1 Exercise#
(Car Method)
Add a
wash
method to yourCar
class that:prints
washing the YEAR MAKE MODEL
changes
is_clean
toTrue
.
Call the
wash()
method oncar
andtruck
then print theis_clean
property of each.
Solution to Exercise 98 (Car Method)
1class Car:
2 def __init__(self, make, model, year, color, is_clean=False):
3 self.make = make
4 self.model = model
5 self.year = year
6 self.color = color
7 self.is_clean = is_clean
8
9 def wash(self):
10 print(f"Washing the {self.year} {self.make} {self.model}")
11 self.is_clean = True
12
13car = Car(
14 make="Honda",
15 model="Accord",
16 year=2010,
17 color="Black",
18)
19
20truck = Car(
21 make="Ford",
22 model="F-150",
23 year=2021,
24 color="Red",
25 is_clean=True,
26)
27
28car.wash()
29truck.wash()
30
31print(car.is_clean)
32print(truck.is_clean)
Part 5: Class Properties#
When you set properties inside the class via self.
, or outside of the class
using an object that has already been instantiated, these properties are called
instance properties, which means that they belong
to an individual object.
You can also set properties that belong to the class and are the same for all instances.
You can do this just like assigning any variable, except inside the class.
class Animal:
ears = 2
def __init__(self, name, pic, weight, is_hungry=False):
self.name = name
self.pic = pic
self.weight = weight
self.is_hungry = is_hungry
def feed(self):
if self.is_hungry:
print("Feeding: " + self.name)
self.is_hungry = False
self.weight = self.weight + 1
else:
print(self.name + "is not hungry, thanks anyway.")
You can then access it via dot notation on the class.
Animal.ears
2
As well as on every instance of that class.
cat = Animal(
"Flufosourus",
"(=^o.o^=)__",
7,
True
)
cat.ears
2
You can change the value on any particular instance, but the class value will remain the same. This can be handy for defaults values.
snake = Animal(
"Medusa",
r"_/\__/\_/--{ :>~",
2,
)
snake.ears = 0
print("Medusa's:", snake.ears)
print("Animal:", Animal.ears)
Medusa's: 0
Animal: 2
Part 5.1: Exercise#
(Class Properties)
Add the class property doors
to the Car
class and assign it the value of
4
. After creating your truck
object, set the value of its doors
property
to 2
. Print the value of Car.doors
, car.doors
and truck.doors
.
Solution to Exercise 99 (Class Properties)
1class Car:
2 doors = 4
3
4 def __init__(self, make, model, year, color, is_clean=False):
5 self.make = make
6 self.model = model
7 self.year = year
8 self.color = color
9 self.is_clean = is_clean
10
11 def wash(self):
12 print(f"Washing the {self.year} {self.make} {self.model}")
13 self.is_clean = True
14
15car = Car(
16 make="Honda",
17 model="Accord",
18 year=2010,
19 color="Black",
20)
21
22truck = Car(
23 make="Ford",
24 model="F-150",
25 year=2021,
26 color="Red",
27 is_clean=True,
28)
29
30truck.doors = 2
31
32print(Car.doors)
33print(car.doors)
34print(truck.doors)
Part 6: Gotchas with Mutable Types#
You have to be careful with mutable types when it comes to default arguments or class properties, as they can lead to unexpected behavior.
Let’s say we add a class attribute toys
to the Animal
class, and assign it
to an empty list.
class Animal:
ears = 2
toys = []
def __init__(self, name, pic, weight, is_hungry=False):
self.name = name
self.pic = pic
self.weight = weight
self.is_hungry = is_hungry
def feed(self):
if self.is_hungry:
print("Feeding: " + self.name)
self.is_hungry = False
self.weight = self.weight + 1
else:
print(self.name + "is not hungry, thanks anyway.")
Then we create a cat
object and add some toys
.
cat = Animal(
"Flufosourus",
"(=^o.o^=)__",
7,
)
cat.toys.append("catnip mouse")
cat.toys.append("cardboard box")
cat.toys.append("laser pointer")
print(cat.toys)
['catnip mouse', 'cardboard box', 'laser pointer']
What would toys
contain on a new snake
object?
You may be surprised to see that it has the same contents as the cat
object.
That is because all instances share the exact same object. Since a list is
mutable, (unlike an integer or string), any instance can make changes that will
apply to all instances of the same class.
snake = Animal(
"Medusa",
r"_/\__/\_/--{ :>~",
2,
)
print(snake.toys)
['catnip mouse', 'cardboard box', 'laser pointer']
The same behavior is seen when using a mutable object for a default value.
This is because the default value is created when the method or function is defined, not when an object is instantiated.
class Animal:
ears = 2
def __init__(self, name, pic, weight, is_hungry=False, toys=[]):
self.name = name
self.pic = pic
self.weight = weight
self.is_hungry = is_hungry
self.toys = toys
def feed(self):
if self.is_hungry:
print("Feeding: " + self.name)
self.is_hungry = False
self.weight = self.weight + 1
else:
print(self.name + "is not hungry, thanks anyway.")
As a result, all instances that use the default value share the exact same object.
cat = Animal(
"Flufosourus",
"(=^o.o^=)__",
7,
)
cat.toys.append("catnip mouse")
cat.toys.append("cardboard box")
cat.toys.append("laser pointer")
print(cat.toys)
['catnip mouse', 'cardboard box', 'laser pointer']
snake = Animal(
"Medusa",
r"_/\__/\_/--{ :>~",
2,
)
print(snake.toys)
['catnip mouse', 'cardboard box', 'laser pointer']
The moral of the story is, if you want a mutable property that is different for
each instance of a class, assign it in the __init__()
function.
class Animal:
ears = 2
def __init__(self, name, pic, weight, is_hungry=False, toys=None):
self.name = name
self.pic = pic
self.weight = weight
self.is_hungry = is_hungry
if not toys:
toys = []
self.toys = toys
def feed(self):
if self.is_hungry:
print("Feeding: " + self.name)
self.is_hungry = False
self.weight = self.weight + 1
else:
print(self.name + "is not hungry, thanks anyway.")
This will ensure the mutable property is created when the object is instantiated so it will be different for each instance.
cat = Animal(
"Flufosourus",
"(=^o.o^=)__",
7,
)
cat.toys.append("catnip mouse")
cat.toys.append("cardboard box")
cat.toys.append("laser pointer")
print(cat.toys)
['catnip mouse', 'cardboard box', 'laser pointer']
snake = Animal(
"Medusa",
r"_/\__/\_/--{ :>~",
2,
)
print(snake.toys)
[]
Part 6.1: Exercise#
(Mutable Gotchas)
Add the class property
features
to yourCar
class and assign it to an empty list. Append somefeatures
to yourcar
object. Printcar.features
andtruck.features
.Remove the
features
class property, and instead add it as an argument to__init__
with an empty list for a default value. Append somefeatures
to yourcar
object. Printcar.features
andtruck.features
.Change the
features
default toNone
. In your__init__
function, check iffeatures
is falsy. If it is, set it to an empty list. Append somefeatures
to yourcar
object. Printcar.features
andtruck.features
.