Overloaded overloading
"If Python doesn't support overloading, how does '+' work which can be either addition or concatenation, correct?"
My collegue Peter asked me this once. Let me explain this question, then answer it. And it starts with the word "overloading", which has two different meanings in Python.
When you write a class in Python, you can have only one version of each method. For example, this doesn't work:
- import math
-
- class Point:
- def __init__(self, x, y):
- self.x, self.y = x, y
-
- def distance(self, other_point):
- # Distance from this point to another point
- return math.sqrt(
- (self.x-other_point.x)**2 +
- (self.y-other_point.y)**2 )
-
- def distance(self, x, y):
- # Distance from this point to other x-y coords
- return math.sqrt( (self.x-x)**2 + (self.y-y)**2 )
See the two distance() methods? See how it's defined twice?
In Python, only the second version is kept. The first one is silently overwritten and erased. If you call Point.distance() with one argument - matching the first distance(), but not the second - you will get an error.
But if you translate this class to Java, the situation is different. Java lets you define both versions of distance(), and Java will use one or the other, depending on how many arguments you pass in. This is called "method overloading".
But Python doesn't let you do that. Peter knew this, and that's why he wondered about the "+" operator - and why Python lets you use it for different operations. Doesn't that conflict with Python's "no overloading" rule?
The answer is that while Python does not support method overloading...
It DOES support operator overloading.
And that's why you can use "+" for both strings and numbers, and in fact you can make it work with your own classes as well - using "magic method" hooks. For the "+" operator, we do that by defining a method called __add__. For example, in Point:
- class Point:
- def __init__(self, x, y):
- self.x, self.y = x, y
- # ...
- def __add__(self, other):
- return Point(self.x + other.x, self.y + other.y)
This "__add__" method can be defined on any of your classes. When you do, it lets you add instances of them together:
>>> p = Point(1,1) + Point(2,2) >>> print(p.x, p.y) 3 3
If you check, you'll find both float() and str() have their own __add__ methods. And they implement addition and string concatenation, respectively.
See how this works? When we're talking about programming languages, "Overloading" refers to two different things. And Python does one of them, but not the other.
(You might say the term "overloaded" is... overloaded.)
P.S. Going back to the Point class - what would you do if you really need overloading for the distance() method?
Answer: There's two ways to do it. The more general method is to implement the method overloading yourself, by making distance() a dispatching method. That means its job is not to make any calculations directly; but rather determine which method should handle it, and then call it.
Typically you do this with generic argument names, and checking types with isinstance(), or the presence/absence of certain arguments:
- import math
- class Point:
- def __init__(self, x, y):
- self.x, self.y = x, y
- def distance(self, first, second=None):
- if isinstance(first, Point):
- # distance to other Point
- assert second is None
- return self.distance_to_other(first)
- elif isinstance(first, tuple):
- # distance to (x, y) tuple
- assert second is None
- return self.distance_to_pair(first)
- else:
- # distance to coordinates
- assert second is not None
- return self.distance_to_coordinates(first, second)
- def distance_to_coordinates(self, x, y):
- 'Distance from this point to other x-y coordinates'
- return math.sqrt( (self.x-x)**2 + (self.y-y)**2 )
- def distance_to_other(self, other_point):
- 'Distance from this point to another point'
- return self.distance_to_coordinates(
- other_point.x, other_point.y)
- def distance_to_pair(self, pair):
- 'Distance from this point to (x, y) tuple'
- x, y = pair
- return self.distance_to_coordinates(x, y)
The second and generally better way is to use the @singledispatch decorator, in Python's functools module. That lets you cleanly register different "dispatch" methods based on the type of the first argument.
That doesn't work with the Point class here, though, because its dispatching logic is too complex. But if you ever need to do something like this, try to use @singledispatch first.