When I started writing the initial version of Pytubedata, the primary goal in my mind was to structure the code in a way that would be easy to scale (extending functionalities) and maintain in later stages. How did I do it? Classes.
I, for a long time, didn't understand the real purpose of classes. However much I read about them, they were hard to grasp until I started using them. In this article, I will share my understanding of Classes with examples from Pytubedata.
Define the requirements
Before writing any piece of code, we should first explicitly define its functionalities and its limitations. What it will do and what it will not do, both are important. This was what I wanted Pytubedata to do-
Make an API call to public endpoints of YouTube Data API and return the response.
To make those requests it needs 4 things- base URL, endpoint, API key, and parameters.
Class and its Members
A class, as I would define it, is a blueprint that primarily holds data and functions, also called members of the class, together.
The base URL and API key are used in all requests, so it makes sense to keep them together. Creating the first class api
that would hold this data together.
class api:
def __init__(self, key):
self.base_url = "https://youtube.com/api/v3"
self.key = key
The __init__
function here is a constructor. A class is a blueprint, and a blueprint is a general model to create something. You can create multiple entities using that blueprint. A constructor's job is to assign values to variables whenever a new entity is created using that class. (more on entities below)
Now left the endpoint and parameters. An API can have many endpoints and each endpoint will have different parameters. So let's create a separate class for each endpoint and store the endpoint and the parameters together.
def Channel:
def __init__(self):
self.endpoint = "/channels"
def get_channel(self, id, max_results):
params = {
"channelId": id,
"part": snippet,
"maxResults": max_results
}
return params
Since some parameters take the user's input so we can create a function to assign the value to those parameters.
Put it all together
Now we need to define a function that will make the API calls. Since the api
class holds the key
and base_url
for the API call, we will define a function in the same class. (Note: now the class is holding data and function that use that data together)
import requests
class api:
def __init__(self, key):
# data members
self.base_url = "https://youtube.com/api/v3"
self.key = key
# method to make API call
def request(self, channel_id, max_results):
requests.request(
method="GET",
url=self.base_url
params= # how do I put params here?
)
Having all the data structured now we are ready to make an API call. But wait, how are we gonna put all this data together? Remember entities from the constructor discussion?
Those entities are called objects. Objects are essentially copies of a class and we can make as many as you want of them. So when we want to make an API call to the channel endpoint we can just create the object of Channel
class.
def request(self, channel_id, max_results):
c = Channel(channel_id) # creating object of Channel Class
requests.request(
method="GET",
url=self.base_url
params=c.get_channel(
id=channel_id,
max_results=max_results
)
)
Now the main code
key = "123"
client = api(key=key)
client.request(channel_id="123", max_results=10)
There, we successfully made an API call that returns data about a channel given its id.
Encapsulation
Encapsulation is what makes a class special.
It extends the holding functionality of the class with the ability to restrict access of data members and functions to the outside world using access modifiers. A motivation for this concept; someone using this code can modify the value of key
or base_url
by accessing them using objects which can break the code.
key = "123"
client = api(key=key)
client.key = "867" # value of key changed
There exist 3 access modifiers:
Public
- Public is the default access modifier and, as the name suggests, anyone can access the public members of a class using the object
Protected
- Protected members can be accessed only within the class and its child classes (You will learn about these in Inheritance)
Private
- Private members cannot be accessed anywhere outside the class
To prevent malicious coders from breaking our code, in our case manipulating variables like endpoint
, base_url
, and api_key
we can make these variables Private.
In Python, there doesn't exist the concept of access modifiers. But there is a hack to mimic the same behavior using underscore (_).
You can add two underscores before a variable, which will tell Python that those members are private and not to be given access outside the class.
import requests
class api:
def __init__(self, key):
# data members
self.__base_url = "https://youtube.com/api/v3"
self.__key = key
# method to make API call
def request(self, channel_id, max_results):
requests.request(
method="GET",
url=self.base_url
params= # how do I put params here?
)
key = "123"
client = api(key=key)
client.__key = "abc"
Now this will give an error since __key
is a private variable and cannot be accessed outside the class.
One shortcoming of using private variables is that now the user won't even be able to peek at the values of the private variables, which can be an issue sometimes. For eg, here one might wanna look at the value of __key
and if it is assigned the right value. We can just define a Public function, that will return the value of __key
, within the class.
def get_key(self):
return self.__key
Now that you have understood encapsulation, I would like to redefine the class definition-
A class is a blueprint that primarily encapsulates data and functions, also called members of the class, together.
Let's summarize the concepts we have covered in this article-
Classes
Constructors
Objects
Encapsulation
This is the most basic functionality of classes. From here, a lot more has been added to classes but that's for another blog. Until then, check out getter
and setter
in Python.