Understanding OOP Concepts
Object-oriented programming (OOP) is a fundamental paradigm in Python that enables developers to create modular, reusable, and maintainable code. This article explores the core concepts of OOP in Python including classes, objects, inheritance, polymorphism, and encapsulation.
Classes and Objects
A class serves as a blueprint for creating objects. It defines the attributes (data) and methods (functions) that objects instantiated from the class will possess.
Class Definition
class BankAccount:
# Class variable - shared across all instances
bank_name = "Global Bank"
# Constructor method
def __init__(self, account_number, owner_name, initial_balance=0):
# Instance variables - unique to each instance
self.account_number = account_number
self.owner_name = owner_name
self.balance = initial_balance
# Instance method
def deposit(self, amount):
if amount > 0:
self.balance += amount
return f"Deposited {amount}. New balance: {self.balance}"
return "Invalid deposit amount"
def withdraw(self, amount):
if amount > 0 and amount <= self.balance:
self.balance -= amount
return f"Withdrew {amount}. New balance: {self.balance}"
return "Invalid withdrawal amount"
def get_balance(self):
return self.balance
Creating Objects
# Instantiation - creating an object from a class
account1 = BankAccount("ACC001", "Alice Johnson", 5000)
account2 = BankAccount("ACC002", "Bob Smith", 10000)
# Accessing attributes
print(account1.owner_name) # Alice Johnson
print(account2.balance) # 10000
# Calling methods
print(account1.deposit(1500)) # Deposited 1500. New balance: 6500
print(account2.withdraw(2000)) # Withdrew 2000. New balance: 8000
The Role of self
The self parameter in Python methods refers to the current instance of the class. When a method is called, Python automatically passes the instance as the first argument. This allows each object to access and modify its own attributes.
class Calculator:
def __init__(self, model):
self.model = model
def add(self, a, b):
print(f"Using {self.model} calculator:")
return a + b
calc = Calculator("Scientific")
result = calc.add(5, 3) # self is automatically passed
print(result) # 8
Class Variables vs Instance Variables
Understanding the distinction between class and instance variables is crucial:
class Employee:
# Class variable - shared by all instances
company_name = "Tech Corporation"
def __init__(self, employee_id, name):
# Instance variable - unique to each instance
self.employee_id = employee_id
self.name = name
@classmethod
def get_company(cls):
return cls.company_name
emp1 = Employee("E001", "Alice")
emp2 = Employee("E002", "Bob")
print(Employee.company_name) # Tech Corporation
print(emp1.company_name) # Tech Corporation (accessed from class)
# Modification affects all instances
Employee.company_name = "New Tech Corp"
print(emp1.company_name) # New Tech Corp
print(emp2.company_name) # New Tech Corp
Inheritence
Inheritance allows a new class to inherit attributes and methods from an existing class. The new class is called a subclass or derived class, and the existing class is the superclass or base class.
Single Inheritance
# Base class
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
return f"I am {self.name}, {self.age} years old"
# Derived class
class Student(Person):
def __init__(self, name, age, student_id):
super().__init__(name, age) # Call parent constructor
self.student_id = student_id
def study(self, subject):
return f"{self.name} is studying {subject}"
student = Student("Alice", 20, "S12345")
print(student.introduce()) # I am Alice, 20 years old
print(student.study("Python")) # Alice is studying Python
Multiple Inheritance
Python supports multiple inheritance, allowing a class to inherit from multiple parent classes:
class Worker:
def work(self):
return "Working..."
class Learner:
def learn(self):
return "Learning..."
class Intern(Worker, Learner):
def daily_task(self):
return f"{self.work()} and {self.learn()}"
intern = Intern()
print(intern.daily_task()) # Working... and Learning...
Method Resolution Order (MRO)
When multiple inheritance is used, Python uses MRO to determine the order in which base classes are searched:
class A:
def greet(self):
return "Hello from A"
class B(A):
def greet(self):
return "Hello from B"
class C(A):
def greet(self):
return "Hello from C"
class D(B, C):
pass
d = D()
print(d.greet()) # Hello from B
print(D.__mro__) # Shows the inheritance chain
Polymorphism
Polymorphism allows objects of different classes to be treated uniformly through a common interface.
Using Inheritance
class Bird:
def fly(self):
return "Some birds can fly"
class Penguin(Bird):
def fly(self):
return "Penguins cannot fly"
class Sparrow(Bird):
def fly(self):
return "Sparrows can fly"
def demonstrate_flight(bird):
print(bird.fly())
bird = Bird()
penguin = Penguin()
sparrow = Sparrow()
demonstrate_flight(bird) # Some birds can fly
demonstrate_flight(penguin) # Penguins cannot fly
demonstrate_flight(sparrow) # Sparrows can fly
Duck Typing
Python follows the principle: "If it walks like a duck and quacks like a duck, it's a duck." This means objects are judged by their behavior rather than their type:
class Duck:
def speak(self):
return "Quack!"
class Dog:
def speak(self):
return "Woof!"
def make_speak(obj):
print(obj.speak())
duck = Duck()
dog = Dog()
make_speak(duck) # Quack!
make_speak(dog) # Woof!
Encapsulation
Encapsulation restricts direct access to certain attributes, providing controlled interfaces for data manipulation.
Private Variables
Python uses name mangling to create private attributes:
class SecureVault:
def __init__(self, password):
self.__secret_code = password # Private attribute
def verify(self, code):
return self.__secret_code == code
def __private_method(self):
return "This is private"
vault = SecureVault("secret123")
print(vault.verify("secret123")) # True
# vault.__secret_code # AttributeError - cannot access directly
# Access via name mangling
print(vault._SecureVault__secret_code) # secret123
Property Decorator
The @property decorator provides controlled access to attributes:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return (self.celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9
temp = Temperature(0)
print(temp.fahrenheit) # 32.0
temp.fahrenheit = 100
print(temp.celsius) # 37.777...
Class Methods and Static Methods
class MathUtils:
@staticmethod
def add(x, y):
return x + y
@classmethod
def description(cls):
return f"This is a {cls.__name__} class"
print(MathUtils.add(5, 3)) # 8
print(MathUtils.description()) # This is a MathUtils class
Composition
Composition creates "has-a" relationships, where one class contains another as a component:
class Engine:
def start(self):
return "Engine started"
class Car:
def __init__(self, model):
self.model = model
self.engine = Engine() # Composition
def start_car(self):
return f"{self.model}: {self.engine.start()}"
car = Car("Tesla Model 3")
print(car.start_car()) # Tesla Model 3: Engine started
Summary
Object-oriented programming in Python provides several key mechanisms:
| Concept | Purpose |
|---|---|
| Classes | Blueprints for creating objects |
| Objects | Instances with unique attribute values |
| Inheritance | Code reuse through parent-child relationships |
| Polymorphism | Uniform treatment of different object types |
| Encapsulation | Controlled access to internal data |
These concepts work together to create clean, maintainable, and extensible software designs.