Everything About Methods & Classes in Python

abhinaya rajaram
Python in Plain English
15 min readMay 12, 2023

--

In this article, I am going to explain the concept of Objects and Classes in Python.

Why Object Oriented Programming (OOP)? OOP allows programmers to create objects with methods and classes. All these techniques help developers organize the code base and is very helpful when you want to scale and repeat.

What are Classes?- To put it simply, you are just grouping data based on some logic. Classes are based on objects in the real world (like Customer or Product). Other times, classes are based on concepts in our system, like HTTPRequest or Owner.

What is a Method & how is it different from a Function? When a function is defined inside a class, it is called a method. It usually performs an action and is not very different from our traditional understanding of functions. Note that we are talking of instance methods here, more on other types of methods later.

What do methods do? → Methods help you return the object, use information about the object or sometimes change the object itself. For example ( sorting on a list, counting occurrences in a tuple etc). To put it simply, they perform some action.

Introduction

Here is the syntax and also some terms you need to be familiar with before we dive deeper.

To help you understand this better, imagine you are a customer care executive and you are assigned the task of sending emails out to customers on a daily basis. To do this efficiently, you might want to start creating templates that can be reused.

Classes can be thought of as templates, a kind of blueprint. Only after you have filled in the details of the template and sent that email out, you can consider the task as finished. Similarly, when you create a class, you have only created the template, you still have to initialize it.

Take the below example:

We have a Class called Diet_Plan that takes in the parameters

— What food item was allowed by the dietician

— The number of calories the patient is allowed to eat in a day.

Initialize

Within a class, you have the init method (see screenshot above) that is invoked every time you create an instance of the class. This is why when we call __init__, we initialize objects by saying things like self.foodnam_allowed = foodname_allowed.

As you scroll below, you will see a second method.

— In this second method, will take account of the calories the patient has taken, and the food items he has eaten so far. The results of the function will tell you how many calories he can continue taking throughout the day based on the maximum calories prescribed by the dietician.

Creation of the Object

To complete the process of object creation, you need to invoke it within brackets and enter all the inputs. Once you complete doing that, your object gets created.

class Diet_Plan(object):
"""A man goes to dietician and takes in the amount of calories he is allowed to eat. Diet Plan have the
following properties:

Attributes:
foodname_allowed: A string representing the food,
amount_by_dietician: A float tracking the current amount of calories that is left for him to eat during the day
"""
#thousand_ml_glass_milk_calories = 619

def __init__(self, *foodname_allowed, amount_by_dietician = 500):
self.foodname_allowed = foodname_allowed
self.amount_by_dietician = amount_by_dietician


def eat(self, total_cals_consumed, *foodeaten):

self.amount_by_dietician -= total_cals_consumed
return f"Calories that I can take for the day are {self.amount_by_dietician}"

Self

Through the self parameter, instance methods can freely access attributes and other methods on the same object. This gives them a lot of power when it comes to modifying an object’s state. For example, the variable “amount_by dietician” is being used within the “eat” method through the self keyword.

Implementation

  1. Calling a Method

You can call the class as if it were a function, but make sure you pass the parameters and make sure it is inside the bracket. In this case, we know that ‘Milk’ & ‘Eggs’ were prescribed by the dietician and a total of 2000 calories was permitted, so we enter all that.

Note: The finger_crossed object, known as an instance, is the realized version of the Diet_Plan class and it contains all the parameters inside it (2000 ,Milk , Eggs)

2. How to check if the instance is actually created

When you assign a class object to a variable “fingers_crossed” and check its type, you get results that say that this is an instance of the Diet_Plan class.

You should also be able to type fingers_crossed.foodname_allowed and you will see Milk and Eggs pop up.

3 . Calling an attribute — You can notice how method “eat” is always called within brackets and attributes “ foodname_allowed” is called without brackets “()”.

4. Methods use information from the object itself. You need to reference an instance of the class. If you take a deeper look at the code, you can see how I am calling ‘self.amount_by_dietician’ instead of ‘amount_by_dietician’ because the keyword “self” keyword is what is connecting the ‘amount_by_dietician’ to the object.

Concept of Class Object Attribute — Same for any Instance of Class

Use Case → A patient goes to the dietician and the dietician prescribes the maximum amount of calories the patient can have and the food items he is allowed to eat. We enter all those details as parameters. We now enter the food items the patient has consumed so far and based on that entry, we want to see how many more calories he can continue to have through the day. We also want to know how many of the calories he consumed can be counted as good calories.

class Diet_Plan(object):
"""A man goes to dietician and takes in the amount of calories he is allowed to eat. Diet Plan have the
following properties:

Attributes:
foodname_allowed: A string representing the food allowd by dietician,
amount_by_dietician: A float tracking the current amount of calories that is left for him to eat during the day
"""
thousand_ml_glass_milk_calories = 619

def __init__(self, amount_by_dietician, *foodname_allowed ):
self.foodname_allowed = foodname_allowed
self.amount_by_dietician = amount_by_dietician


def eat(self, total_cals_consumed, *foodeaten):
self.amount_by_dietician -= total_cals_consumed
for name in foodeaten:
if name == 'Milk':
print(f"Patient has had milk so atleast {self.thousand_ml_glass_milk_calories:n} calories were good")

return f"Calories that I can take for the day are {self.amount_by_dietician}"

If you are aware of any information that holds true for all instances of the class then you are better off adding that up right above the def keyword of the init method, so you do not have to keep repeating that information in the methods section below each time.

For example: A glass of whole milk usually contains 619 calories and this will be true for all instances, so let me add that right at the top to avoid repetition.

How to call Class Object Attribute that is common→You can call them in the same manner using the self keyword i.e. the self.thousand_ml_glass_milk_calories.

After having entered what the dietician allowed (2000 calories, Milk, Eggs) in the above section, we now specify what the patient consumed by calling the eat method.

Since the patient has consumed milk and looks like the quantity consumed was a little over one glass of milk (619 cals).

Result → We are interested in knowing how many more calories the patient can consume. In this case, the balance is 2000- 800 =1200 calories. We also see a message stating that 694 out of the 800 might have been the good calories that came from milk.

To summarize, we are using the class object attribute defined above in the instance methods below.

I can keep extending the code and use the thousand_ml_glass_milk_calories variable in other sections of my code directly. You can see how I have referenced this in the method called ‘details’.

class Diet_Plan(object):
"""A man goes to dietician and takes in the amount of calories he is allowed to eat. Diet Plan have the
following properties:

Attributes:
foodname_allowed: A string representing the food allowd by dietician,
amount_by_dietician: A float tracking the current amount of calories that is left for him to eat during the day
"""
thousand_ml_glass_milk_calories = 619

def __init__(self, amount_by_dietician, *foodname_allowed ):
self.foodname_allowed = foodname_allowed
self.amount_by_dietician = amount_by_dietician


def eat(self, total_cals_consumed, *foodeaten):
self.amount_by_dietician -= total_cals_consumed
self.foodeaten = foodeaten
self.total_cals_consumed = total_cals_consumed
for name in foodeaten:
if name == 'Milk':
print(f"Patient has had milk so atleast {self.thousand_ml_glass_milk_calories:n} calories were good")

return f"Calories that I can take for the day are {self.amount_by_dietician}"


def details(self):
for name in self.foodeaten:
if ('Milk' in self.foodeaten) and (len(self.foodeaten)) == 1 :
if self.total_cals_consumed > self.thousand_ml_glass_milk_calories:
return('Only Milk was consumed and it was more than one glass')
else:
return('Only Milk was consumed and it was less than one glass')
elif ('Milk' not in self.foodeaten):
return('Milk was not consumed')
elif (('Milk' in self.foodeaten) and (len(self.foodeaten)) >1 ):
return('Milk was not the only item consumed')
else:
pass

Inheritance- (Borrowing some behavior from Parent)

Inheritance is the process by which a “child” class derives the data and behavior of a “parent” class. It comes in handy when you want to reduce complexity & reuse code.

Just like a child inherits characteristics of the parent, the child class in the programming world gets access to the parent object's functionalities eventually paving the way for extensibility, standardization, and the creation of class libraries.

For example, you have a Base class of “Mammal,”. An“Elephant” would qualify as a Derived class.

Use Case for Inheritance — Repeating/ Borrowing from Parent

Use Case → I want to see the total number of rows in the dataframe every time and also wish to see the total number of rows after I have filtered out a few elements. And all this can be seen from the Child Class.

In the code below, I have created a data frame, a parent class that displays the total number of rows and total NULL values in the data frame.

Down below, I have created a child class where I have filtered for a particular value and again displayed the total number of rows and Null values in this smaller subset.

data = [['Ice Cream ', '20220918.xlsx'], ['Jelly', '20210918.xlsx'], ['Candy', '20200918.xlsx'], [None, '20210918.xlsx']]

# Create the pandas DataFrame
df = pd.DataFrame(data, columns=['Weakness Name', 'File_Name'])
class Parent:
def __init__(self):
print(f" This is the bigger df and consists of {df.shape[0]} rows.\nWhile it can be called from parent class, it is also displayed automatically by calling the child class,\nsimply by virtue of it being in init of parent class")

def na_calc(self):
isnone = df['Weakness Name'].isnull().sum()
print(f" In the larget dataset, we have {isnone} None values in Weakness_Name column")

def size(self):
print("Thr bigger dataset")


class Child(Parent):
options = ['20220918.xlsx']
# selecting rows based on condition
filtered_df_9_18 = df[df['File_Name'].isin(options)]
def __init__(self):
Parent.__init__(self)

#print(filtered_df)
print(f"Smaller dataset from File Name 20220918 consists of {self.filtered_df_9_18.shape[0]} rows/row")
def na_calc(self):

isnonee = self.filtered_df_9_18['Weakness Name'].isnull().sum()
print(f" The smaller dataset has {isnonee} None values")

def file(self):
print("This file contains filtered data from file dated 2022-9-18")

Here are some pointers to help you structure:

  1. Create a parent class just like any other class and include the things that you want to see every time you run the code. In my case, I want to see how many rows my dataset has and how many null values it contains every time.
  2. Create a child class and inside the bracket write the name of the parent class. See “class Child(Parent)” .
  3. Next, within the child class right after the init method use the “ Parent.__init__(self)”. This will ensure you are inheriting from the parent class.
  4. Now, in the child class, write some code within the init method. Know that calling the child class will invoke init methods of both child and parent.

Here is how you can interpret the results

  1. Calling init of child class invokes init related actions of both child and parent(i.e size of both datasets big and small, just what we wanted)→When you create a child object and invoke it, everything within the init of the parent class & child class both will be printed out. In my case the number of rows for the entire dataset as well the total rows for the subset both are printed out every time I call the child class. See first example in the screenshot above.
  2. Calling methods outside init within child class only invokes child class methods (i.e na_calc of child will only bring ‘NA’ is the smaller/filtered dataset)→Outside of init statements, when you call individual methods of child class, only those specific child class-related methods alone will be invoked. See the second example.
  3. Child object accesses methods outside init belonging to parent class d.size() but parent class cannot access methods of child class a.file(). See below.

Class Method vs. Static Method vs. Instance Method

In the above sections, we talked about instance methods & we know that the instance method performs a set of actions on the data/value provided by the instance variables.

Class Methods are called on the class. This means that these methods actually change the value of a class variable which would then impact the class variable across all class objects. We will see that in a moment. Just know that these methods can change the class state itself.

Last, but not the least, we have static methods. These do not have access to instance variables nor class attributes/class variables, they are useful when you need some type of conversion or you need to isolate tasks.

Here are some pointers:

  • We must explicitly define in the code whether it is a class method or a static method.
  • How to define instance methods →We know this already. We use self as the first parameter in the instance method when defining it.
  • How to define class method → We will use the @classmethod decorator or the classmethod() function to define the class method. Note that you need to use cls as the first parameter in the class method when defining it. The cls refers to the class.
  • How to use static method →We need to use the @staticmethod decorator or the staticmethod() function to define a static method.

Let me show you how class methods and static methods are used:

#Static methods, Class methods and Instance methods
class Zoo:
# class variables
zoo_name = 'Central Park Zoo'

def __init__(self, animal_name, animal_age):
self.animal_name = animal_name
self.animal_age = animal_age

# instance method
def display(self):
# access instance variables
print(f'The zoo has an adorable {self.animal_name} who is {self.animal_age} years old.')
# access class variables
print(f'This {self.animal_name} is in {self.zoo_name}.')

@classmethod
def change_zoo(cls, zoo_name):
# access class variable
print(f'Animal was located in {cls.zoo_name}.')
cls.zoo_name = zoo_name
print(f'Animal has been moved to {Zoo.zoo_name}')

@staticmethod
def task_isolation(task):
# can't access instance or class attributes
return ['Eating', 'Sleeping', 'Playing', task]

# create object
e = Zoo('Chimp', 12)
# call instance method
e.display()

# call class method
Zoo.change_zoo('San Diego Zoo')

Zoo.task_isolation('Fighting')

To summarize, you can see how I have used the class method to change the zoo name and how the static method is doing stuff that is independent of the parameters specified in the instance methods.

Polymorphism used with Inheritance

Concept → One person acts in different capacities at work, at home, as a parent, similarly, objects in Python can also take different forms.

The word polymorphism is taken from the Greek words poly (many) and morphism (forms).

Uses → Using polymorphism, you can perform the same action in many different ways.

How →A method processes objects differently depending on the class type or data type.

Polymorphism can be implemented using function overloading, method overriding, and operator overloading.

To understand Polymorphism better, think of the time when you were a teen. The teenage years are not that bad because you get the best of both worlds. While you may have enjoyed the perks of being taken care of, getting pocket money etc you also got to enjoy being able to make a few decisions of your own and being able to drive on your own. Similarly, we use polymorphism with Inheritance when you want to borrow some common features from the parent class (the color and style of the bag, all bags are bound to have) but also do your own things within the child class (getting the price of a specific brand).

class Bag:

def __init__(self, name, color, price):
self.name = name
self.color = color

def show(self):
print(f'This bag is of type {self.name} and is {self.color} in color' )

def use(self):
print('A bag is typically used to carry things')

def price(self):
print('Generally prices range from 20-10,000$')


# inherit from bag class
class Coach(Bag):
def use(self):
print('Coach can sometimes work as a status symbol also')

def price(self):
print('Coach Bags are typically expensive & start at about $100')


# Coach Object
c = Coach('Rachel', 'Red', 20)
c.show()
# calls methods from Coach class
c.use()
c.price()

# BagObject
v = Bag('Tote', 'white', 10)
v.show()
# calls method from the Bag class
v.use()
v.price()

Here are some pointers:

Parent Class → Bag and within this bag class we have color and style & also price

Child Class → Coach Class which is a famous bag brand. Within coahc, which we have the method called price that is expected to bring the price of coach bags only and has the power to override the price of a general bag.

What did we manage to pull here:

Using this concept of polymorphism we created a way wherein our code can be extended and easily maintained over time. I can go on adding new brands like “MK” and “Fossil” and adding price() within it. Price method of Bag class will be overidden and I will see the price of those individual brands. However show() method will not be overridden as long as you dont include show() as a method in those other classes.

Polymorphism without Inheritance

Sometimes it is not possible or prudent to put members under the same class but you may still have the requirement to do one operation for both members.

In the below example, I have a plant and a dog that need to be fed water and given exposure to the sun every day. A plant and a dog are both living beings not of the same kind so I do not want to create a class and put them both inside that common class.

Instead, I will create 2 different classes, create the same instance method names that will do the same task & invoke them both through a for- loop. I am not going to link both the classes or use the concept of inheritance here.

class Plant:
def feed_water(self):
print("I am a plant & I have had water")
def sun(self):
print("I am a plant & I have had sunlight exposure today")

class Dog:
def feed_water(self):
print("I am a dog & I have had water")

def sun(self):
print("I am a dog & I went out for a walk and has some sun")



p = Plant()
d = Dog()

# iterate objects of same type
for item in (p, d):
# call methods without checking class of object
item.feed_water()
item.sun()

What did we just do here:

We put two different objects( Dog, Plant) into a tuple( data type that can store all types of variable types and cannot add/ delete items) and iterate through it using the item variable.

Behind, the scenes, python will check object’s class type for you and execute the methods (sun , feed_water) present in its class.

Adding a small variation for you. Imagine you are out with your dog and you only need to keep your plant out in the sun and dont need to feed water. You also do not wish to keep on typing method names again and again. To tackle this, I am going to use the method (feed_water just once inside a function).

This function will take any object as a parameter and execute its method without checking its class type. Using this, we can call object actions using the same function instead of repeating method calls.

def cl(obj):
obj.sun()

cl(p)

Method Overloading

When you call the same method with different parameters, it is known as method overloading.You cannot create a function multiply (a, b. c) and just pass 2 numbers inside, you will get a TypeError.

You can however workaround the logic inside the code and use built-in functions like range to achieve this.

for i in range(3): print(i, end=', ')
print()
for i in range(100, 110): print(i, end=', ')
print()
for i in range(2, 14, 2): print(i, end=', ')

Conclusion

I hope you have been able to develop an intuitive understanding of object-oriented Python & going forward you can communicate the intent more clearly and maintain code easily.

--

--