Pictured is a peer learning day occuring

 

Object-Oriented Programming in Python


In this article, I’ll be going over the following topics:

  • What is Object-Oriented Programming (a.k.a. OOP)?
  • How did OOP come to be?
  • Why would you use OOP?
  • The 4 main principles of OOP
  • Technical terms for OOP
  • Some basic examples in Python

In order to understand anything about Object-Oriented Programming, we need to first take a step back and see what led to its creation, Procedural Programming! Procedural Programming is everything you have learned so far. Our programs are divided into various functions, and we have data stored in lots of different variables. These functions operate on these variables, and thus we have a program that does something. This style of programming is very simple and straightforward.

Sometimes when programs grow, things start to get messy. More and more functions operate on more and more variables. You may find yourself copying lines of code from one function to another. You may make a change to one function that will break the performance of all the other functions. There is so much inner dependency between all the functions, it becomes problematic. This is called spaghetti code- code that is very hard to read and work with.

One of the ways to work around a problem like this is using Object-Oriented Programming!
With Object-Oriented Programming, we combine a group of related variables and functions into a unit. We call that unit — a 'class'. We refer to these variables as properties/attributes and to the functions as methods. A class is essentially just a description of some object. Before going into further detail about OOP, let’s think of some real-life 'objects'. A dog, let’s say. What attributes would a dog have? A few of them could be name, height and weight. What things can a dog do (a.k.a. methods)? Maybe walk, eat, sleep. Let’s define a simple Dog class in Python:

Class Dog:
   def  __init__(self, name, height, weight):
       self.name = name
       self.height = height
       self.weight = weight
   def eat(self):
       print("{} is eating".format(self.name))
   def walk(self):
       print("{} is walking".format(self.name))
   def sleep(self):
       print("{} is now asleep ZZzzZ".format(self.name))

This might seem very confusing. It’s okay! We’ll go over it step by step. The first line is self-explanatory; we define a class with the name 'Dog', which is how we will represent a Dog in our program.

The next line is the first 'method' of our class. A method is essentially just a function defined only within our class 'Dog'. This method is different from the others, and you can tell by its name. Python has built-in names for certain methods — like the one we use here. This method is where our object is initialized.

Instance methods have only one difference from ordinary functions, they must include an extra first parameter self in the list of arguments. This argument does not have to be specified outside of the class, Python knows to provide it itself (we will go through this later). Why would we need to include this parameter then? The variable refers to the object itself, thus the name self. This might be hard to fully grasp, and we’ll go more in depth soon. For now, we’ll remember that every single instance method of any class needs to include at least the self parameter.

Inside our __init__ method, we declare and assign values to public attributes (private attributes would look like: self.__name = name). These three attributes are specific for each instance/object of this class. The next three methods are the different things a dog can do. Let’s take a look at creating an object with this class.

max = Dog("max", 70, 40)
charlie = Dog("charlie", 50, 20)

Max and Charlie are instances of the class Dog. Each instance has three attributes, and their values are personal.

max.eat()
charlie.sleep()
max.walk()
Output
"max is eating"
"charlie is now asleep ZZzzZ"
"max is walking"

Here we are calling different methods for each of our Dog instances. This is a very basic description of OOP, and there’s lots more to be covered.

The 4 Principles Of Object-Oriented Programming

Encapsulation, Abstraction, Polymorphism, Inheritance. Also known as the 'four pillars of OOP'.

Encapsulation

In an Object-Oriented program, you can restrict access to methods and attributes of a certain class. This prevents accidental modification of data and unwanted changes and is known as encapsulation.

In Python, we can create private attributes and methods using __. These methods are not accessible outside of the class (partly true - read more here). We can test to see if a method is private by trying to access it:

Class PrivateMethod:
    def __init__(self):
        self.__attr = 1
    def __printhi(self):
        print("hi")
    Def printhello(self):
        print("hello")

a = PrivateMethod()
a.printhello()
a.__printhi()
a.__attr

Output:
Hello
AttributeError: 'PrivateMethod' object has no attribute '__printhi'
AttributeError: 'PrivateMethod' object has no attribute '__attr'

As we can see, it’s as if the method and attribute do not exist, thus we get an error. In order to access and modify private attributes, we use getter and setter methods (getter — get an attribute, setter — set an attribute):

Class Person:
    def __init__(self, name, age):
        self.name = name
        self.__age = age
    @property
    def age(self):
        print("Retrieving {}s age...".format(self.name))
        return self.__age
    @age.setter
    def age(self, value):
        if type(value) is not int:
            raise TypeError("Age must be a number")
        If value < 0:
            Raise ValueError("Age can’t be negative")
        print("Setting {}s to {}...".format(self.name, value))
        self.__age = age

These methods have something called decorators, which tells the interpreter that the following method is going to be special.

In this program, we use two decorators, property and <attribute>.setter.

The @property decorator defines a getter method, a class function that will return a private attribute. The method name must match the attribute name.

The @<attribute>.setter decorator defines a setter method, a flash function that will assign a new value to a private attribute. Also here the method name must match the attribute name, and we also need to accept a new parameter that will be the attributes new value, and check if the value is valid (in this case we raise an error if it isn’t).

We can test out this code as such:

adam = Person("adam", 48)
ethan = Person("ethan", 20)
adam.age
ethan.age
adam.age = 50
ethan.age = 21
Output
Retrieving adams age...
49
Retrieving ethans age…
50
Setting adams age to 50...
Setting ethans age to 21...

This is quite spectacular in my opinion. We don’t have to call the getter or setter methods, the interpreter knows exactly what to call in every situation! Also, notice how each instance contains its attribute with the same name, but different values. These two instance attributes have nothing to do with each other!

Abstraction

This principle is less physical (programmable) but more logical. Abstraction can be thought of as a natural extension of encapsulation.

At some point, our programs become very large, with various different classes that are using each other. In order to keep our program as logical and simple as possible — we apply abstraction to our classes. This means that each class should only expose its mechanisms that need to be public.

Let’s take a look at an object you’re probably familiar with: A coffee machine. All we need to do is press a button, and voilà! The coffee comes out. We aren’t interested in all the noises and processes happening in the back of the machine, we just want our coffee.

Same thing with a class! All the 'behind the scenes' mechanisms should be encapsulated and left private, thus creating less confusion when dealing with many classes at once.

Inheritance

A lot of the times our classes will be interacting with each other, and we might even want a specific class to be a subclass of some other class.


OOP Inheritance diagram

This is easy to understand with a simple example. Let’s say we’re building some kind of database to store every person at a university. Some of them are students, and some of them are employees, but everyone shares the class Person. Let’s create the class Person:

class Person:
    def __init__(self, name, lastname):
        self.name = name
        self.lastname = lastname
    def print_person(self):
        print("{} {}".format(self.name, self.lastname))

Every person has two attributes, a first and last name.
Every student and employee of the school will inherit the class Person, assuming they are all human 😃:

class Employee(Person):
    def __init__(self, name, lastname, employee_id):
        super().__init__(self, name, lastname)
        self.employee_id = employee_id
    def print_employee(self):
        print("My name is: {} {}, employee number: {}".format(self.name, self.lastname, self.employee_id))
    
    class Student(Person):
    def __init__(self, name, lastname, student_id):
        super().__init__(self, name, lastname)
        self.student_id = student_id
    def print_student(self):
        print("My name is: {} {}, student number: {}".format(self.name, self.lastname, self.student_id))

When defining a class that inherits another class, we need to include the super-class in the new class’s definition. In order to use the super-class within a sub-class, we could use the super() function with the corresponding parameters. We are able to call a parent class a few different ways in Python, and this is just one of them.

The line super().__init__(self, name, lastname) could also be written as: Person.__init__(self, name, lastname). In the second case, we call the initialization function directly from the class.

Polymorphism

Polymorphism describes situations in which something appears in different forms. In OOP, this means objects of a particular class can be used as if it belonged to a different class. This concept is most explainable with an example. Let’s take two classes: Dog and Cat, that both inherit a parent class Animal.

class Animal:
    def eat(self):
        print("{} is eating slowly".format(self.__class__.__name__))
    
class Dog(Animal):
    def eat(self):
        print("Dog is eating super fast")
    
class Cat(Animal):
    pass                         

Now we can instantiate these classes into two objects, and run the method.

jack = Dog()
jill = Cat()
jack.eat()
jill.eat()
Output
"Dog is eating super fast"
"Cat is eating slowly"

Something special is happening here - we are overwriting a method. In the parent class, we print using self.__class__.__name__. Each class has a list of built-in methods that we can use to retrieve different things. In this case, we use the method __class__.__name__ to retrieve the name of the current class.

Two different versions of the eat method exists; One in the Animal class, and one in the Dog class. When we have inheritance, the program will first search for the called method in its own class. If it isn’t available, it will search in its inherited classes. In this case, we have a different definition of eat in the Dog class, therefore that method is called and not the parents one.

A true example to show polymorphism would be iterating through different objects:

for animal in (jack, jill):
    animal.eat()

Output:
"Dog is eating very fast"
"Cat is eating slowly"

The for loop first iterates through the instantiation of the Dog class first, and the Cat class after. This is why we see the output of the Dogs methods before the Cats methods.

Accessing class methods this way (the polymorphic way) shows that Python doesn’t care which class the object belongs to, it just executes its methods (assuming they exist in the class).

Conclusion

In this article, we went over the basics of Object-Oriented Programming and the four main principles. I’m sure this seems like a lot of information that might be hard to fully grasp, but once you get the basics, the rest will come more easily.

Object-Oriented Programming has many advantages to Procedural Programming, and you might find yourself programming this way from now on.

I would recommend reading a bit more about OOP and trying to program some of the basics (great problems for beginners).

Written by:

Ethan Mayer, Cohort 8 (SF Campus)

Student at Holberton School

Resources