Post

Mastering Classes and Objects in Python

Dive into Python's core object-oriented programming concepts with hands-on examples. Learn how classes and objects work, explore default methods, inheritance, and practical techniques to structure your code effectively.

Mastering Classes and Objects in Python

Classes and Objects form the foundation of Object-Oriented Programming (OOP). Python relies on this concept, and understanding it is essential for writing structured, scalable, and maintainable code.

Think of classes as blueprints that group related data and functions/methods together which can be used and accessed by others. This is what we use to gain complete flexibility when coding in Python. They contain methods that can work together using concepts like HOF.

Objects is a term given to an instance of a class being used. They allow for parameters inside the class to store values or even perform functions. Think of assigning a normal variable to a function, here you would assign a normal variable to a class which then becomes an Object.

To give you a deeper analogy on Classes and Objects, everything we code in on Python is an Object. Your normal list() collection data type is an Object that can store information and perform functions on that data, like using List Comprehension.

A helpful way to conceptualize this is to think of a class as a blueprint, and an object as a specific instance built from that blueprint.

Creating a Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Syntax
class ClassName:
    # The __init__ method is called when we create a new object
    # 'self' refers to the instance being created
    def __init__(self, param1, param2):
        self.param1 = param1  # Store values in the object
        self.param2 = param2

    # A method inside the class
    def function1(self):
        # Access the object's data using 'self.'
        print(f"Param1 is {self.param1}, Param2 is {self.param2}")

## Using this Class:

# Creating an object of ClassName
obj1 = ClassName("Hello", 42)

# Calling a method on the object
obj1.function1()

Output:

Param1 is Hello, Param2 is 42

Hint: When creating classes it’s recommended to use camel case for the naming convention on each instance of a class which you’ll see on most of the examples I provide.

The self function is a very important concept to understand as it helps knowing what it does to create code. When I take this through ChatGPT to explain, this is what it says:

All methods inside a class must include self as their first parameter. This is because self refers to the current instance of the class. The main attributes of an object are typically defined inside the __init__ method and stored on self. Other methods within the class can then access and use these attributes by referencing self.attribute_name. This makes self the mechanism that allows data to be shared across methods within the same object.

To use a more analogy-based approach to explain, self can be viewed as a parameter that stores data within an object using the correct class information. When we create an object, the values we pass in travel through the classes __init__ functions and get stored in the correct positional parameter variable. self is also used in all the methods inside the original class, as this is what allows the parameters and their values to travel between them also.

This is what allows multiple methods in a class to operate on the same underlying data—an essential feature of object-oriented design.

Here is an example of all of what I have mentioned:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person:
    def __init__(self, name, age): # Self parameter with other parameters
        self.name = name
        self.age = age

    def greet(self): # including self in all other functions
        print(f"Hello, my name is {self.name}, and I am {self.age} years old.") # Referring to each self attribute or parameter

    def have_birthday(self):
        self.age += 1
        print(f"Happy birthday! I am now {self.age}.")

p1 = Person("Alice", 25)
p1.greet()          # Hello, my name is Alice, and I am 25 years old.
p1.have_birthday()  # Happy birthday! I am now 26.
p1.greet()          # Hello, my name is Alice, and I am 26 years old.

Default Object Methods

Just as functions can define default parameters, classes can do the same through the __init__ method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person:
      def __init__(self, firstname='Sheikh', lastname='Hussain', age=250, country='UK', city='Manchester'):
          self.firstname = firstname
          self.lastname = lastname
          self.age = age
          self.country = country
          self.city = city

      def person_info(self):
        return f'{self.firstname} {self.lastname} is {self.age} years old. He lives in {self.city}, {self.country}.'

p1 = Person()
print(p1.person_info())
# Sheikh Hussain is 250 years old. He lives in Manchester, UK.

p2 = Person('John', 'Doe', 30, 'Nomanland', 'Noman city')
print(p2.person_info())
# John Doe is 30 years old. He lives in Noman city, Nomanland.

Taken from the course

Using Methods in Classes to Modify Values

This is a useful feature that allows us to manage and change information within an object we create. This logic illustrates how information can be stored and changed for each instance of an object.

In the example below, a method is used to append values to a list that belongs to the object itself:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Person:
    def __init__(self, firstname, lastname, age, skills):
        self.firstname = firstname
        self.lastname = lastname
        self.age = age
        self.skills = []

    def add_skill(self, skill):
        self.skills.append(skill)

    def person_info(self):
        return f'{self.firstname} {self.lastname} is {self.age} and has these skills:\n {self.skills}'

p1 = Person("Sheikh", "Hussain", 29, "HTML")
print(p1.person_info())
# Sheikh Hussain is 29 and has these skills:
# []

p1.add_skill("CSS")
print(p1.person_info())
# Sheikh Hussain is 29 and has these skills:
# ['CSS']

p1.add_skill("HTML")
print(p1.person_info())
# Sheikh Hussain is 29 and has these skills:
# ['CSS', 'HTML']

Think of the instance of the object you create as being a storage box with special features or functions that it can perform.

With each call of the object, you can retrieve different information that you have stored in it, and also have functions performed on the information.

Inheriting Class Values

The term inheritance in Classes refer to having one class and it’s methods transfer over to another class. The original class is regarded as parent and the subsidiaries as child.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Parent(): # Original Class
    def __init__(self, firstname, lastname, occupation):
        self.firstname = firstname
        self.lastname = lastname
        self.occupation = occupation

    def person_info(self):
        return f'{self.firstname} {self.lastname} and I am a {self.occupation}'

class Child(Parent): # Child class with 'Parent' as parameter
    pass

parent = Parent("Katrina", "Kaif", "Wife")
child = Child("Sheikh", "Hussain", "Son") # Uses the same parameters/variables as Parent class.

print(parent.person_info())
# Katrina Kaif and I am a Wife
print(child.person_info())
# Sheikh Hussain and I am a Son

Inheriting Parameters

We can also have all the parameters of one class transferred to a new class. This is done using the super() function. This allows the child class to reuse the parents’ initialised logic for itself and then have new parameters exclusive to itself too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Parent(): # initial class
    def __init__(self, full_name, occupation, gender): # initial parameters
        self.full_name = full_name.upper() # function in our parameter
        self.occupation = occupation
        self.gender = gender

    def person_info(self):
        gender = "He/Him" if self.gender == "Male" else "She/Her" # Creating a function for this variable
        return f"I'm {self.full_name} and I'm a {self.occupation} my pronoun is {gender}"
    # This call will return one type of object

class Child(Parent):
    def __init__(self, full_name, occupation, gender, school):
        super().__init__(full_name, occupation, gender) # Method used to initialise the same parameters while transferring them over
        self.school = school # Adding a new parameter for this iteration
    def person_info(self):
        parent_info = super().person_info()
        return f"{parent_info} and I attend {self.school}" # This class will return the same object with an addition to it.
p1 = Parent("harriet hilda", "Mother", "Female")
print(p1.person_info())
# I'm HARRIET HILDA and I'm a Mother my pronoun is She

p2 = Child("sheikh hussain", "Son", "Male", "Some School")
print(p2.person_info())
# I'm SHEIKH HUSSAIN and I'm a Son my pronoun is He and I attend Some School

Concluding Statement

After revisiting this topic, I would say that my understanding is a little better, but the application of these features will remain a little hazy until I’ve had some practical exercises with them.

I will most likely get AI to create some tasks for me to do, including the ones from the original course, to help me internalise this topic. This is what I like to do with most of the things I learn, as it gets me to learn at a steeper speed.

Programming logic emerges from refined creativity, which is developed through repetition and problem-solving. Repetition alone builds on familiarity but true intuition comes from knowing when and why to apply certain structures that I’ve learnt.

Footnote

This post is licensed under CC BY 4.0 by the author.