Skip to main content

Dunder or Magic Methods

Dunder or Magic Methods in Python

Dunder (double underscore) or magic methods are special methods in Python that allow for customization of classes and objects. These methods are called magic because they can change the behavior of code in unexpected ways. Understanding and implementing these methods can greatly enhance the functionality and flexibility of your Python programs.

Constructing Objects and Expressions

In Python, objects are instances of classes, which define the attributes and methods of the object. The process of creating an object in Python involves defining a class, which specifies the structure and behavior of the object, and then creating instances of that class.

Defining Classes in Python

To define a class in Python, you use the class keyword, followed by the name of the class. For example, the following code defines a simple class called Person:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def say_hello(self):
        print(f"Hello, my name is {self.name} and I am {self.age} years old.")

The __init__ method is a special method that is called when an instance of the class is created. It initializes the attributes of the object.

Python __init__ Magic Method

The __init__ method is a special magic method that is called when an instance of a class is created. It initializes the attributes of the object. In the example above, the __init__ method takes two parameters, name and age, which are used to initialize the name and age attributes of the object.

Creating Instances of Classes in Python

To create an instance of a class, you call the class as if it were a function, passing any arguments that the __init__ dunder method requires. For example, the following code creates two instances of the Person class:

# Defining a car class
class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
    
    def describe_car(self):
        print(f"The car is a {self.year} {self.make} {self.model}.")
    
    
# Creating an instance of Car class
car1 = Car("Honda", "Accord", 2021)

# Calling the describe_car method
car1.describe_car()

# Output: The car is a 2021 Honda Accord.
# Defining a book class
class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
    
    def describe_book(self):
        print(f"The book '{self.title}' is written by {self.author} and has {self.pages} pages.")
        

# Creating an instance of Book class
book1 = Book("The Alchemist", "Paulo Coelho", 208)

# Calling the describe_book method
book1.describe_book()

# Output: The book 'The Alchemist' is written by Paulo Coelho and has 208 pages.

Creating Iterator Objects

An iterator is an object that enables sequential iteration (looping) over a collection of items, one item at a time. In Python, you can create iterator objects using classes or functions.

Python Generator Class

You can create an iterator using the generator class in Python. The generator class is a type of object that is used to create iterable objects using the yield statement.

class MyGenerator:
    def __init__(self):
        self.num = 0

    def __iter__(self):
        return self
    
    def __next__(self):
        if self.num <= 5:
            value = self.num
            self.num += 1
            return value
        else:
            raise StopIteration
def my_generator():
    num = 0
    while num <= 5:
        yield num
        num += 1

# Using the generator class
gen = MyGenerator()
for x in gen:
    print(x)

# Using the function generator
gen = my_generator()
for x in gen:
    print(x)

In this example, MyGenerator is a generator class that inherits from the built-in object class. It defines an __init__() method that initializes the num attribute to 0. It also defines __iter__() method that returns the iterator object (self in this case) and __next__() magic method that generates the next value in the sequence.

You can also create an iterator using a Python function generator. A function generator is afunction that contains the yield statement.

In this example, my_generator function is a function generator that uses the yield statement to generate the next value in the sequence.

In both examples above, you can create an iterator object as follows:

Both code examples will output the values 0, 1, 2, 3, 4, and 5 when iterated over.

Handling Attribute References

Attribute references are used to access attributes of an object in Python. They can be accessed using the dot notation syntax and can also be accessed dynamically using the getattr() function.

The getattr() function takes two arguments - the object whose attribute needs to be accessed and the name of the attribute as a string. If the attribute is not found, an AttributeError is raised.


class Dog:
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

my_dog = Dog("Max", "German Shepherd")
print(my_dog.name) ### Output

my_cat = {"name": "Fluffy", "breed": "Persian"}
cat_name = getattr(my_cat, "name")
print(cat_name) ### Output

In first case, we create a Dog class and access the name attribute using the dot notation syntax.

In second case, we create a dictionary object my_cat and access the name attribute dynamically using the getattr() function. We store the value of the attribute in cat_name and print it out.

Representing Objects as Strings with Magic Method

In Python, we can represent objects as string using the __repr__() dunder method. This method is called when we use the repr() function or when we print an object using print() function.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def __repr__(self):
        return f"Point({self.x}, {self.y})"
        
p = Point(2, 3)
print(p)  ### Output

In the above code, we have defined a Point class with x and y attributes. We have also defined a __repr__() dunder method that returns a string representation of the Point object. When we print the p object, it calls the __repr__() magic method to get its string representation.

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        
    def __repr__(self):
        return f"Car(make={self.make}, model={self.model}, year={self.year})"
        
c = Car("Toyota", "Camry", 2021)
print(c)  ### Output

In this example, we have defined a Car class with make, model, and year attributes. We have also defined a __repr__() method that returns a string representation of the Car object. When we print the c object, it calls the __repr__() dunder method to get its string representation.

Cleaning Up Objects with Dunder Method

In Python, objects are automatically garbage collected when they are no longer needed. However, sometimes it may be necessary to define additional cleanup actions for an object. This can be done using the __del__ method, which is called when the object is about to be destroyed.

This dunder method is useful for releasing resources such as files, network connections, or other system-level objects that are not automatically managed by Python.

class MyClass:
    def __init__(self):
        self.file = open('example.txt', 'r')

    def __del__(self):
        self.file.close()

In this example, the MyClass constructor creates a file object and stores it in the file instance variable. When the object is destroyed, the __del__ method is called, which closes the file.

Performing Comparisons with Dunder Methods

Python provides multiple ways to compare values, variables, or expressions. Some commonly used operators for performing comparisons include ==, !=, >, <, >=, <=, in, and is.

Python Compare Strings

The __lt__() method is used to implement the less than comparison operator in Python. It returns True if the first string is less than the second string and False otherwise.

string1 = "apple"
string2 = "banana"
if string1.__lt__(string2):
    print("string1 is less than string2")
else:
    print("string1 is greater than or equal to string2")

# Output:
#string1 is less than string2
fruits = ["apple", "banana", "orange", "kiwi"]
sorted_fruits = sorted(fruits, key=lambda x: x.__lt__("c"))
print(sorted_fruits)
# Output:

# ['orange', 'kiwi', 'apple', 'banana']

In the above example, we have sorted the list of fruits in ascending order based on whether the first character of each string is less than or greater than c. lambda x: x.__lt__(c) returns True if the first character of x is less than c and False otherwise.

Contribute with us!

Do not hesitate to contribute to Python tutorials on GitHub: create a fork, update content and issue a pull request.

Profile picture for user AliaksandrSumich
Python engineer, expert in third-party web services integration.
Updated: 05/03/2024 - 21:52
Profile picture for user angarsky
Reviewed and approved