Python - Classes

21.
How can you implement operator overloading in Python classes?

Operator overloading in Python allows you to define how operators behave for objects of your class. This is achieved by implementing special methods, often referred to as magic or dunder methods, in your class. Each operator has a corresponding special method that you can define to customize its behavior.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # Overloading the addition operator
    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

    # Overloading the equality operator
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    # Overloading the string representation
    def __str__(self):
        return f"({self.x}, {self.y})"

# Creating objects of the Point class
point1 = Point(1, 2)
point2 = Point(3, 4)

# Using the overloaded addition operator
result = point1 + point2

# Checking equality using the overloaded equality operator
are_equal = point1 == point2

# Displaying the results
print(f"Result of addition: {result}")
print(f"Are points equal? {are_equal}")

In this example, the Point class overloads the addition operator (__add__), the equality operator (__eq__), and the string representation (__str__). The overloaded addition operator allows you to use the + operator with objects of the Point class. The overloaded equality operator allows you to compare two points for equality using the == operator. The overloaded string representation defines how a point object should be displayed when converted to a string.

Output:

Result of addition: (4, 6)
Are points equal? False

22.
Discuss the use of properties in Python classes for attribute access.

In Python, properties are a way to customize the access and modification of class attributes. They allow you to define getter, setter, and deleter methods for an attribute, giving you more control over its behavior. Properties are often used to encapsulate attribute access and provide a clean interface for working with objects.

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

    # Getter method for the radius property
    @property
    def radius(self):
        return self._radius

    # Setter method for the radius property
    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    # Deleter method for the radius property
    @radius.deleter
    def radius(self):
        print("Deleting the radius")
        del self._radius

# Creating an object of the Circle class
circle = Circle(5)

# Accessing the radius property (getter)
current_radius = circle.radius

# Modifying the radius property (setter)
circle.radius = 7

# Deleting the radius property (deleter)
del circle.radius

# Displaying the results
print(f"Current radius: {current_radius}")

In this example, the Circle class has a private attribute (_radius) and a property named radius. The property is defined with a getter method, a setter method, and a deleter method. The getter method allows you to access the value of the _radius attribute, the setter method allows you to modify the value, and the deleter method allows you to delete the attribute.

Output:

Deleting the radius
Current radius: 5

23.
What is the purpose of the __del__ method in a class?

In Python, the __del__ method is a special method that is called when an object is about to be destroyed. It is used to perform cleanup actions or release resources before an object is deleted. However, it's important to note that the __del__ method is not guaranteed to be called at a specific time, and relying on it for critical cleanup is not recommended.

class MyClass:
    def __init__(self, name):
        self.name = name

    # __del__ method for cleanup
    def __del__(self):
        print(f"Deleting {self.name}")

# Creating objects of the MyClass class
obj1 = MyClass("Object 1")
obj2 = MyClass("Object 2")

# Deleting one of the objects explicitly
del obj1

# Output will show that __del__ is called when an object is deleted

In this example, the MyClass class has a __del__ method that prints a message indicating the deletion of an object. When an object of this class is explicitly deleted using the del keyword, the __del__ method is called.

Output:

Deleting Object 1

24.
How do you use class decorators in Python?

In Python, class decorators are a way to modify or extend the behavior of a class. They are applied to a class definition using the @decorator syntax. Class decorators can be used to add new methods, modify existing methods, or perform other actions when a class is defined. Let's see an example of using a class decorator to add a method to a class:

# Class decorator to add a new method
def add_method(cls):
    # New method to be added to the class
    def new_method(self):
        return "This is a new method"

    # Adding the new method to the class
    cls.new_method = new_method
    return cls

# Applying the class decorator
@add_method
class MyClass:
    def existing_method(self):
        return "This is an existing method"

# Creating an object of the decorated class
obj = MyClass()

# Accessing both existing and new methods
existing_result = obj.existing_method()
new_result = obj.new_method()

# Displaying the results
print(existing_result)
print(new_result)

In this example, the add_method class decorator adds a new method named new_method to the decorated class MyClass. When an object of the decorated class is created, both the existing method (existing_method) and the newly added method (new_method) can be accessed.

Output:

This is an existing method
This is a new method