Python Grab Bag: Special Methods
Introduction
Special Methods in Python, also known as magic methods or dunders, are methods that allow special syntax to operate on objects. Special methods start and end with double underscores, thus the name dunders. For example, objects that have a __len__ method can have the builtin len() function called on them; objects that have the __iter__ method can be used in iteration with for i in object. We’re going to go over a couple of implementations, but the options are basically limitless.
Addition with Animal Objects
The addition operator can be used on custom objects. Let’s implement a cat class, and when we + a male cat and female cat, we should get a kitten.
class Cat():
def __init__(self, gender):
self.gender = gender
def __add__(self, other):
# just give the gender of the first Cat
if self.gender != other.gender:
return Kitten(self.gender)
else:
return None
class Kitten(Cat):
def __init__(self, gender):
super(gender)
male_cat = Cat('male')
female_cat = Cat('female')
# Creating a Kitten!
print(male_cat + female_cat)
# <__main__.Kitten object at 0x7fb92d738a90>
Iteration over a Tree
Trees are data structures that can be accessed in a variety of ways. Let’s implement a binary tree, which is a tree with a root and two nodes that are either leaves, empty, or another binary tree.
# Binary tree implementation, just sticking to integers for simplicity
class IntegerBinaryTree():
def __init__(self, value, left, right):
# Nodes can be None or IntegerBinaryTree
self.value = value
self.left = left
self.right = right
To iterate through this binary tree, it requires its own __iter__ and an additional BinaryTreeIterator class that itself implements __iter__ and __next__. Using the for i in object syntax, Python will call the iterator’s __next__ method until a StopIteration exception is raised. When IntegerBinaryTree’s __iter__ method is called, it needs to return an instance of BinaryTreeIterator. BinaryTreeIterator will iterate through instances of IntegerBinaryTree depth first, though many implementations are possible. Depth first will require a stack, while breadth first requires a queue.
# BinaryTreeIterator class with the required methods implemented
# I did this from scratch, probably not the best implementation but it works
class BinaryTreeIterator():
def __init__(self, b_tree):
self.stack = []
self.current = b_tree
def __iter__(self):
return self
def __next__(self):
if not self.current:
raise StopIteration
value = self.current.value
if (left := self.current.left) is not None:
self.stack.append(self.current)
self.current = left
elif (right := self.current.right) is not None:
self.current = right
else:
while True:
try:
if (right := self.stack.pop().right) is not None:
self.current = right
break
except IndexError:
self.current = None
break
return value
class IntegerBinaryTree():
def __init__(self, value, left, right):
self.value = value
self.left = left
self.right = right
def __iter__(self):
return BinaryTreeIterator(self)
Now that the iter method is defined, it can be tested with a binary tree.
1
/ \
2 6
/ \ / \
3 4 7 9
/ \ / \
5 8 10 11
eleven = IntegerBinaryTree(11, None, None)
ten = IntegerBinaryTree(10, None, None)
nine = IntegerBinaryTree(9, ten, eleven)
eight = IntegerBinaryTree(8, None, None)
seven = IntegerBinaryTree(7, None, eight)
six = IntegerBinaryTree(6, seven, nine)
five = IntegerBinaryTree(5, None, None)
four = IntegerBinaryTree(4, five, None)
three = IntegerBinaryTree(3, None, None)
two = IntegerBinaryTree(2, three, four)
root = IntegerBinaryTree(1, two, six)
for value in root:
print(value)
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
# 10
# 11