Object-Oriented Programming Paradigm
Encapsulation allows you to bundle data (attributes) and behaviors (methods) within a class to create a cohesive unit. By defining methods to control access to attributes and its modification, encapsulation helps maintain data integrity and promotes modular, secure code.
Inheritance enables the creation of hierarchical relationships between classes, allowing a subclass to inherit attributes and methods from a parent class. This promotes code reuse and reduces duplication.
Abstraction focuses on hiding implementation details and exposing only the essential functionality of an object. By enforcing a consistent interface, abstraction simplifies interactions with objects, allowing developers to focus on what an object does rather than how it achieves its functionality.
Polymorphism allows you to treat objects of different types as instances of the same base type, as long as they implement a common interface or behavior. Pythonβs duck typing make it especially suited for polymorphism, as it allows you to access attributes and methods on objects without needing to worry about their actual class.
class Dog:
species = "Canis familiaris" # class Attributes
def __init__(self, name, age): # constructor
self.name = name # Instance Attributes
self.age = age
# Instance method
def description(self):
return f"{self.name} is {self.age} years old"
# Another instance method
def speak(self, sound):
return f"{self.name} says {sound}"
Class attributes are shared across all instances of a class, while instance attributes are unique to each instance, allowing individual objects to have their own attribute values.
Why don't we use the new keyword for creating an instance of a class in Python?
π§© 1. Python Uses __new__()
and __init__()
Internally
- When you create an object like
obj = MyClass()
, Python internally calls:-
__new__()
β allocates memory and returns the instance. -
__init__()
β initializes the instance with given arguments.
-
- You donβt need to explicitly call
new()
unless you're doing something advanced (like customizing object creation in metaclasses).
π§ 2. Simplicity and Readability
- Python emphasizes clean and readable syntax.
- Using
MyClass()
is intuitive and consistent with Pythonβs philosophy of simplicity.
ποΈ 3. Dynamic and Flexible Object Model
- Python is dynamically typed and doesnβt require explicit memory management.
- Object creation is abstracted away to keep the language high-level and flexible.
How to Access the values
dog1 = Dog()
// Accessing Instance attribute
dog1.age
gog1.speak('Hello')
// Accessing Class Attributes
Dog.species
# Strange thing in Python (We can update the values):-
dog1.age = 10
dog1.age
dog1.species = "Felis silvestris"
dog1.species
Q. Do we have private, public, and protected attributes like Java or not in Python?
Python does not have strict access modifiers like private
, protected
, and public
as in Java or C++. Instead, it uses naming conventions to indicate the intended level of access:
π 1. Public Attributes
- Default behavior: All attributes and methods are public by default.
- You can access them freely from outside the class.
class MyClass:
def __init__(self):
self.name = "Ashutosh" # public attribute
obj = MyClass()
print(obj.name) # β
Accessible
π‘οΈ 2. Protected Attributes
- Indicated by a single underscore prefix:
_attribute
- This is a convention, not enforcement. It signals: βThis is for internal use.β
- Still accessible from outside, but discouraged.
class MyClass:
def __init__(self):
self._salary = 5000 # protected attribute
obj = MyClass()
print(obj._salary) # β οΈ Accessible, but not recommended
π 3. Private Attributes
- Indicated by a double underscore prefix:
__attribute
- Python performs name mangling to make it harder to access from outside.
class MyClass:
def __init__(self):
self.__password = "secret" # private attribute
obj = MyClass()
# print(obj.__password) # β AttributeError
print(obj._MyClass__password) # β
Accessible via name mangling
Q. Can we extend a child class from more than 1 parent class?
𧬠What is Multiple Inheritance?
It allows a class to inherit attributes and methods from multiple base classes.
class Parent1:
def greet(self):
print("Hello from Parent1")
class Parent2:
def farewell(self):
print("Goodbye from Parent2")
class Child(Parent1, Parent2):
pass
obj = Child()
obj.greet() # Inherited from Parent1
obj.farewell() # Inherited from Parent2
β οΈ Things to Watch Out For
Python uses the Method Resolution Order (MRO) to determine which method to call when there are conflicts (e.g., same method name in multiple parents).
You can check MRO using:
print(Child.__mro__)
π§ Behind the Scenes
Python uses the C3 linearization algorithm to resolve method calls in multiple inheritance. This ensures a consistent and predictable order.
π§ͺ Example: Method Resolution Order (MRO)
class Parent1:
def show(self):
print("Parent1 show()")
class Parent2:
def show(self):
print("Parent2 show()")
class Child(Parent1, Parent2):
pass
obj = Child()
obj.show()
π Output:
Parent1 show()
β Why?
Python uses Method Resolution Order (MRO) to decide which method to call. In this case, Parent1
is listed first in the inheritance, so its show()
method is used.
You can inspect MRO like this:
print(Child.__mro__)
π§ Using super()
in Multiple Inheritance
Letβs modify the example to use super()
:
class Parent1:
def show(self):
print("Parent1 show()")
super().show()
class Parent2:
def show(self):
print("Parent2 show()")
class Child(Parent1, Parent2):
def show(self):
print("Child show()")
super().show()
obj = Child()
obj.show()
π Output:
Child show()
Parent1 show()
Parent2 show()
β Why?
-
super()
follows the MRO chain. - In
Child
,super().show()
callsParent1.show()
. - In
Parent1
,super().show()
callsParent2.show()
.
π MRO Chain:
Child β Parent1 β Parent2 β object
π Diamond Inheritance Problem
𧬠Structure:
class A:
def show(self):
print("A")
class B(A):
def show(self):
print("B")
class C(A):
def show(self):
print("C")
class D(B, C):
pass
obj = D()
obj.show()
π Output:
B
β Why?
Python uses C3 linearization to determine the Method Resolution Order (MRO). In this case, the MRO for class D
is:
D β B β C β A β object
So D.show()
calls B.show()
first.
π§ C3 Linearization Algorithm Explained
C3 linearization is used to compute the MRO in a consistent and predictable way. It ensures:
- Preservation of local precedence order (order in which base classes are listed).
- Monotonicity (subclasses preserve the order of their parents).
- Consistency (no ambiguity in method resolution).
π§ How It Works (Simplified Steps):
For a class D(B, C)
:
- Start with
D
's parents:[B, C]
- Get MROs of
B
andC
:B β [B, A, object]
C β [C, A, object]
- Merge these lists with the direct parents
[B, C]
using the C3 algorithm:- Pick the first class that doesnβt appear later in any other list.
- Repeat until all classes are merged.
π Result:
D β B β C β A β object
π§ͺ Using super()
in Diamond Inheritance
class A:
def show(self):
print("A")
class B(A):
def show(self):
print("B")
super().show()
class C(A):
def show(self):
print("C")
super().show()
class D(B, C):
def show(self):
print("D")
super().show()
obj = D()
obj.show()
π Output:
D
B
C
A
β Why?
-
super()
follows the MRO. - Each class calls the next one in the MRO chain.
π View MRO Programmatically
print(D.__mro__)
Reference:-
Top comments (0)