Object-Oriented Programming
Understanding Object-Oriented Programming is one of those things that sets beginner programmers apart from intermediates. The term itself sounds fancy and intimidating, but my hope is that after reading this article, you’ll never want to go back to functional programming.
Functional vs. Object-Oriented Programming
Functional programming is the kind of programming you’ve likely done up to this point. The code runs using only the abstraction of functions. In computer science, an abstraction is an important concept that you will encounter frequently. It involves removing the nitty-gritty details and focusing on big picture functionality. A function is an abstraction, since we only see the function name (without the definition) in the main script. A user can call the function without necessarily understanding how it was implemented.
There are other kinds of abstractions, one of which is called a class. A class is a collection of related variables (called attributes) and functions (called methods). Classes are useful for combining data and functionality in a process called encapsulation. For example, we can define a class called Person
. We can associate data with a person (name
, age
), as well as functionality (introduce_self()
). We can define this class as follows.
A class is just a user-defined blueprint (just like a function) — it doesn’t contain any user data until we call it and pass it input arguments.
The method defined on line 3 of our class definition, __init__(self, name, age)
, is the constructor method of our class. The constructor method is run when we instantiate a class, like this:
The constructor method is in charge of assigning input variables to class attributes that can be accessed in any method of the class (without passing them as input arguments to the method). This is done using self
. When a class is instantiated, the instance is called an object — self
is a way of referring to the object from within the class definition, even before it is instantiated. Class attributes can be called from within a class using self.<attribute>
, and class methods can be called from within a class using self.<method>
.
In this case, the variable jane
is an object. We can “tell jane
to do something” (have the object jane
run one of its methods), like so.
The output to the terminal will be the following.
Hello, my name is Jane. I am 37.
Note that we could have accomplished this task using functional programming. We could have defined a function introduce(name, age)
like this.
If we call the function introduce()
with input arguments “Jane”
and 37
, we get the exact output we had when we created a Person
object and called it’s introduce_self()
method. The difference is that the function introduce()
is not part of any larger abstraction — it exists alone and can be used by anyone, at anytime in a script. The introduce_self()
function exists only within the abstraction of the Person
class. It can only be called using dot-notation on an object (jane.introduce_self()
). More precisely, if we tried to run introduce_self()
on its own, we would get a NameError
telling us the function we are trying to call is not defined.
Benefits Of Object-Oriented Programming
Inheritance
Suppose we decide to create two other classes called Engineer
and Teacher
. We want these classes to have all of the same attributes and methods as the Person class plus more attributes and methods. Instead of including the attributes and methods that already exist in the Person
class, we can have the Engineer
and Teacher
classes extend from the Person
class, like this.
If we do this, then the Engineer
and Teacher
classes will automatically contain all attributes and methods from the Person
class (name
, age
, introduce_self()
), without us having to include that code in the Engineer
and Teacher
class definitions. That is, the following would be a valid line of code.
The output of this code would be:
Hello, my name is Sally. I am 27.
We call Person
the parent class, and Engineer
and Teacher
the child classes. We say that child classes inherit the attributes and methods of the parent class in a process called inheritance.
We can add additional methods to both Engineer
and Teacher
, and they will not be added to Person
. Methods added to Engineer
and Teacher
can have the same name or different names. If they have the same name, that is OK! This is because, in object oriented programming, methods are only defined within the scope of the class — outside of the class, those function names are undefined. For example, we can add a method called describe_profession()
to both the Engineer
and Teacher
class and implement it differently in each. This is a powerful concept in computer science called polymorphism.
When we create Engineer
and Teacher
objects and call describe_profession()
on each, we will we see different outputs, depending on the type of object (Engineer
or Teacher
).
I code software in C.
I teach High School Pre-Calculus.
Note that we do not need a constructor (the __init__()
method) in child classes. If an attribute or method is not found for a particular class, its parent class is searched.
It is possible for a child class method to override a parent class method. For example, we can redefine the method introduce_self()
in the Engineer
and Teacher
classes.
Now, when we create Engineer
and Teacher
objects and call introduce yourself on them, the function will no longer use introduce_self()
definition in the Person
class.
Hello, my name is Sally. I am 27. I am an Engineer.
Hello, my name is Jeff. I am 31. I am a Teacher.
If we want to add additional methods to the Engineer
or Teacher
classes, we can just add them as we would any method. However, if we want to add an additional attribute (let’s say, type
) to the Engineer
or Teacher
classes, we need to redefine the constructor(__init__()
). Up to this point, the child classes, Engineer
and Teacher
, have been using the constructor of their parent class (Person
) to instantiate the class and create an object. The constructor of Person
takes two input arguments: name
and age
. In our new constructor, we will include these input arguments and our new input argument (type
). Within the constructor, we need to ensure that all input arguments become attributes of the class. To do this, we write self.type = type
to define our new attribute — but to define the parent attributes, we can just run the parent constructor. We can access the parent of any child object using super()
(which is analogous to how we can use self
to access the object itself). That is, to run the constructor of the parent object, we can just run super().__init__(name, age)
, calling the constructor method of the parent object, and passing the input arguments to the child object on to the parent object. Note that in the following code, I have updated the introduce_self()
methods of the child classes to use our new attribute type
.
Hello, my name is Sally. I am 27. I am a Software Engineer.
Hello, my name is Jeff. I am 31. I am a High School Teacher.
Abstraction
Inheritance is a means of implementing abstraction. When you create a class, knowledge of its implementation is not necessarily needed for use. For example, any engineer can use our Person
class if we provide them with the call signature and the methods. Together, this information is called an Application Programming Interface (API). The API documentation for our Person
class might look something like this:
# The Person Class# Syntax
person = Person(name, age)# Attributes
name : str
age : int# Methods
introduce_self()
Any engineer could look at this documentation and use our Person
class, without ever seeing the class code itself. Companies like Google, Amazon, and Apple capitalize on this concept, allowing users to use their software, but not see how it is implemented.
Abstraction is also a useful concept in designing code. It allows you to focus on the big picture of your design, and helps with organization. For example, now that you are familiar with classes and inheritance, the next time you want to write code you should start by creating a software design. (Remember, software is just a set of instructions for your computer, so your Python code is software!) A software design for our Person
/Engineer
/Teacher
classes example (or Person
class hierarchy) might start out looking something like this.
We could expand on our design by adding attributes and methods to each class.
This visual representation of our Person
class hierarchy is called a Unified Modeling Language (UML) Class Digram. UML Class Diagrams are a useful software design technique, providing information about the relationship between classes in the hierarchy, in addition to the attributes and methods associated with each. This is helpful for understanding the design on a high-level, without worrying about the details of implementation (which is the definition of abstraction). You can make this kind of diagram before you even write any code.
Object-Oriented Programming Languages
An object-oriented programming language is a programming language built entirely on objects. That is, no variables or functions can exist unless they exist within an object. Python is unique in that, while it is considered an object-oriented programming language, it also supports functional programming. Many languages only (unless you include lambdas) support object-oriented programming. That is, you can’t write a function and then use that function in a script — the function needs to be contained within an object (making it a method). Some such languages include:
- Java
- C++
Final Thoughts
Ultimately, it is up to you whether or not you take an object-oriented or functional approach to your project. Making that decision really comes down to the size of your project. If it is a big undertaking, an object-oriented approach will help with organization and future development. If it is just a matter of making a few computations, a functional approach might make more sense. At least now, after reading this, you can hopefully make an informed decision.