What are these __method__ in 🐍 you sssssay? Look no further!
Tags: python, programming, beginners
Published: 2023-09-20 12:28:39

Have you ever used or seen the `__init__()` or `__str__()` method in Python classes? These are called dunder methods! Dunder, short for "double underscore", is a term used in Python to refer to special methods that allow programmers to modify the behavior of objects in a logical and intuitive way. In this article, we will examine Python's dunder methods and show how to use them to create more Pythonic code. We'll cover the basics of dunder methods, their common use cases, and best practices for using them effectively.

### How do Dunder Methods work?

Dunder (**d**ouble **under**score) methods, are unique in the Python programming language that have double underscores defined on either side of their names, such as `__init__()`, `__str__()`, or `__eq__()`. These methods allow you to modify the behaviour of objects and give a more user-friendly interface for dealing with them. You can specify how instances of the class should act when they are generated, printed, compared, or utilised in arithmetic operations by specifying dunder methods in a class declaration.

## Common Use Cases for Dunder Methods

Dunder methods are extensively used in Python libraries and frameworks, and have become a standard convention in the Python community. Here are some common use cases for dunder methods:

### Initialization

The `__init__()` method is used to initialize a new instance of a class. It is called when a new object is created, and is used to set the initial state of the object. This method takes a `self` parameter, which refers to the instance of the class being initialized, and any additional parameters that are needed to set the initial state of the object. The `__init__` method is most commonly used to initialize the attributes of an object when it is created.

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

person = Person("John", 25)
print(person.name) # Output: Alice
print(person.age) # Output: 25
```

### String Representation

The `__str__()` and `__repr__()` methods serve the purpose of defining a string representation of an object. 

> While `__str__()` is used to define a human-readable string representation, `__repr__()` is used to define a machine-readable format for the object.
> 

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

    def __str__(self):
        return f"{self.name} ({self.age} years old)"

person = Person("John", 25)
print(person) # Output: John (25 years old)
```

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

    def __repr__(self):
        return f"Person(name='{self.name}', age={self.age})"

person = Person("John", 25)
print(person) # Output: Person(name='John', age=25)
```

> The `__init__` method can be used in scenarios involving multiple inheritance to initialize attributes of all parent classes.
> 

```python
class Animal:
    def __init__(self, species):
        self.species = species

    def speak(self):
        pass

class Mammal:
    def __init__(self, mammal_type):
        self.mammal_type = mammal_type

    def feed_young(self):
        pass

class Dog(Animal, Mammal):
    def __init__(self, name, age):
        Animal.__init__(self, "Dog")
        Mammal.__init__(self, "Canine")
        self.name = name
        self.age = age

    def speak(self):
        return "Woof"

dog = Dog("Fido", 3)
print(dog.species) # Output: Dog
print(dog.mammal_type) # Output: Canine
print(dog.name) # Output: Fido
print(dog.age) # Output: 3
print(dog.speak()) # Output: Woof
```

### **Container Operations**

Dunder methods can be used to define the behavior of container operations such as `len()`, indexing (`[]`), assignment to an indexed value (`obj[index] = value`), deletion of an indexed value (`del obj[index]`), and membership testing (`in`)

- **`__len__()`**
    
    The `__len__()` method is used to define the behavior of the `len()` function for objects of a class. It should return the number of items in the container.
    

```python
class MyList:
    def __init__(self, items):
        self.items = items

    def __len__(self):
        return len(self.items)

my_list = MyList([1, 2, 3])
print(len(my_list)) # Output: 3
```

- **`__getitem__()`**

The `__getitem__()`
 method is used to define the behavior of indexing (`[]`) for objects of a class. It should return the item at the given index.

```python
class MyList:
    def __init__(self, items):
        self.items = items

    def __getitem__(self, index):
        return self.items[index]

my_list = MyList([1, 2, 3])
print(my_list[2]) # Output: 3
```

- **`__setitem__()`**

The `__setitem__()` method is used to define the behavior of assignment to an indexed value (`obj[index] = value`) for objects of a class. It should set the item at the given index to the given value.

```python
class MyList:
    def __init__(self, items):
        self.items = items

    def __setitem__(self, index, value):
        self.items[index] = value

my_list = MyList([1, 2, 3, 4, 5])
my_list[2] = 66
print(my_list.items) # Output: [1, 2, 66, 4, 5]
```

- **`__delitem__()`**

The `__delitem__()` method is used to define the behavior of deletion of an indexed value (`del obj[index]`) for objects of a class. It should delete the item at the given index.

```python
class MyList:
    def __init__(self, items):
        self.items = items

    def __delitem__(self, index):
        del self.items[index]

my_list = MyList([1, 2, 3, 4, 5])
del my_list[2]
print(my_list.items) # Output: [1, 2, 4, 5]
```

- **`__contains__()`**
- The `__contains__()` method is used to define the behavior of membership testing (`in`) for objects of a class. It should return `True` if the given value is present in the container, and `False` otherwise.

```python
class MyList:
    def __init__(self, items):
        self.items = items

    def __contains__(self, value):
        return value in self.items

my_list = MyList([1, 2, 3, 4, 5])
print(2 in my_list) # Output: True
print(10 in my_list) # Output: False
```

These examples demonstrate how container operations can customize the behavior of Python objects. By defining these methods in a class definition, you can provide a more intuitive interface for interacting with objects and make your code more flexible and customizable.

### Comparison

Dunder methods can be used to define the behavior of comparison operators such as `==`, `<`, `>`, `<=`, and `>=` for objects of a class. These methods should return `True` or `False` depending on whether the comparison is true or false.

- **`__eq__()`**

The `__eq__()` method is used to compare two objects for equality. 

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

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

person1 = Person("Alice", 25)
person2 = Person("Bob", 30)
person3 = Person("Alice", 25)

print(person1 == person2) # Output: False
print(person1 == person3) # Output: True

```

- **`__lt__()`**

The `__lt__()` method is used to compare two objects to check if one is less than the other.

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

    def __lt__(self, other):
        return self.age < other.age

person1 = Person("Alice", 25)
person2 = Person("Bob", 30)

print(person1 < person2) # Output: True
print(person2 < person1) # Output: False

```

- **`__gt__()`**

The `__gt__()` method is used to compare two objects to check if one is greater than the other.

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

    def __gt__(self, other):
        return self.age > other.age

person1 = Person("Alice", 25)
person2 = Person("Bob", 30)

print(person1 > person2) # Output: False
print(person2 > person1) # Output: True

```

- **`__le__()`**

The `__le__()` method is used to compare two objects to check if one is less than or equal to the other. Here's an example:

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

    def __le__(self, other):
        return self.age <= other.age

person1 = Person("Alice", 25)
person2 = Person("Bob", 30)
person3 = Person("Charlie", 25)

print(person1 <= person2) # Output: True
print(person2 <= person1) # Output: False
print(person1 <= person3) # Output: True

```

- **`__ge__()`**

The `__ge__()` method is used to compare two objects to check if one is greater than or equal to the other. Here's an example:

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

    def __ge__(self, other):
        return self.age >= other.age

person1 = Person("Alice", 25)
person2 = Person("Bob", 30)
person3 = Person("Charlie", 25)

print(person1 >= person2) # Output: False
print(person2 >= person1) # Output: True
print(person1 >= person3) # Output: True

```

### Arithmetic Operations

Dunder methods can be used to define the behavior of arithmetic operators such as `+`, `-`, `*`, and `/` for objects of a class. These methods should return a new object that represents the result of the arithmetic operation.

- **`__add__()`**

The `__add__()` method is used to define the behavior of the `+` operator when applied to two objects. Here's an example:

```python
	class Vector:
	    def __init__(self, x, y):
	        self.x = x
	        self.y = y
	
	    def __add__(self, other):
	        return Vector(self.x + other.x, self.y + other.y)
	
	v1 = Vector(1, 2)
	v2 = Vector(3, 4)
	v3 = v1 + v2
	
	print(v3.x) # Output: 4
	print(v3.y) # Output: 6
	
```

- **`__sub__()`**

The `__sub__()` method is used to define the behavior of the `-` operator when applied to two objects. Here's an example:

```python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v2 - v1

print(v3.x) # Output: 2
print(v3.y) # Output: 2

```

- **`__mul__()`**

The `__mul__()` method is used to define the behavior of the `*` operator when applied to two objects. Here's an example:

```python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __mul__(self, other):
        return Vector(self.x * other, self.y * other)

v1 = Vector(1, 2)
v2 = v1 * 3

print(v2.x) # Output: 3
print(v2.y) # Output: 6

```

- **`__truediv__()`**

The `__truediv__()` method is used to define the behavior of the `/` operator when applied to two objects. Here's an example:

```python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __truediv__(self, other):
        return Vector(self.x / other, self.y / other)

v1 = Vector(3, 6)
v2 = v1 / 3

print(v2.x) # Output: 1.0
print(v2.y) # Output: 2.0
```

## Best Practices for Using Dunder Methods

While dunder methods can be a powerful tool for customizing the behavior of objects in Python, it is important to use them thoughtfully and appropriately. Here are some best practices for using dunder methods effectively:

- Use dunder methods to define object behavior in a way that is consistent with Python conventions and libraries.
- Use descriptive names for dunder methods that accurately reflect their purpose and behavior.
- Be aware of the specific names and behaviors of dunder methods used by Python itself, and use them appropriately when defining your own dunder methods.
- Use dunder methods judiciously. Too many can make your code more complex and harder to understand.
- Thoroughly test your dunder methods to ensure that they behave as expected and do not introduce unexpected side effects.

## Conclusion

Dunder methods are a powerful feature of Python that enables developers to customize the behavior of objects in a consistent and intuitive way. By using dunder methods effectively, you can make your code more readable, maintainable, and flexible. You can also ensure that it is consistent with the expectations of other Python developers. Understanding the usage of dunder methods is an important part of writing Pythonic code, and can help you become a more effective Python developer.