DEV Community

Ashutosh Sarangi
Ashutosh Sarangi

Posted on

Intermediate Python OOP For Gen AI

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}" 
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ›‘οΈ 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
Enter fullscreen mode Exit fullscreen mode

πŸ”’ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

⚠️ 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__)
Enter fullscreen mode Exit fullscreen mode

🧠 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()
Enter fullscreen mode Exit fullscreen mode

πŸ” Output:

Parent1 show()
Enter fullscreen mode Exit fullscreen mode

βœ… 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__)
Enter fullscreen mode Exit fullscreen mode

🧠 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()
Enter fullscreen mode Exit fullscreen mode

πŸ” Output:

Child show()
Parent1 show()
Parent2 show()
Enter fullscreen mode Exit fullscreen mode

βœ… Why?

  • super() follows the MRO chain.
  • In Child, super().show() calls Parent1.show().
  • In Parent1, super().show() calls Parent2.show().

πŸ”„ MRO Chain:

Child β†’ Parent1 β†’ Parent2 β†’ object
Enter fullscreen mode Exit fullscreen mode

πŸ’Ž 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()
Enter fullscreen mode Exit fullscreen mode

πŸ” Output:

B
Enter fullscreen mode Exit fullscreen mode

βœ… 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Preservation of local precedence order (order in which base classes are listed).
  2. Monotonicity (subclasses preserve the order of their parents).
  3. 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 and C:
    • 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
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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()
Enter fullscreen mode Exit fullscreen mode

πŸ” Output:

D
B
C
A
Enter fullscreen mode Exit fullscreen mode

βœ… Why?

  • super() follows the MRO.
  • Each class calls the next one in the MRO chain.

πŸ” View MRO Programmatically

print(D.__mro__)
Enter fullscreen mode Exit fullscreen mode

Reference:-

Top comments (0)