Object-Oriented Programming II

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of “objects.” For example, consider a remote-controlled toy car: the car itself is the object, and the remote control is the device that controls it. You can move the RC car forward, backward, left, and right using the remote, illustrating the basic concept of OOP. Python supports Object-Oriented Programming through features like functions and classes.

Class

A class is a prototype, blueprint, or design that defines the variables and methods for specific objects. It serves to encapsulate the content of the program that will be executed, containing attributes/data types and methods to carry out a program. In Python, a class is defined using the keyword class, followed by the name of the class, like this: class class_name. Calling a class is similar to calling a function/method in a program by invoking the class name along with its parameters. Typically, a class contains many methods/functions that represent the properties of that class.

Classes can take various forms in different programming languages, such as abstract classes, data classes, and more. A class can also relate to other classes in a relationship known as inheritance, where a Parent Class (Base Class) has a Child Class (Derived Class) that inherits the same properties and variables.

For example, consider a car class: the car has attributes like engine, wheels, battery, etc. The car is a class, while the engine, wheels, battery, etc., are functionalities.

Important Aspects of Declaring a Class:

  • Initialization Function
  • Self Parameter

Initialization Function

This is a mandatory function that must be initialized when creating and declaring a class. It is used to add arguments and store parameters within the class.

Self Parameter

This is a variable that can only be used within the class declaration.

Simple Class Declaration Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Employee:
"Common base class for all employees"
empCount = 0

def __init__(self, name="Employee", salary=5000):
self.name = name
self.salary = salary
Employee.empCount += 1

def displayCount(self):
print("Total Employees: %d" % Employee.empCount)

def printEmployee(self):
print("Name:", self.name, "\nSalary:", self.salary)

employee1 = Employee("Azhar", 5000)

employee1.printEmployee()
employee1.displayCount()

employee2 = Employee("Gerald", 4000)

employee2.printEmployee()
employee2.displayCount()

Getter Method

This method retrieves data from the class. It is typically used when there is data that needs to be exported or accessed from the class.

You can use a getter method by creating a function that returns values in the class declaration.

Setter Method

This method changes data within the class. Sometimes setters are needed when dealing with immutable data or data that requires modification.

You can use the setter method similarly to declaring the init function in the class.

Example of Getter and Setter Methods:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Employee:
"Common base class for all employees"
empCount = 0

def __init__(self, name="Employee", salary=5000):
self.name = name
self.salary = salary
Employee.empCount += 1

def displayCount(self):
print("Total Employees: %d" % Employee.empCount)

def printEmployee(self):
print("Name:", self.name, "\nSalary:", self.salary)

# Getter Method Declaration
def getName(self):
return self.name

def getSalary(self):
return self.salary

# Setter Method Declaration
def setName(self, name):
self.name = name

def setSalary(self, salary):
self.salary = salary

employee1 = Employee()

employee1.printEmployee()
employee1.displayCount()

# Example of Getter Implementation
employeeName = employee1.getName()
print("Employee's name is", employeeName)

# Example of Setter Implementation
employee1.setName("Azhar")
employee1.setSalary(10000)
employee1.printEmployee()

employee2 = Employee("Gerald", 4000)

employee2.printEmployee()
employee2.displayCount()

The Modern Pythonic Way: @property Decorator

The manual getName()/setName() pattern shown above is common in languages like Java or C#, but it is not the idiomatic style in modern Python. Python code today typically uses the built-in @property decorator instead, so that a “getter” and “setter” can be accessed like a normal attribute (employee.salary) instead of a method call (employee.getSalary()), while still allowing validation logic behind the scenes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Employee:
def __init__(self, name: str = "Employee", salary: float = 5000):
self.name = name
self._salary = salary # underscore convention marks it as "internal"

@property
def salary(self) -> float: # Getter: accessed like an attribute
return self._salary

@salary.setter
def salary(self, new_salary: float): # Setter: still allows validation
if new_salary < 0:
raise ValueError("Salary cannot be negative")
self._salary = new_salary

employee1 = Employee("Azhar", 5000)
print(employee1.salary) # Calls the getter, no parentheses needed
employee1.salary = 6000 # Calls the setter automatically
print(employee1.salary)

dataclass (Modern Standard for Simple Data-Holding Classes)

For classes whose main purpose is to store data (like Employee or Student), modern Python offers the @dataclass decorator (from the built-in dataclasses module), which automatically generates __init__, a readable __repr__, and comparison methods — removing a lot of boilerplate code:

1
2
3
4
5
6
7
8
9
from dataclasses import dataclass

@dataclass
class Student:
name: str
score: float

student1 = Student("ARZ", 95)
print(student1) # Automatically prints: Student(name='ARZ', score=95)

Both @property and @dataclass are considered current industry best practice and are commonly seen in professional Python codebases, though understanding the classic __init__/getter/setter pattern above is still essential, since it explains what these modern tools are doing under the hood.

Inheritance

Inheritance was mentioned briefly at the start of this module (“a Parent Class has a Child Class that inherits the same properties”) but was never demonstrated in code. Inheritance is one of the four core pillars of OOP, and lets a new class reuse the attributes and methods of an existing class:

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
# Parent Class (Base Class)
class Animal:
def __init__(self, name: str):
self.name = name

def eat(self):
print(f"{self.name} is eating")

def make_sound(self):
print(f"{self.name} makes a sound")

# Child Class (Derived Class) - inherits from Animal
class Dog(Animal):
def make_sound(self): # Overriding the parent method
print(f"{self.name} says: Woof!")

class Cat(Animal):
def make_sound(self): # Overriding the parent method
print(f"{self.name} says: Meow!")

dog = Dog("Rex")
cat = Cat("Milo")

dog.eat() # Inherited from Animal -> "Rex is eating"
dog.make_sound() # Overridden in Dog -> "Rex says: Woof!"
cat.make_sound() # Overridden in Cat -> "Milo says: Meow!"

Dog(Animal) means Dog inherits everything from Animal. Both Dog and Cat reuse the eat() method without rewriting it, while each provides its own version of make_sound(). This avoids code duplication — the same core benefit functions gave us earlier, now applied to classes.

Polymorphism

Polymorphism (“many forms”) means objects of different classes can be used through the same interface, each responding in its own way. Using the Dog and Cat classes above:

1
2
3
4
animals = [Dog("Rex"), Cat("Milo"), Dog("Buddy")]

for animal in animals:
animal.make_sound() # Same method call, different behavior per object

Output:

1
2
3
Rex says: Woof!
Milo says: Meow!
Buddy says: Woof!

Even though animal.make_sound() is called the same way for every item, Python automatically runs the correct version depending on the actual object type (Dog or Cat). This is polymorphism in practice, and it’s what makes code like this loop possible without needing if isinstance(animal, Dog): ... checks everywhere.

Encapsulation (Naming Clarification, Added)

The @property example shown earlier in this module (using self._salary with a leading underscore) is actually an OOP concept called Encapsulation — bundling data and the methods that protect/control access to it inside a class. Python doesn’t have true “private” variables like Java (private int salary;); instead it uses a naming convention:

  • self.name — public, accessible directly
  • self._name — “protected” by convention only (a single underscore signals “internal use,” but Python does not enforce it)
  • self.__name — “private” (double underscore triggers Python’s name-mangling, making it harder — though not impossible — to access from outside the class)

Abstraction

The last of the four pillars is Abstraction: exposing only the relevant, essential features of an object while hiding the complicated implementation details behind a simple interface. As a real-world analogy, when you drive a car, you only interact with the steering wheel, pedals, and gear stick — you don’t need to know how the engine, fuel injection, or transmission actually work internally to be able to drive it.

In Python, Abstraction is commonly implemented using the built-in abc (Abstract Base Class) module. An abstract class cannot be instantiated directly — it exists only to be inherited from, and it forces every child class to implement certain methods, using the @abstractmethod decorator:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from abc import ABC, abstractmethod

# Abstract Class - cannot be instantiated directly
class Shape(ABC):
@abstractmethod
def area(self):
pass # No implementation here, just a required "contract"

@abstractmethod
def perimeter(self):
pass

# Concrete Class - must implement all abstract methods
class Rectangle(Shape):
def __init__(self, length: float, width: float):
self.length = length
self.width = width

def area(self):
return self.length * self.width

def perimeter(self):
return 2 * (self.length + self.width)

class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius

def area(self):
return 3.14 * self.radius ** 2

def perimeter(self):
return 2 * 3.14 * self.radius

# shape = Shape() # This would raise: TypeError: Can't instantiate abstract class

rectangle = Rectangle(5, 3)
circle = Circle(4)

print(f"Rectangle Area: {rectangle.area()}, Perimeter: {rectangle.perimeter()}")
print(f"Circle Area: {circle.area()}, Perimeter: {circle.perimeter()}")

Trying to create Shape() directly raises a TypeError, because Shape is only meant to define what every shape must be able to do (area() and perimeter()), not how — the “how” is left entirely to each concrete child class like Rectangle and Circle. Notice how this reuses the same Shape/Rectangle/Circle idea from the Polymorphism example above: Abstraction is what guarantees, at the class-design level, that every shape subclass will always provide an area() method, instead of just relying on convention.

Encapsulation, Inheritance, Polymorphism, and Abstraction (hiding internal complexity) together form the four pillars of Object-Oriented Programming.

Exercise

Exercise 1

Create a program that implements a class named Student with methods to display the biodata of the student entered by the user.

Exercise 2 (Inheritance & Polymorphism)

Create a parent class named Shape with a method area() that returns 0. Create two child classes, Rectangle and Circle, that each override area() with the correct formula. Store several shape objects in a list and loop through them, printing each shape’s area using the same area() method call (polymorphism).

Exercise 3 (Abstraction)

Create an abstract class named PaymentMethod with an abstract method pay(amount). Create two concrete child classes, CreditCard and EBanking, that each implement pay(amount) with their own printed message (e.g., “Paid $50 using Credit Card”). Instantiate both classes, store them in a list, and loop through the list calling pay() on each, similar to Exercise 2 above.

Exercise 4

Create a class that implements getter and setter methods, utilizing branching and looping as in the previous exercise. The program should accept user input for variable declarations and store them in a class, allowing manipulation and modification based on user input. The data can be displayed using the getter and setter methods.

Example Output:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
===== OOP Program =====
1. Declare Object
2. Display Object
3. Change Object Value
4. Delete Object
5. Exit Program
Enter Your Choice (1/2/3/4/5): 2 (Input by User)

Name: None
Score: None

===== OOP Program =====
1. Declare Object
2. Display Object
3. Change Object Value
4. Delete Object
5. Exit Program
Enter Your Choice (1/2/3/4/5): 1 (Input by User)
Enter Your Name: ARZ (Input by User)
Enter Your Score: 95 (Input by User)
Data Successfully Added

===== OOP Program =====
1. Declare Object
2. Display Object
3. Change Object Value
4. Delete Object
5. Exit Program
Enter Your Choice (1/2/3/4/5): 2 (Input by User)

Name: ARZ
Score: 95

===== OOP Program =====
1. Declare Object
2. Display Object
3. Change Object Value
4. Delete Object
5. Exit Program

Enter Your Choice (1/2/3/4/5): 3 (Input by User)
What would you like to change (Name/Score): Score (Input by User)
Enter New Score: 100 (Input by User)
Score Data Successfully Changed

===== OOP Program =====
1. Declare Object
2. Display Object
3. Change Object Value
4. Delete Object
5. Exit Program

Enter Your Choice (1/2/3/4/5): 2 (Input by User)

Name: ARZ
Score: 100

===== OOP Program =====
1. Declare Object
2. Display Object
3. Change Object Value
4. Delete Object
5. Exit Program
Enter Your Choice (1/2/3/4/5): 4 (Input by User)
Data Successfully Deleted

===== OOP Program =====
1. Declare Object
2. Display Object
3. Change Object Value
4. Delete Object
5. Exit Program
Enter Your Choice (1/2/3/4/5): 2 (Input by User)

Name: None
Score: None

===== OOP Program =====
1. Declare Object
2. Display Object
3. Change Object Value
4. Delete Object
5. Exit Program
Enter Your Choice (1/2/3/4/5): 5 (Input by User)
Thank you for using my program.