Custom Numeric Magic Methods

PythonPythonBeginner
Practice Now

Introduction

In this tutorial, we will cover Python magic methods related to numeric operations. Magic methods are special methods in Python classes that start and end with double underscores (__). They are also known as "dunder" methods (double underscores).

These magic methods allow you to define how instances of your class behave with certain operations, like addition or subtraction.

We'll cover the following sections:

  1. Unary Operators
  2. Binary Operators
  3. In-place Operations

Let's get started!

Unary Operators

Unary operations are operations that involve a single operand, such as negation, absolute value, etc.

Let's start with a simple object. In number.py, create a class named MyNumber that have an attribute value.

class MyNumber:
    def __init__(self, value: float):
        self.value = value

__neg__

The __neg__ magic method defines how the negation operation should behave. When you use the - operator on an instance of your class, this method is called.

    ## ... (previous code in number.py)

    def __neg__(self) -> 'MyNumber':
        """Returns the negation of the instance's value."""
        return MyNumber(-self.value)

__abs__

The __abs__ magic method defines how the absolute value operation should behave. When you use the abs() function on an instance of your class, this method is called.

    ## ... (previous code in number.py)

    def __abs__(self) -> 'MyNumber':
        """Returns the absolute value of the instance's value."""
        return MyNumber(abs(self.value))

__round__

The __round__ magic method defines how the rounding operation should behave. When you use the round() function on an instance of your class, this method is called.

    ## ... (previous code in number.py)

    def __round__(self, ndigits: int = None) -> 'MyNumber':
        """Rounds the instance's value to the nearest whole number or specified number of digits."""
        return MyNumber(round(self.value, ndigits))

__floor__

The __floor__ magic method defines how the floor operation should behave. When you use the math.floor() function on an instance of your class, this method is called.

## math module should be import at the top of number.py
import math

    ## ... (previous code in number.py)

    def __floor__(self) -> 'MyNumber':
        """Returns the largest integer less than or equal to the instance's value."""
        return MyNumber(math.floor(self.value))

__ceil__

The __ceil__ magic method defines how the ceiling operation should behave. When you use the math.ceil() function on an instance of your class, this method is called.

    ## ... (previous code in number.py)

    def __ceil__(self) -> 'MyNumber':
        """Returns the smallest integer greater than or equal to the instance's value."""
        return MyNumber(math.ceil(self.value))

Example: Using the Unary Operators

Now that we have defined the unary operators for our MyNumber class, let's see how they work in unary_example.py:

import math
from number import MyNumber

## Create a new MyNumber object
a = MyNumber(5)
## Use the __neg__ method with the print function
print(f'{a.value=}, {-a.value=}')  ## Output: a.value=5, -a.value=-5

## Create another new MyNumber object
a = MyNumber(-5)
## Use the __abs__ method with the print function
print(f'{a.value=}, {abs(a).value=}')  ## Output: a.value=-5, abs(a).value=5

## Create the third new MyNumber object
a = MyNumber(5.678)
## Use the __round__ method with the print function
print(f'{a.value=}, {round(a, 2).value=}')  ## Output: a.value=5.678, round(a, 2).value=5.68

## Use the __floor__ method with the print function
print(f'{a.value=}, {math.floor(a).value=}')  ## Output: a.value=5.678, math.floor(a).value=5

## Use the __ceil__ method with the print function
print(f'{a.value=}, {math.ceil(a).value=}')  ## Output: a.value=5.678, math.ceil(a).value=6

Then typing the following command in the terminal to execute the script.

python unary_example.py

Binary Operators

Binary operations are operations that involve two operands, such as arithmetic operations like addition, subtraction, multiplication, and division, as well as comparison operations like equality, inequality, less than, greater than, etc.

__add__

The __add__ magic method defines how the addition operation should behave. When you use the + operator on

instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __add__(self, other: 'MyNumber') -> 'MyNumber':
        """Returns the sum of the instance's value and the other instance's value."""
        return MyNumber(self.value + other.value)

__sub__

The __sub__ magic method defines how the subtraction operation should behave. When you use the - operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __sub__(self, other: 'MyNumber') -> 'MyNumber':
        """Returns the difference of the instance's value and the other instance's value."""
        return MyNumber(self.value - other.value)

__mul__

The __mul__ magic method defines how the multiplication operation should behave. When you use the * operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __mul__(self, other: 'MyNumber') -> 'MyNumber':
        """Returns the product of the instance's value and the other instance's value."""
        return MyNumber(self.value * other.value)

__truediv__

The __truediv__ magic method defines how the true division operation should behave. When you use the / operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __truediv__(self, other: 'MyNumber') -> 'MyNumber':
        """Returns the result of dividing the instance's value by the other instance's value."""
        return MyNumber(self.value / other.value)

__floordiv__

The __floordiv__ magic method defines how the floor division operation should behave. When you use the // operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __floordiv__(self, other: 'MyNumber') -> 'MyNumber':
        """Returns the largest integer less than or equal to the result of dividing the instance's value by the other instance's value."""
        return MyNumber(self.value // other.value)

__mod__

The __mod__ magic method defines how the modulo operation should behave. When you use the % operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __mod__(self, other: 'MyNumber') -> 'MyNumber':
        """Returns the remainder of dividing the instance's value by the other instance's value."""
        return MyNumber(self.value % other.value)

__pow__

The __pow__ magic method defines how the power operation should behave. When you use the ** operator or the pow() function on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __pow__(self, other: 'MyNumber') -> 'MyNumber':
        """Returns the instance's value raised to the power of the other instance's value."""
        return MyNumber(self.value ** other.value)

Example: Using the Binary Operators

Now that we have defined the binary operators for our MyNumber class, let's see how they work in binary_example.py:

from number import MyNumber

## Create two new MyNumber objects
a = MyNumber(5)
b = MyNumber(3)
print(f'{a.value=}, {b.value=}') ## Output: a.value=5, b.value=3

## Use the __add__ method with the print function
print(f'{(a+b).value=}')  ## Output: (a+b).value=8

## Use the __sub__ method with the print function
print(f'{(a-b).value=}')  ## Output: (a-b).value=2

## Use the __mul__ method with the print function
print(f'{(a*b).value=}')  ## Output: (a*b).value=15

## Use the __truediv__ method with the print function
print(f'{(a/b).value=}')  ## Output: (a/b).value=1.6666666666666667

## Use the __floordiv__ method with the print function
print(f'{(a//b).value=}')  ## Output: (a//b).value=1

## Use the __mod__ method with the print function
print(f'{(a%b).value=}')  ## Output: (a%b).value=2

## Use the __pow__ method with the print function
print(f'{(a**b).value=}')  ## Output: (a**b).value=125

Then typing the following command in the terminal to execute the script.

python binary_example.py

In-place Operations

In-place operations are operations that modify the value of an object in place, without creating a new object. They are denoted by the augmented assignment operators such as +=, -=, *=, /=, etc.

If the in-place operator is not defined for a Python class, then the binary operator will be used instead when an in-place operation is attempted.

There is an example in inplace_example1.py, change the binary operators to in-place operators:

from number import MyNumber

## Create two new MyNumber objects
a = MyNumber(5)
b = MyNumber(3)
print(f'{a.value=}, {b.value=}') ## Output: a.value=5, b.value=3

a += b
## Use the __add__ method with the print function
print(f'after a+=b: {a.value=}')  ## Output:after a+=b: (a+b).value=8

To run the example, type the following command in the terminal:

python inplace_example1.py

The result shows that the __add__ was used when attempted the += operation.

Then we will implement in-place operations in MyNumber, and their behaviour with the corresponding binary operators will be slightly different.

__iadd__

The __iadd__ magic method defines how the in-place addition operation should behave. When you use the += operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __iadd__(self, other: 'MyNumber') -> 'MyNumber':
        """Adds the other instance's value to the instance's value in-place."""
        print(f'input: {self.value=}, {other.value=}')
        self.value += other.value
        print(f'after +=: {self.value=}')
        return self

__isub__

The __isub__ magic method defines how the in-place subtraction operation should behave. When you use the -= operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __isub__(self, other: 'MyNumber') -> 'MyNumber':
        """Subtracts the other instance's value from the instance's value in-place."""
        print(f'input: {self.value=}, {other.value=}')
        self.value -= other.value
        print(f'after -=: {self.value=}')
        return self

__imul__

The __imul__ magic method defines how the in-place multiplication operation should behave. When you use the *= operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __imul__(self, other: 'MyNumber') -> 'MyNumber':
        """Multiplies the instance's value by the other instance's value in-place."""
        print(f'input: {self.value=}, {other.value=}')
        self.value *= other.value
        print(f'after *=: {self.value=}')
        return self

__itruediv__

The __itruediv__ magic method defines how the in-place true division operation should behave. When you use the /= operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __itruediv__(self, other: 'MyNumber') -> 'MyNumber':
        """Divides the instance's value by the other instance's value in-place."""
        print(f'input: {self.value=}, {other.value=}')
        self.value /= other.value
        print(f'after /=: {self.value=}')
        return self

__ifloordiv__

The __ifloordiv__ magic method defines how the in-place floor division operation should behave. When you use the //= operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __ifloordiv__(self, other: 'MyNumber') -> 'MyNumber':
        """Performs in-place floor division on the instance's value by the other instance's value."""
        print(f'input: {self.value=}, {other.value=}')
        self.value //= other.value
        print(f'after //=: {self.value=}')
        return self

__imod__

The __imod__ magic method defines how the in-place modulo operation should behave. When you use the %= operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __imod__(self, other: 'MyNumber') -> 'MyNumber':
        """Performs in-place modulo operation on the instance's value by the other instance's value."""
        print(f'input: {self.value=}, {other.value=}')
        self.value %= other.value
        print(f'after %=: {self.value=}')
        return self

__ipow__

The __ipow__ magic method defines how the in-place power operation should behave. When you use the **= operator on instances of your class, this method is called.

    ## ... (previous code in number.py)

    def __ipow__(self, other: 'MyNumber') -> 'MyNumber':
        """Raises the instance's value to the power of the other instance's value in-place."""
        print(f'input: {self.value=}, {other.value=}')
        self.value **= other.value
        print(f'after **=: {self.value=}')
        return self

Example: Using the In-place Operations

Now that we have defined the in-place operators for our MyNumber class, let's see how they work in example3.py:

from number import MyNumber

## Create a new MyNumber objects
a = MyNumber(13)

## Use the __iadd__ method
a += MyNumber(5)
## Output:
## input: self.value=13, other.value=5
## after +=: self.value=18

## Use the __isub__ method
a -= MyNumber(5)
## Output:
## input: self.value=18, other.value=5
## after -=: self.value=13

## Use the __imul__ method
a *= MyNumber(5)
## Output:
## input: self.value=13, other.value=5
## after *=: self.value=65

## Use the __itruediv__ method
a /= MyNumber(5)
## Output:
## input: self.value=65, other.value=5
## after /=: self.value=13.0

## Use the __ifloordiv__ method
a //= MyNumber(2)
## Output:
## input: self.value=13.0, other.value=2
## after //=: self.value=6.0

## Use the __imod__ method
a %= MyNumber(4)
## Output:
## input: self.value=6.0, other.value=4
## after %=: self.value=2.0

## Use the __ipow__ method
a **= MyNumber(3)
## Output:
## input: self.value=2.0, other.value=3
## after **=: self.value=8.0

Then typing the following command in the terminal to execute the script.

python inplace_example2.py

Summary

In this tutorial, we explored Python magic methods related to numeric operations, which allow you to define custom behavior for your classes when interacting with different types of numeric operations. We covered unary operators, binary operators, and in-place operations, learning how to implement each magic method along the way.

By implementing these magic methods in your custom classes, you can create intuitive and easy-to-use objects that work seamlessly with standard Python operations. This not only improves the readability of your code but also makes it more maintainable and user-friendly.

As you continue to develop your Python skills, consider experimenting with other magic methods to further customize the behavior of your classes and create even more powerful abstractions.

Other Python Tutorials you may like