Advanced Python Operator Overloading with Custom Point Class
Operator Overloading in Python
Operator overloading is a fundamental concept in object-oriented programming that allows developers to define custom behaviors for standard operators when applied to user-defined types. In Python, this powerful feature enables us to create more intuitive and natural interactions with our custom classses. Let's explore how to implement operator overloading using a custom Point class as our example.
The Coordinate Class
First, let's define a simple Coordinate class that represents a point in 2D space with x and y coordinates.
class Coordinate:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __repr__(self):
return f'Coordinate({self.x}, {self.y})'
def __str__(self):
return f'({self.x}, {self.y})'
Testing the basic functionality:
c1 = Coordinate()
print(c1) # Output: (0, 0)
print(repr(c1)) # Output: Coordinate(0, 0)
c2 = Coordinate(3, 7)
print(c2) # Output: (3, 7)
Implementing Special Methods
Python provides special methods (also known as magic methods) that allow us to customize the behavior of our objects. Let's implement some of these methods to our Coordinate class.
Index Access Methods
class Coordinate:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __repr__(self):
return f'Coordinate({self.x}, {self.y})'
def __getitem__(self, index):
if index in range(-2, 2):
return self.y if index in (1, -1) else self.x
raise IndexError("Index out of range")
def __setitem__(self, index, value):
if index in (0, -2):
self.x = value
elif index in (1, -1):
self.y = value
else:
raise IndexError("Index out of range.")
Testing index access:
c = Coordinate(4, 9)
print(c[0], c[1]) # Output: 4 9
print(c[-2], c[-1]) # Output: 4 9
c[0] = 10
print(c) # Output: (10, 9)
c[1] = 20
print(c) # Output: (10, 20)
Length and Boolean Methods
class Coordinate:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __repr__(self):
return f'Coordinate({self.x}, {self.y})'
def __len__(self):
return 2
def __bool__(self):
return self.x >= 0 and self.y >= 0
Testing length and boolean:
c = Coordinate(3, 4)
print(len(c)) # Output: 2
print(bool(c)) # Output: True
c = Coordinate(-1, 5)
print(bool(c)) # Output: False
Unary Operators
class Coordinate:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __repr__(self):
return f'Coordinate({self.x}, {self.y})'
def __neg__(self):
return Coordinate(-self.x, -self.y)
def __pos__(self):
return Coordinate(self.x + 1, self.y), Coordinate(self.x, self.y + 1)
def __abs__(self):
return Coordinate(abs(self.x), abs(self.y))
Testing unary operators:
c = Coordinate(-3, 5)
print(-c) # Output: Coordinate(3, -5)
print(+c) # Output: (Coordinate(4, 5), Coordinate(-3, 6))
print(abs(c)) # Output: Coordinate(3, 5)
Callable Method
class Coordinate:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __repr__(self):
return f'Coordinate({self.x}, {self.y})'
def __call__(self, dx=0, dy=0):
return Coordinate(self.x + dx, self.y + dy)
Testing callable:
c = Coordinate(2, 3)
new_c = c(4, 5)
print(new_c) # Output: Coordinate(6, 8)
Comparison Operators
Let's implement comparison operators to compare Coordinate objects.
class Coordinate:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __repr__(self):
return f'Coordinate({self.x}, {self.y})'
def __eq__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x and self.y == other.y
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x and self.y < other.y
def __gt__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x < other.x and self.y == other.y
def __le__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x and abs(self.y - other.y) == 1
def __ge__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.y == other.y and abs(self.x - other.x) == 1
Testing comparison operators:
c1 = Coordinate(2, 5)
c2 = Coordinate(2, 5)
c3 = Coordinate(3, 5)
print(c1 == c2) # Output: True
print(c1 != c3) # Output: True
print(c1 < c2) # Output: False
print(c1 > c3) # Output: False
print(c1 <= c2) # Output: True
print(c1 >= c3) # Output: False
Bitwise Operators
We can also overload bitwise operators for our Coordinate class.
class Coordinate:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __repr__(self):
return f'Coordinate({self.x}, {self.y})'
def __and__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x != other.x and self.y != other.y
def __or__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x or self.y == other.y
def __xor__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return (self.x == other.x and self.y != other.y) or (self.x != other.x and self.y == other.y)
def __invert__(self):
return Coordinate(self.y, self.x)
def __lshift__(self, other):
if not isinstance(other, int):
return NotImplemented
return Coordinate(self.x + other, self.y)
def __rshift__(self, other):
if not isinstance(other, int):
return NotImplemented
return Coordinate(self.x, self.y + other)
Testing bitwise operators:
c1 = Coordinate(1, 2)
c2 = Coordinate(3, 4)
print(c1 & c2) # Output: True
print(c1 | c2) # Output: False
print(c1 ^ c2) # Output: True
print(~c1) # Output: Coordinate(2, 1)
print(c1 << 2) # Output: Coordinate(3, 2)
print(c1 >> 3) # Output: Coordinate(1, 5)
Arithmetic Operators
Finally, let's implement arithmetic operators for our Coordinate class.
class Coordinate:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __repr__(self):
return f'Coordinate({self.x}, {self.y})'
def __add__(self, other):
if hasattr(other, '__getitem__') and len(other) == 2:
return Coordinate(self.x + other[0], self.y + other[1])
return NotImplemented
def __sub__(self, other):
if hasattr(other, '__getitem__') and len(other) == 2:
dx, dy = tuple(map(float, other))
return ((self.x - dx)**2 + (self.y - dy)**2)**0.5
return NotImplemented
def __mul__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return (self.x == other.x and abs(self.y - other.y) == 1) or (self.y == other.y and abs(self.x - other.x) == 1)
def __truediv__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
if self.x == other.x and self.y != other.y:
return 1 if self.y < other.y else -1
if self.y == other.y and self.x != other.x:
return 1 if self.x < other.x else -1
return False
def __pow__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
if self.x == other.x and self.y != other.y:
step = 1 if self.y < other.y else -1
return [Coordinate(x, self.y) for x in range(self.x + step, other.x, step)]
if self.y == other.y and self.x != other.x:
step = 1 if self.x < other.x else -1
return [Coordinate(self.x, y) for y in range(self.y + step, other.y, step)]
return None
Testing arithmetic operators:
c1 = Coordinate(1, 2)
c2 = Coordinate(4, 2)
c3 = Coordinate(4, 5)
print(c1 + c2) # Output: Coordinate(5, 4)
print(c1 - c2) # Output: 3.0
print(c1 * c2) # Output: True
print(c1 / c2) # Output: 1
print(c1 ** c2) # Output: [Coordinate(2, 2), Coordinate(3, 2)]
Complete Implementation
Here's the complete Coordinate class with all the implemented special methods:
class Coordinate:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def __repr__(self):
return f'Coordinate({self.x}, {self.y})'
def __str__(self):
return f'({self.x}, {self.y})'
def __getitem__(self, index):
if index in range(-2, 2):
return self.y if index in (1, -1) else self.x
raise IndexError("Index out of range")
def __setitem__(self, index, value):
if index in (0, -2):
self.x = value
elif index in (1, -1):
self.y = value
else:
raise IndexError("Index out of range.")
def __len__(self):
return 2
def __abs__(self):
return Coordinate(abs(self.x), abs(self.y))
def __bool__(self):
return self.x >= 0 and self.y >= 0
def __neg__(self):
return Coordinate(-self.x, -self.y)
def __pos__(self):
return Coordinate(self.x + 1, self.y), Coordinate(self.x, self.y + 1)
def __call__(self, dx=0, dy=0):
return Coordinate(self.x + dx, self.y + dy)
def __eq__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x and self.y == other.y
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x and self.y < other.y
def __gt__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x < other.x and self.y == other.y
def __le__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x and abs(self.y - other.y) == 1
def __ge__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.y == other.y and abs(self.x - other.x) == 1
def __and__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x != other.x and self.y != other.y
def __or__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return self.x == other.x or self.y == other.y
def __xor__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return (self.x == other.x and self.y != other.y) or (self.x != other.x and self.y == other.y)
def __invert__(self):
return Coordinate(self.y, self.x)
def __lshift__(self, other):
if not isinstance(other, int):
return NotImplemented
return Coordinate(self.x + other, self.y)
def __rshift__(self, other):
if not isinstance(other, int):
return NotImplemented
return Coordinate(self.x, self.y + other)
def __add__(self, other):
if hasattr(other, '__getitem__') and len(other) == 2:
return Coordinate(self.x + other[0], self.y + other[1])
return NotImplemented
def __sub__(self, other):
if hasattr(other, '__getitem__') and len(other) == 2:
dx, dy = tuple(map(float, other))
return ((self.x - dx)**2 + (self.y - dy)**2)**0.5
return NotImplemented
def __mul__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
return (self.x == other.x and abs(self.y - other.y) == 1) or (self.y == other.y and abs(self.x - other.x) == 1)
def __truediv__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
if self.x == other.x and self.y != other.y:
return 1 if self.y < other.y else -1
if self.y == other.y and self.x != other.x:
return 1 if self.x < other.x else -1
return False
def __pow__(self, other):
if not isinstance(other, Coordinate):
return NotImplemented
if self.x == other.x and self.y != other.y:
step = 1 if self.y < other.y else -1
return [Coordinate(x, self.y) for x in range(self.x + step, other.x, step)]
if self.y == other.y and self.x != other.x:
step = 1 if self.x < other.x else -1
return [Coordinate(self.x, y) for y in range(self.y + step, other.y, step)]
return None
This implementation demonstrates the power and flexibility of Python's operator overloading capabilities, allowing us to create intuitive and natural interactions with our custom Coordinate class.