In Python, just as you can use operators like + and - with numeric types (int and float), you can also define the behavior of these operators for classes (objects) you create yourself. This is called “Operator Overloading.”
For example, when dealing with vector calculations, matrix calculations, or custom currency classes, this allows for intuitive coding. This article explains how to implement special methods (magic methods) corresponding to arithmetic operations, with concrete code examples.
List of Special Methods for Arithmetic Operators
The special methods corresponding to each operator are as follows. By defining (overriding) these within a class, you can determine the behavior when that operator is used.
| Operator | Special Method | Meaning |
+ | __add__ | Addition |
- | __sub__ | Subtraction |
* | __mul__ | Multiplication |
/ | __truediv__ | True Division (Standard division) |
// | __floordiv__ | Floor Division (Integer division) |
% | __mod__ | Modulo (Remainder of division) |
** | __pow__ | Exponentiation (Power) |
Implementation Example: 2D Vector Class
As an example, let’s create a Vector class representing a mathematical vector (having x and y components). We will implement addition and subtraction between vectors, and multiplication and division with scalars (numbers).
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
# Display clearly for debugging
return f"Vector({self.x}, {self.y})"
# --- Addition (+) ---
def __add__(self, other):
# Addition between Vectors
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented
# --- Subtraction (-) ---
def __sub__(self, other):
# Subtraction between Vectors
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
return NotImplemented
# --- Multiplication (*) ---
def __mul__(self, scalar):
# Vector * Scalar (Number)
if isinstance(scalar, (int, float)):
return Vector(self.x * scalar, self.y * scalar)
return NotImplemented
# --- True Division (/) ---
def __truediv__(self, scalar):
# Vector / Scalar (Number)
if isinstance(scalar, (int, float)):
if scalar == 0:
raise ValueError("Division by zero is not allowed.")
return Vector(self.x / scalar, self.y / scalar)
return NotImplemented
# --- Floor Division (//) ---
def __floordiv__(self, scalar):
# Vector // Scalar (Number)
if isinstance(scalar, (int, float)):
if scalar == 0:
raise ValueError("Division by zero is not allowed.")
return Vector(self.x // scalar, self.y // scalar)
return NotImplemented
# --- Modulo (%) ---
def __mod__(self, scalar):
# Vector % Scalar (Number)
if isinstance(scalar, (int, float)):
if scalar == 0:
raise ValueError("Division by zero is not allowed.")
return Vector(self.x % scalar, self.y % scalar)
return NotImplemented
# --- Exponentiation (**) ---
def __pow__(self, exponent):
# Vector ** Exponent
if isinstance(exponent, (int, float)):
return Vector(self.x ** exponent, self.y ** exponent)
return NotImplemented
# --- Verification ---
v1 = Vector(10, 20)
v2 = Vector(3, 4)
print(f"v1: {v1}")
print(f"v2: {v2}")
print("-" * 20)
# Addition and Subtraction
print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
# Multiplication (Scalar)
print(f"v1 * 2 = {v1 * 2}")
# Division
print(f"v1 / 4 = {v1 / 4}") # Result includes float
print(f"v1 // 3 = {v1 // 3}") # Result is int (floored)
# Modulo and Exponentiation
print(f"v1 % 3 = {v1 % 3}")
print(f"v2 ** 2 = {v2 ** 2}")
Output:
v1: Vector(10, 20)
v2: Vector(3, 4)
--------------------
v1 + v2 = Vector(13, 24)
v1 - v2 = Vector(7, 16)
v1 * 2 = Vector(20, 40)
v1 / 4 = Vector(2.5, 5.0)
v1 // 3 = Vector(3, 6)
v1 % 3 = Vector(1, 2)
v2 ** 2 = Vector(9, 16)
Implementation Points
1. Type Checking (isinstance)
It is important to check if the operand is of the expected type. For example, in __add__, calculations are performed only if the other operand (other) is also a Vector class. In __mul__, it handles cases where the operand is a number (int or float).
2. Returning NotImplemented
If the types do not match or the operation is undefined, Python convention dictates returning NotImplemented instead of raising an error (TypeError). This allows the Python interpreter to determine “calculation is not possible with this class” and leaves room to try the reverse operation (e.g., other.__radd__) on the object on the right side of the operator.
3. Returning a New Instance
Generally, the result of an operation should return a new instance rather than modifying itself (self) (immutable behavior). This prevents side effects where v1‘s value changes when executing v3 = v1 + v2.
4. Handling Zero Division
For division-related operations (/, //, %), you can enhance safety by writing code to raise a ValueError or ZeroDivisionError when the denominator is 0.
Summary
- By defining special methods like
__add__and__sub__, you can use+and-with custom classes. - Check the type of the other operand; if calculable, return a new instance, otherwise return
NotImplemented. - This enables intuitive coding and improves the usability of the class.
