Pillars of Object-Oriented Programming#
Author: Mike Wood
Learning objectives: By the end of this notebook, you should be able to list the four pillars of object-oriented programming and demonstrate them in Python.
Defining an Example Class#
In this notebook, we will look at a number of features of object oriented programming. To get us started, lat’s define an example class called School:
# define a class called School that has:
# - one attribute for name that is provided
# as an argument and assigned at initialization
# - one method called print_name that prints
# the School object's name
class School:
def __init__(self, name):
self.name = name
def print_name(self):
print(self.name)
Object-Oriented Programming in Python#
Object-oriented programming (OOP) is the idea that code should be centered around objects. Classes are used to generate objects that hold data. Further, objects contain methods that are used to access and modify the data that they hold.
OOP is a coding framework that is taught often in Computer Science courses. The extent to which OOP is implemented in coding frameworks depends on your development team. Some teams are more strict with OOP protocols than others. Versitile developers will be well-versed in OOP, so it’s a good idea to build a foundation for implementing code with OOP approach in Python.
There are 4 “pillars” of Object-Oriented Programming:
Pillar |
Description |
|---|---|
inheritance |
new classes are created from existing classes, providing a mechanism to reuse code and facilitate efficient development |
polymorphism |
methods can be used equivalently across similar classes, allowing for versitile and easily-maintained code |
abstraction |
classes are derived from simplified frameworks so that development is focused on underlying details rather than implementation |
encapsulation |
classes contain methods to access and manipulate data contained in object, helping to protect data from unintended manipulation |
Inheritance#
As we observed in the notebook on classes, subclasses can inherit attributes and methods from a given class. Begin by defining three subclasses of the School class:
# define a new class called Kindergarten which extends the School class
# fill in the body of the class with the keyword pass
class Kindergarten(School):
pass
# define a new class called Montessori which extends the School class
# fill in the body of the class with the keyword pass
class Montessori(School):
pass
# define a new class called University which extends the School class
# fill in the body of the class with the keyword pass
class University(School):
pass
Since each class is a subclass of the superclass School, each subclass has the print_name method from inherited from the superclass:
# create a list of school objects using each
# of the subclasses above
schools = [Kindergarten('Lowell Elementary School'),
Montessori('San Jose Montessori School'),
University('San Jose State University')]
# loop through each of the School objects
# and call the print_name method
for school in schools:
school.print_name()
Lowell Elementary School
San Jose Montessori School
San Jose State University
The inheritance property of classes allows the developer to focus on the development of subclasses without having to rework the details of the methods and attributes defined within the superclass. Further, if the superclass method needs to be updated, then it only needs to be done once rather than in all of the subclasses.
Polymorphism#
In the previous example, we saw that we could use the same print_method on objects created in each subclass because it was inherited from the superclass. We can go a step further and define functions in each of our subclasses that could be used in a similar way - this is the idea of polymorphism. Modify the subclasses above to include a new method called print_age_group:
# define a new class called Kindergarten
# which extends the School class
# fill in the body of the class with
# a method to print the following string:
# 'In Kindergarten, the age group is 3-5 years old.'
class Kindergarten(School):
def print_age_group(self):
print('In Kindergarten, the age group is 3-5 years old.')
# define a new class called Montessori
# which extends the School class
# fill in the body of the class with
# a method to print the following string:
# 'In Montessori schools, the age group is 5-18 years old.'
class Montessori(School):
def print_age_group(self):
print('In Montessori schools, the age group is 5-18 years old.')
# define a new class called University
# which extends the School class
# fill in the body of the class with
# a method to print the following string:
# 'The age group of university students is typically 17 or older.'
class University(School):
def print_age_group(self):
print('The age group of university students is typically 17 or older.')
We can create three objects from each class and use the function the same way, even though the function is uniquely defined in each subclass:
# recreate the list of school objects using each of the subclasses above
schools = [Kindergarten('Lowell Elementary School'),
Montessori('San Jose Montessori School'),
University('San Jose State University')]
# loop through each of the School objects and call the print_age_group method
for school in schools:
school.print_age_group()
In Kindergarten, the age group is 3-5 years old.
In Montessori schools, the age group is 5-18 years old.
The age group of university students is typically 17 or older.
It’s important to note that we are accessing a different method from each of our subclasses, but using it in the same way. This is similar to, but subtly different than, using the same method inherited in each subclass from the superclass.
Abstraction#
As a developer, you may want to ensure that subclasses generated from your class contain certain methods. This will ensure that, when the subclass is implemented alongside other existing subclasses, each will have the same function. The idea of an abstract class is that it provides a framework by which other classes that are defined from it are defined.
Python has a specific module for this purpose called abc a.k.a. the abstract base class. There are two key components of the abc module: a superclass called ABC and a decorator called abstract method. Both can be imported from abc as follows:
# import ABC and abstractmethod from the abc module
from abc import ABC, abstractmethod
Using abc, a class can then be described by a framework:
from abc import ABC, abstractmethod
# Edit the School class above to extend the ABC class
# write an abstract method called typical_class_size
# which must be implemnted in the subclasses of School
# provide the keyword pass for the body
# of the typical_class_size method
class School(ABC):
def __init__(self, name):
self.name = name
def print_name(self):
print(self.name)
@abstractmethod # Decorator to define an abstract method
def typical_class_size(self):
pass
# define a new class called Kindergarten
# which extends the School class
# fill in the body of the class with
# a typical_class_size method to print the following string:
# 'The typical class size in Kindergarten is 20 students.'
class Kindergarten(School):
def typical_class_size(self):
print('The typical class size in Kindergarten is 20 students.')
# define a new class called Montessori
# which extends the School class
# fill in the body of the class with
# a typical_class_size method to print the following string:
# 'The typical class size in Montessori schools is 50 students.'
class Montessori(School):
def typical_class_size(self):
print('The typical class size in Montessori schools is 50 students.')
# define a new class called University
# which extends the School class
# fill in the body of the class with a
# typical_class_size method to print the following string:
# 'The typical class size of university classes is 20-400 students.'
class University(School):
pass
# def typical_class_size(self):
# print('The typical class size of university classes is 20-400 students.')
# recreate the list of school objects using each of the subclasses above
schools = [Kindergarten('Lowell Elementary School'),
Montessori('San Jose Montessori School'),
University('San Jose State University')]
# loop through each of the School objects and call the print_age_group method
for school in schools:
school.typical_class_size()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[8], line 4
1 # recreate the list of school objects using each of the subclasses above
2 schools = [Kindergarten('Lowell Elementary School'),
3 Montessori('San Jose Montessori School'),
----> 4 University('San Jose State University')]
6 # loop through each of the School objects and call the print_age_group method
7 for school in schools:
TypeError: Can't instantiate abstract class University with abstract method typical_class_size
Questions:
What happens if you remove the abstractmethod decorator (without any other changes)?
What happens if you also remove the
typical_class_sizemethod from one of the subclasses (without any other changes)?
Key point: By extending the ABC class and using the abstractmethod decorator, the School class is designed to raise an exception if a subclass is not organized with the necessary components.
Encapsulation#
Finally, the idea of encapsulation is that an object should contain all of the data and methods required for its operation. For example, we can use getters and setters if we like:
# revisit the School class
class School:
def __init__(self, name):
self.name = name
# call the set_enrollment method below to set
# the enrollment instance variable
self.set_enrollment(0)
# define a method set_enrollment which takes in students
# as an argument and assigns them to an enrollment instance variable
def set_enrollment(self, students):
self.__enrollment = students
# define a method get_enrollment which returns the
# enrollment instance variable
def get_enrollment(self):
return(self.__enrollment)
Try the getter and setter below:
# define a School object for sjsu
sjsu = School('San Jose State University')
# try the getter method and print the number of students enrolled
students = sjsu.get_enrollment()
print(students)
# try the setter method
sjsu.set_enrollment(26000)
# try the getter method again and print the number of students enrolled
students = sjsu.get_enrollment()
print(students)
# try setting the enrollment variable manually
sjsu.__enrollment = 1000
# try accessing the enrollment variable manually
# and printint out its value
students = sjsu.get_enrollment()
print(students)
0
26000
26000
Questions:
What happens if we repeat the same thing but use one underscore in front of the variable name?
What happens if we repeat the same thing but use two underscores in front of the variable name?
A note about Python: hidden variables are not that common. Encapsulation usually means that you can use a method without worrying much about the internal machinery that makes it work. Typically, you just use the single underscore variables to signal to co-developers that the variables are designed to be used for internal use, rather than accessed by the use of the object (although they could if they wanted to).