by Dr Seán Carroll
Python 101 - The (very) Basics
17. Basic object-oriented programming concepts
In this lesson, we’ll introduce the basics of Object Oriented Programming in Python. So far we’ve been writing our code in what could be described as a functional style. We’ve done a good job at avoiding code duplication by using functions, but even this will tend to get messy as our programs get larger and larger. This is where Object Oriented Programming comes in.
Object-Oriented Programming (OOP) is a programming paradigm that represents data as objects that have associated attributes and methods (another term for functions that belong to a class). OOP allows us to create modular programs and reuse code with minimal code duplication. There is a lot of jargon here, so let’s break it down.
A function is a block of code that performs a specific task - we’ve seen these already. A class is a collection of attributes and methods that defines a blueprint or template for creating objects. And objects are simply instances of classes (remember the class is just the template).
Before we introduce too many more terms or concepts, let’s just define a class and create an object. We’ll start with a simple class that represents an animal. When you hear class, I just want you to think blueprint or template. Our animal class will have two attributes, a name and a sound. It will also have one method, make_sound
. Let’s see how this looks in code.
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
def make_sound(self):
return f'{self.name} makes the sound: {self.sound}'
We’ve defined the class using the class
keyword. Within the class definition, two things are happening;
first, we have the __init__
method, also knows as the constructor method. This is a special method that is called when we create an instance of the class. The __init__
method is where we define the attributes of the class. In this case, we have two attributes, name
and sound
. The self
keyword is used to refer to the instance of the class when it’s created. In other words, when we create an instance of this class (a new Animal object), the constructor is where we set the name and sound of the animal object to the values passed in when the object is created.
The second thing happening in the class definition is the make_sound
method definition. It’s a function that we can call on any instance of the class. In this case, the method simply prints the sound of the animal.
For example, we can create two different animal objects, a dog and a cat, and set their names and sounds to different values. The key thing to recognise is that we end up with two different animal objects that are based on the same class!
In a nutshell, this is the efficiency that OOP gives us. We can create as many animal objects as we want, and they will all have sound and name attributed with potentially different values.
# Create objects of the `Animal` class
dog = Animal("Dog", "Woof")
cat = Animal("Cat", "Meow")
Now we can call the make_sound()
method on each object and print the response:
print(dog.make_sound()) # Output: Dog makes the sound: Woof
print(cat.make_sound()) # Output: Cat makes the sound: Meow
Inheritance
OOP allows us to make use of a property called inheritance. Inheritance allows us to form new classes by inheriting properties from classes that have already been defined.
The newly formed classes are called derived classes, the classes that we derive from are called base classes. Important benefits of inheritance are code reuse and reduction of complexity of a program. The derived classes (descendants) override or extend the functionality of base classes (ancestors).
So, continuing our animal example, we can create a new class called Bird
that inherits from the Animal
class but also extends the functionality of the Animal
class by adding a new method called fly
:
# Inheritance example
class Bird(Animal):
def __init__(self, name, sound, can_fly):
super().__init__(name, sound)
self.can_fly = can_fly
def fly(self):
if self.can_fly:
return f'{self.name} can fly'
else:
return f'{self.name} cannot fly'
Notice that we’ve passed the Animal class as an argument to the Bird class. This is how we tell Python that the Bird class inherits from the Animal class.
The super()
method is used to call a method from the parent Animal
class, in this case, the __init__
method. This ensures that the initialisation code of the parent class is executed before the subclass’s own initialization code.
When we create a Bird
object, we need to initialise its attributes, including the ones inherited from the Animal
class. We use super().__init__(name, sound)
within the __init__
method of the Bird
class to call the __init__
method of the Animal
class, ensuring that the name and sound attributes are properly initialised.
We can now create a new object based on the Bird
:
# Create an object of the `Bird` class
parrot = Bird("Parrot", "Squawk", True)
Now we can call the inherited make_sound
method and the new fly
method on the new instance of the Bird
class, parrot
:
print(parrot.make_sound()) # Output: Parrot makes the sound: Squawk
print(parrot.fly()) # Output: Parrot can fly
Polymorphism
Polymorphism is a concept in object-oriented programming whereby objects of different classes can be treated as objects of a common superclass. This means that a single function or method can operate on multiple types of objects, as long as they implement the same method or have a common interface.
To demonstrate polymorphism, let's imagine a function called make_sound_twice
that takes an animal
object as an argument and calls the make_sound
method on it twice. We can pass in a dog
object and a parrot
object, and the function will work on both of them even though they are objects of a different class.
def make_sound_twice(animal):
return animal.make_sound() + " " + animal.make_sound()
print(make_sound_twice(dog)) # Output: Dog makes the sound: Woof Dog makes the sound: Woof
print(make_sound_twice(parrot)) # Output: Parrot makes the sound: Squawk Parrot makes the sound: Squawk
The make_sound_twice()
function works with both dog
and parrot
objects because it relies on the common make_sound()
method. This demonstrates polymorphism, as the function can work with different object types that share a common interface (method).
This allows us to easily add new classes with a make_sound()
method and use them with the existing make_sound_twice()
function without any modifications, making the code more flexible and easier to extend.
Encapsulation
Encapsulation is another benefit we get from using classes and is the process of restricting access to methods and attributes in order to prevent the data from being modified directly.
Encapsulation is achieved by declaring attributes and methods as private
. In Python, we denote private attributes and methods using the underscore _
symbol.
Let's leave our Animal and Bird classes to one side and declare a new class called BankAccount
:
class BankAccount:
def __init__(self, balance):
self.__balance = balance
def get_balance(self):
return self.__balance
def deposit(self, amount):
self.__balance += amount
def withdraw(self, amount):
if amount > self.__balance:
return "Insufficient funds"
else:
self.__balance -= amount
return f'Withdrawn: {amount}'
In this example, encapsulation is demonstrated by restricting direct access to the balance
attribute and providing methods to interact with it instead.
We can now instantiate a new account object of the BankAccount
class:
account = BankAccount(1000)
And execute the encapsulation methods to manipulate and retrieve the balance:
account.deposit(500)
print(account.get_balance()) # Output: 1500
print(account.withdraw(200)) # Output: Withdrawn: 200
print(account.get_balance()) # Output: 1300
There are a number of additional concepts that we could cover here and as you get more familiar with OOP, you'll come across them. But for now, this is a sufficient introduction to OOP in Python.