Class
-
A blueprint for creating objects.
-
It defines a set of attributes and methods that the created objects will have.
class Dog:
def __init__(self, name, age):
self.name=name
self.age=age
def bark(self):
print(f"{self.name} says woof!")
Object
-
An instance of a class.
-
We use instances of a class to use its defined attributes and methods.
my_dog = Dog("Buddy", 8)
my_dog.bark()
# output : Buddy says woof!
Encapsulation
-
Encapsulation is about bundling data (attributes) and methods that operate on the data into a single unit (class).
-
It also involves restricting direct access to some components often using private attributes or methods.
Public
-
accessible form anywhere
class MyClass:
def __init__(self):
self.public_var = "I am a public var"
def public_method(self):
print("I am a public method")
obj = MyClass()
obj.public_method()
print(obj.public_var)
# Output:
# I am a public method
# I am a public var
-
public var and public method can easily be accessed from anywhere outside the class. There are no restrictions in modifying public members.
Protected
-
start with a single ' _ ', these are meant for internal use but can be accessed externally.
-
protected members are intended to be used only within a class and its subclasses (derived classes), they are not meant to be accessed directly from outside the class hierarchy.
class MyClass:
def __init__(self):
self._protected_var="this is a protected var"
def _protected_method(self):
print("THis is a protected method")
obj = MyClass()
obj._protected_method()
print(obj._protected_var)
# Output:
# THis is a protected method
# this is a protected var
-
the underscore is just a convention and it signals the devs that its supposed to be protected. However python does not enforce this restriction, you can still access these protected members from outside the class but it is considered a bad practice.
-
sub classes can access and modify the protected memebrs.
class SubClass(MyClass):
def access_protected(self):
print(self._protected_var)
sub_obj = SubClass()
sub_obj.access_protected()
# output - this is a protected var
Private
- Start with double underscore ' __ ', these are not accessible directly from outside the class. they are meant to be used only inside the class in which they are defined.
class MyClass:
def __init__(self):
self.__private_var = "i am private variable"
def __private_method(self):
print("I am a private method")
def get_private_var(self):
print(self.__private_var)
def call_private_method(self):
self.__private_method()
obj = MyClass()
obj.get_private_var()
obj.call_private_method()
# output
# i am private variable
# I am a private method
Why use Public, Protected & Private?
-
Encapsulation - By controlling access to attributes and methods, you ensure that the internal state of an object is not accidentally modified from outside the class
-
Code Maintainability - Restricting access to certain members make it easier to change the internal implementation without affecting other parts of the code.
-
Prevent Misuse - Private members prevent accidental use of certain classes which are not meant to be exposed.
class Person:
def __init__(self, name, age):
self.__name = name
self.__age = age
def get_name(self):
return self.__name
def set_name(self, new_name):
if isinstance(new_name, str):
self.__name = new_name
else:
print("name must be str only")
def get_age(self):
return self.__age
def set_age(self, new_age):
if isinstance(new_age, int):
self.__age = new_age
else:
print("age must only be a number")
person1 = Person("Bob", 23)
print(person1.get_name())
print(person1.get_age())
# Bob
# 23
person1.set_age(25)
person1.set_name("Charan")
print(person1.get_name())
print(person1.get_age())
# Charan
# 25
person1.set_age("25")
person1.set_name(50)
# age must only be a number
# name must be str only
Inheritance
Inheritance allows a class (child or subclass) inherit attributes and methods form another class (parent or superclass)
Single Inheritance
one class inherits from another.
class Animal:
def speak(self):
print("Animal speaks")
class Dog(Animal):
def bark(self):
print("Dog barks")
dog1 = Dog()
dog1.speak()
dog1.bark()
# output
Animal speaks
Dog barks
Multilevel Inheritance
a class inherits from a derived class.
class Puppy(Dog):
def play(self):
print("Puppy wants to play")
Multiple Inheritance
Inherits from more than one class
class A:
def method_a(self):
print("Method A")
class B:
def method_b(self):
print("Method B")
class C(A, B):
def method_c(self):
print("Method C")
What’s MRO?
MRO stands for Method Resolution Order, and it’s the order in which Python looks for methods in a class hierarchy. It ensures that methods are called in a consistent and predictable way, especially in cases of multiple inheritance. You can check the MRO of a class using ClassName.mro()
or help(ClassName)
**.
class A:
def method(self):
return "A"
class B(A):
def method(self):
return "B"
class C(A):
def method(self):
return "C"
class D(B, C):
pass
print(D.mro())
# Output: [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
d = D()
print(d.method())
# Output: B
In the example above, the MRO for class D
is [D, B, C, A, object]
. When d.method()
is called, Python searches for the method
in D
, then B
, then C
, then A
, and finally object
. Since B
's method is found first, it is executed.
Super()
In Python, super()
is like asking for help from your parent when you're stuck with a task. It lets you call a method or access something from your parent class (the class your current class inherits from). This is useful because it avoids repeating code and ensures that the parent class's logic is still used.
class A:
def __init__(self):
print("A")
class B(A):
def __init__(self):
# super().__init__()
print("B")
super().__init__()
class C(A):
def __init__(self):
# super().__init__()
print("C")
super().__init__()
class D(B, C):
def __init__(self):
print("D")
super().__init__()
d = D()
print(d)
"""
output:
D
B
C
A
<__main__.D object at 0x7ac80b8e1730>
"""
Polymorphism
It allows objects of different classes to be treated as objects of a common super class
most common use is when a parent class reference is used to refer to a child class object.
Method Overriding
a sub class provides a specific implementation of a function that is already defined in a super class
class Bird:
def fly(self):
print("Bird Flies")
class Sparrow:
def fly(self):
print("Sparrow flies high")
Duck Typing
-
If it looks like a duck and quacks like a duck, it must be a duck.
-
In python it means that if the object has the required attributes/methods, it can be used in place of the another.
class Bird:
def fly(self):
print("Bird Flies")
class Sparrow:
def fly(self):
print("Sparrow flies high")
def make_fly(bird):
bird.fly()
sparrow = Sparrow()
make_fly(sparrow)
Abstraction
Abstraction lets you interact with objects through a simplified interface while the internal workings are hidden and and can vary between objects. This makes your code more modular and easier to extend or modify.
from abc import ABC, abstractmethod
# Abstract class for Bank Account
class BankAccount(ABC):
def __init__(self, account_number, owner_name):
self.account_number = account_number
self.owner_name = owner_name
self._balance = 0 # Protected variable
@abstractmethod
def get_account_type(self):
pass
def deposit(self, amount):
if amount > 0:
self._balance += amount
return f"Deposited ${amount}. New balance: ${self._balance}"
return "Invalid amount"
def withdraw(self, amount):
if amount > 0 and self._balance >= amount:
self._balance -= amount
return f"Withdrew ${amount}. New balance: ${self._balance}"
return "Insufficient funds"
# Concrete class for Savings Account
class SavingsAccount(BankAccount):
def get_account_type(self):
return f"Savings Account - Owner: {self.owner_name}, Account #: {self.account_number}, Balance: ${self._balance}"
# Concrete class for Checking Account
class CheckingAccount(BankAccount):
def get_account_type(self):
return f"Checking Account - Owner: {self.owner_name}, Account #: {self.account_number}, Balance: ${self._balance}"
# Using the bank system
savings = SavingsAccount("SA123", "Alice")
checking = CheckingAccount("CA456", "Bob")
print(savings.get_account_type()) # Output: Savings Account details
print(checking.get_account_type()) # Output: Checking Account details
print(savings.deposit(1000)) # Output: Deposited $1000. New balance: $1000
print(savings.withdraw(500)) # Output: Withdrew $500. New balance: $500
print(checking.deposit(2000)) # Output: Deposited $2000. New balance: $2000
print(checking.withdraw(2500)) # Output: Insufficient funds
Why have we used @abstractmethod ??
In BankAccount
, @abstractmethod
on get_account_type
forces SavingsAccount
and CheckingAccount
to implement it, ensuring each provides its unique account info while reusing common methods like deposit
and withdraw
.
Without @abstractclass
, subclass wouldn’t be required to implement get_account_type
, which could lead to incomplete or inconsistent behavior.