Python Object Oriented Programming (OOP) - with examples
As you start your journey in Python programming, you might wonder, "If we already have functions in Python for bundling and reusing code, why do we even need Objects and Classes?" The answer lies in the added level of organization and abstraction that classes and objects provide, which is especially beneficial in larger, more complex programs.
Let's use an analogy, building houses. If functions are like individual tasks involved in building a house, such as laying bricks or installing windows, then a Class is like having a blueprint for the house, and Objects are the houses built from that blueprint. While each task, or function, is essential, without a blueprint (Class) to organize and coordinate these tasks, it would be very difficult to construct a cohesive and well-structured house (Object). By defining a class (blueprint), we can create as many objects (houses) as we want, all with the same basic structure but with their own individual attributes.
Overview
Think of Classes as the blueprint for a house. This blueprint defines all the characteristics a house can have, like the number of rooms, the colour of the walls, the type of flooring, and so on. Similarly, Classes in Python serve as a blueprint for creating Objects. They define properties (attributes) and behaviours (methods) that every instance of the class should have.
Objects are individual houses built using the blueprint. For instance, if 'House' is a Class, an Object might be 'myHouse' or 'yourHouse', each built using the 'House' blueprint but perhaps differing in the number of rooms or colour of walls. Likewise, in Python, an Object is an Instance of a Class, carrying its own set of attributes and methods defined by the Class.
Music Player Application
In this post, we'll be building a simple Music Player in Python. This MusicPlayer
Class will act as a basic model for a music player application. It will maintain a playlist of songs and will allow us to add and remove songs. We will build upon this foundation and explore the concepts of Classes, Objects, Methods, and attributes in Python's object-oriented programming. This hands-on approach will help us understand how these pieces work together to form a cohesive, functioning application.
In the next section, we'll dive into the creation of our MusicPlayer
class, and begin to explore the process of instantiation, which allows us to create individual music players, each with its own unique playlist. Let's get started!
Classes
In Python, a class is defined using the keyword class
, followed by the class name. Here's a simplified version of our MusicPlayer
class.
class MusicPlayer:
def __init__(self):
self.playlist = []
In the code above, MusicPlayer
is our class. The __init__
method in Python is a special method that's automatically called when you create a new instance of a Class. It's often referred to as a constructor because it's used to set up or initialize attributes for a new object.
When we define the __init__
method in a class, we typically use it to set the initial state of an instance. For example, in our MusicPlayer
class, we have an __init__
method that initializes the playlist
attribute as an empty list. (New music players come with an empty playlist, right?)
Objects and Instantiation
To create an Object (or an instance) from a Class, we call the Class using its name followed by parentheses.
player = MusicPlayer()
In this case, player
is an Instance or Object of the MusicPlayer
class. The term "Instance" refers to an individual object of a certain Class. Instantiation is the process of creating an Object from a Class. Each object created this way is an Instance of that Class, possessing its own set of attributes and methods as defined by the Class blueprint.
Within a Class, we can define functions which are typically referred to as methods. Here's how we add a method to our MusicPlayer
class.
class MusicPlayer:
def __init__(self):
self.playlist = []
def add_track(self, track):
self.playlist.append(track)
The add_track
method takes a track name as an argument and appends it to the music player's playlist. This allows us to dynamically add new tracks to any instance of the MusicPlayer
class.
Scope and Instance Variables
You may already be familiar with two types of scope in Python - Global and Local. Global scope refers to variables defined outside all functions, and these variables can be accessed from anywhere in the code. Conversely, local scope refers to variables that are defined within a function, and these variables are only accessible within that function.
However, object-oriented programming (OOP) in Python introduces a third level of scope, known as Instance Scope. An instance variable, also called an attribute, is a variable that belongs to one instance of a class. Each object has its own copy of the instance variable, so changes to the variable in one object have no effect on other objects of the same class.
In a class definition, instance variables are initiated with self
, like self.playlist
in our MusicPlayer
class. These variables are accessible within any method in the class, giving them object-level scope. For instance, self.playlist
could be accessed and modified within any method of our MusicPlayer
Class.
Methods in a Class can also have local variables, just like in a standalone function. These are temporary and exist only as long as the method is running. Unlike instance variables, they aren't prefixed with self
. Once the method exits, these variables go away.
Instance Variable Example
class MusicPlayer:
def __init__(self):
self.playlist = [] # Instance variable
def add_track(self, track):
self.playlist.append(track)
player = MusicPlayer() # Create a MusicPlayer instance
player.add_track("Track 1") # Call the add_track method
print(player.playlist) # Prints: ["Track 1"]
In this example, self.playlist
is an instance variable. It's defined in the __init__
method, and it's accessed and modified in the add_track
method. This variable is tied to the instance of the MusicPlayer
class, represented by self
.
When we create an instance of MusicPlayer
with player = MusicPlayer()
, player
has its own playlist
attribute. We can add tracks to player
's playlist by calling player.add_track("Track 1")
, which adds "Track 1" to the playlist.
The playlist
variable belongs to the player
object, and its value is preserved even after the add_track
method has been called. We can verify this by printing player.playlist
after the method call, which returns ["Track 1"].
This is the key feature of instance variables: they belong to an instance of the class, and their values are preserved for as long as the instance exists.
Local Variable Example
class MusicPlayer:
def __init__(self):
self.playlist = [] # Instance variable
def add_track(self, track):
track_to_add = track + " (added)" # Local variable
self.playlist.append(track_to_add)
player = MusicPlayer() # Create a MusicPlayer instance
player.add_track("Track 1") # Call the add_track method
print(player.playlist) # Prints: ["Track 1 (added)"]
In this version of the add_track
method, track_to_add
is a local variable. We create and use this variable within the add_track
method, and it goes out of scope (i.e., ceases to exist) when the method finishes running. If you try to print track_to_add
outside the method, you will get an error because it's not defined in that scope.
Functions vs Methods
You might be wondering about the terms 'functions' and 'methods' that have been used so far, here is how they differ from each other.
- Definition: A function is a piece of code that is called by its name and can be independent of a class, while a method is associated with an object and belongs to a class.
- Access to Data: A function operates on its input arguments and has access to the global scope, while a method has access to the instance and its associated attributes.
- Invocation: A function is called by its name independently, while a method is called on an instance of a class, such as
instance.method()
.
Keep in mind that these differences mainly pertain to the concept of OOP, and "function" and "method" can sometimes be used interchangeably in general coding conversations.
Build the Music Player
Now that we've established a solid understanding of the core principles of Python's object-oriented programming, let's put theory into practice. We're going to work with our MusicPlayer
class, a simplistic representation of a music player, allowing us to add and remove tracks, and control playback. This real-world example will help us visualize the concepts we've learned and see how they interact in a functional piece of code. Let's look at the entire code first and then we will break it down bit by bit.
class MusicPlayer:
def __init__(self):
self.playlist = []
self.isPlaying = False
self.currentTrack = None
def add_track(self, track):
self.playlist.append(track)
def remove_track(self, track):
if track in self.playlist:
self.playlist.remove(track)
def play(self):
if self.playlist:
self.currentTrack = self.playlist[0]
self.isPlaying = True
def next_track(self):
if self.playlist and self.currentTrack in self.playlist[:-1]:
currentIndex = self.playlist.index(self.currentTrack)
self.currentTrack = self.playlist[currentIndex + 1]
def stop(self):
self.isPlaying = False
self.currentTrack = None
class MusicPlayer:
def __init__(self):
self.playlist = []
self.isPlaying = False
self.currentTrack = None
Here we're defining a class named MusicPlayer
. The __init__
method is called when an instance of MusicPlayer
is created. It sets up the initial state of a MusicPlayer
object with three attributes: playlist
, isPlaying
, and currentTrack
. These are instance variables, each MusicPlayer
object will have its own set of these variables.
Next, we have a set of methods that define the behaviors of a MusicPlayer
instance.
def add_track(self, track):
self.playlist.append(track)
The add_track
method adds a track to the playlist. It accesses the instance's playlist attribute with self.playlist
and appends the new track to it.
Similarly, the remove_track
method removes a track from the playlist.
def remove_track(self, track):
if track in self.playlist:
self.playlist.remove(track)
The play
, next_track
, and stop
methods control the playback.
def play(self):
if self.playlist:
self.currentTrack = self.playlist[0]
self.isPlaying = True
The play
method starts the playback if there are any tracks in the playlist, sets the currentTrack
to the first track in the playlist, and changes isPlaying
to True.
The next_track
method switches the current track to the next one in the playlist.
def next_track(self):
if self.playlist and self.currentTrack in self.playlist[:-1]:
currentIndex = self.playlist.index(self.currentTrack)
self.currentTrack = self.playlist[currentIndex + 1]
self.currentTrack in self.playlist[:-1]
- This checks if the current track is in the list of all tracks except the last one. self.playlist[:-1]
is a slicing operation that returns all elements in the playlist except the last one. This check is necessary because if the current track is the last one in the playlist, there is no "next track" to move to.
The stop
method stops the playback.
def stop(self):
self.isPlaying = False
self.currentTrack = None
Let's create an instance of MusicPlayer
and use these methods.
player = MusicPlayer() # Create a MusicPlayer instance
player.add_track("Track 1") # Add "Track 1" to the playlist
player.add_track("Track 2") # Add "Track 2" to the playlist
player.play() # Start playing
player.next_track() # Switch to the next track
player.stop() # Stop playing
This example demonstrates the core concepts we've discussed: classes as blueprints for objects, instantiation of classes into objects, instance variables, and methods. Each MusicPlayer
object has its own state and behavior, and we can manipulate these through the methods defined in the class.
Creating Multiple Instances from the Same Class
One of the powerful features of Classes is that they allow us to create multiple objects, each with its own set of attributes. Let's illustrate this with our MusicPlayer
class.
# Creating two instances of MusicPlayer class
player1 = MusicPlayer()
player2 = MusicPlayer()
# Adding tracks to the players
player1.add_track("Track 1")
player2.add_track("Track A")
# Print the playlist of each player
print(player1.playlist) # Prints: ["Track 1"]
print(player2.playlist) # Prints: ["Track A"]
In the above code, player1
and player2
are distinct instances of the MusicPlayer
class. Even though they are created from the same class, they have separate sets of attributes. When we add a track to player1
, it doesn't affect the playlist of player2
and vice versa.
This ability to create multiple, independent instances from a class is a fundamental part of object-oriented programming and allows for great flexibility and reuse of code.
Passing Arguments to a Method
If you are familiar with functions, you might be wondering about the add_track
method in our MusicPlayer
class, which is defined with two parameters: self
and track
. However, when we call this method, we only pass in one argument. Here's how it works.
When we call a method on an instance of a class, Python automatically passes the instance as the first argument. This argument is conventionally named self
. So, when we call player1.add_track("Track 1")
, Python is actually calling add_track(player1, "Track 1")
behind the scenes.
The self
parameter allows the method to access or modify the attributes of the instance on which it's called. That's why we use self.playlist
in the method definition. It refers to the playlist of the specific MusicPlayer
instance that we're adding a track to.
In essence, self
is the bridge between the instance of the class and its class methods, allowing the methods to operate on the correct instance's attributes. So, even though it looks like we're passing only one argument when calling a method, Python is handling self
for us, ensuring the method works correctly on the instance it's called on.
Initialization Parameters
When we create objects in real-world applications, it's common that each object needs to start with a unique state. Initialization parameters are the perfect tool for this.
In our MusicPlayer
class, let's say we want each music player to be associated with a brand right when it's created. We can achieve this by modifying the __init__
method to accept a brand
parameter.
class MusicPlayer:
def __init__(self, brand):
self.brand = brand
self.playlist = []
self.isPlaying = False
self.currentTrack = None
def add_track(self, track):
self.playlist.append(track)
# ... rest of the code ...
Now, when we instantiate our MusicPlayer
class, we pass in the brand name.
apple_player = MusicPlayer("Apple")
sony_player = MusicPlayer("Sony")
Here, apple_player
and sony_player
are instances of the MusicPlayer
class, each associated with a different brand. The brand
is passed as an argument during instantiation and is used to set the brand
attribute in the __init__
method.
Conclusion
In conclusion, I hope that this journey through Python's object-oriented programming using the MusicPlayer
class has helped to clarify these concepts. We've covered a lot of ground, from understanding classes, objects, and instance variables, to exploring methods, initialization parameters, and the important self
keyword.