Lately, it’s been somewhat hard for me to commit to writing anything actually practical, so I hope y’all don’t mind if I continue down the rabbit hole of abusing programming language features. For instance, last time I wrote about the different ways we could implement addition without using the addition operator. Now, I’m interested in the flip side: overloading operators to have unexpected behaviors.
Table of Contents
- Introduction to Operator Overloading
- Overloading the Add Method for Custom Classes
- How to Go Completely Overboard With Operator Overloading
Introduction to Operator Overloading
In Python, every time we create a class, we have the opportunity to define a variety of standard methods. For example, if you want a string representation of your object, you can override the __str__
and __repr__
methods.
Interestingly, Python also provides facilities for performing basic operations between two objects of the same type. That’s why we can add two numbers together, concatenate two strings together, and merge two lists together with the same +
operator:
4 + 5 # evaluates to 9 [4] + [5] # evaluates to [4, 5] "4" + "5" # evaluates to '45'
To overload the + operator for some class, we only have to implement the __add__
method. As a result, I’m interested in just how much we could abuse this feature to produce comical results. Let’s give it a try!
Overloading the Add Method for Custom Classes
To start, I had this funny idea to model products perhaps due to the proximity to Prime day. As a result, we can make a really simple class representing products that we might want to purchase:
class Product: def __init__(self, name: str, price: float): self.name = name self.price = price
Now, with just these two fields, we could overload the addition operator for our products. Right off the bat, however, it’s not immediately clear what this addition operator should do, which I think makes it an excellent candidate for abuse!
Adding Just One of the Fields
To start, I’m going to implement the add operator to provide the sum of the prices of the two products:
class Product: def __init__(self, name: str, price: float): self.name = name self.price = price def __add__(self, other): return self.price + other.price
As a result, we can add two products together as follows:
apple = Product("Apple", 5) pear = Product("Pear", 6) apple + pear # evaluates to 11
This is kind of weird for a couple of reasons. First, we’re very limited in how this works since we can’t add more than two objects in an expected way:
apple = Product("Apple", 5) pear = Product("Pear", 6) lemon = Product("Lemon", 3) apple + pear + lemon Traceback (most recent call last): File "<pyshell#19>", line 1, in <module> apple + pear + lemon TypeError: unsupported operand type(s) for +: 'int' and 'Product'
Which means we certainly can’t take advantage of existing functions like sum:
sum([apple, pear]) Traceback (most recent call last): File "<pyshell#21>", line 1, in <module> sum([apple, pear]) TypeError: unsupported operand type(s) for +: 'int' and 'Product'
Second, we probably expect the addition operator to combine the objects—perhaps into some new product via alchemy. At least, that’s how the add operator worked in the examples above.
Taken together, this means that we’ve properly abused the add operator, but it’s not like things make a lot more sense with these issues addressed. In other words, what if we tried to combine these products, so they return a new product?
Adding the Entire Object
The next approach is to find a way to combine the two objects, so information from both of the objects is merged in some meaningful way.
class Product: def __init__(self, name: str, price: float): self.name = name self.price = price def __add__(self, other): return Product(self.name + other.name, self.price + other.price)
In this new implementation, we lazily merge the two products by concatenating their names and summing their prices. The result is this sort of Dr. Seuss style of product alchemy:
apple = Product("Apple", 5) pear = Product("Pear", 6) apple_pear = apple + pear apple_pear.name # evaluates to 'ApplePear' apple_pear.price # evaluates to 11
Though, I suppose we could have a bit more fun with it use prefixes and suffixes:
class Product: def __init__(self, name: str, price: float): self.name = name self.price = price def __add__(self, other): self_mid = len(self.name) // 2 other_mid = len(other.name) // 2 return Product(self.name[:self_mid] + other.name[-other_mid:], self.price + other.price)
The result being a slightly more interesting merging of names:
apple = Product("Apple", 5) pear = Product("Pear", 6) apple_pear = apple + pear apple_pear.name # evaluates to 'Apar' apple_pear.price # evaluates to 11
And of course, the addition itself isn’t commutative, so we get these incredibly interesting variations in products based solely on the order in which they’re combined:
apple = Product("Apple", 5) pear = Product("Pear", 6) pear_apple = pear + apple pear_apple.name # evaluates to 'Pele' pear_apple.price # evaluates to 11
We also grant ourselves another hilarious result which is the ability to chain addition across multiple products:
apple = Product("Apple", 5) pear = Product("Pear", 6) lemon = Product("Lemon", 3) homunculus = apple + pear + lemon homunculus.name # evaluates to 'Apon' homunculus.price # evaluates to 14
However, I don’t love that we lost the pear information from the product name. Now, it’s indistinguishable from the combination between an apple and a lemon, though I suppose that’s out of the scope for today!
For what we’ve lost, we’ve gained the wonderful ability to take advantage of the variety of iterable methods, such as sum:
homunculus = sum([pear, lemon], start=apple) homunculus.name # evaluates to 'Apon' homunculus.price # evaluates to 14
Now that’s goofy!
How to Go Completely Overboard With Operator Overloading
Up to this point, we’ve had some fun with implementing the addition operator for a class that otherwise probably shouldn’t implement it. What’s wonderful about that is that Python doesn’t just expose the addition operator, it also exposes all of these operators:
object.sub(self, other)
object.mul(self, other)
object.matmul(self, other)
object.truediv(self, other)
object.floordiv(self, other)
object.mod(self, other)
object.divmod(self, other)
object.pow(self, other[, modulo])
object.lshift(self, other)
object.rshift(self, other)
object.and(self, other)
object.xor(self, other)
object.or(self, other)
If you were to implement many of these as well as their in-place and augmented versions, you could probably develop your own tiny Brainfuck-like programming language within Python. Surely, if you implement enough of them, the following code would make sense:
apple = Product("Apple", 5) pear = Product("Pear", 6) -(apple or pear)**(apple++)
I have no clue what it would do, but the very idea of overloading this many operators is hilarious to me. Ultimately, I think if you find a way to implement them all, you unlock sentience, so be careful.
At any rate, as much of a shitpost as this seems, there are languages like Perl that went down the operator rabbit hole unironically. In fact, I was fairly skeptical when Python first introduced the walrus operator as I’m generally against polluting a language with more obscure symbols. So, hopefully this article serves as some silly evidence for that.
With that said, I’m going to wrap it up for today. If you liked this article and want to read more like it, check out any of the following:
- How to Obfuscate Code in Python: A Thought Experiment
- Where Do Foo, Bar, and Baz Come From in Programming?
- 5 Absurd Ways to Add Two Numbers in Python
And as always, you can help out by heading over to my list of ways to grow the site. Otherwise, take care!
Recent Posts
Teaching at the collegiate level is a wonderful experience, but it's not always clear what's involved or how you get there. As a result, I figured I'd take a moment today to dump all my knowledge for...
It's been a weird week. I'm at the end of my degree program, but it's hard to celebrate. Let's talk about it.